We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Cloud CLI to register a new project in Meltano Cloud
cc @tayloramurphy

Blocked by:
- https://github.com/meltano/infra/issues/514

Related to:

- https://github.com/meltano/infra/issues/513 (internal)
- https://github.com/meltano/meltano/issues/7411



--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/meltano/cloud/cli/project.py`
Content:
```
1 """Meltano Cloud `project` command."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 import logging
7 import sys
8 import typing as t
9 
10 import click
11 import questionary
12 from ulid import ULID
13 
14 from meltano.cloud.api.client import MeltanoCloudClient
15 from meltano.cloud.cli.base import (
16     LimitedResult,
17     get_paginated,
18     pass_context,
19     print_formatted_list,
20 )
21 from meltano.core.utils import run_async
22 
23 if t.TYPE_CHECKING:
24     from meltano.cloud.api.config import MeltanoCloudConfig
25     from meltano.cloud.api.types import CloudProject
26     from meltano.cloud.cli.base import MeltanoCloudCLIContext
27 
28 DEFAULT_GET_PROJECTS_LIMIT = 125
29 MAX_PAGE_SIZE = 250
30 
31 logger = logging.getLogger()
32 
33 
34 class ULIDType(click.ParamType):
35     """A ULID input type.
36 
37     Examples:
38         01BX5ZZKBKACTAV9WEVGEMMVRY
39         01BX5ZZKBKACTAV9WEVGEMMVS1
40     """
41 
42     name = "ulid"
43 
44     def convert(
45         self,
46         value: str | ULID,
47         param: click.Parameter | None,  # noqa: ARG002
48         ctx: click.Context | None,  # noqa: ARG002
49     ) -> str:
50         """Try converting value to a ULID object."""
51         if isinstance(value, ULID):
52             return str(value)
53 
54         if isinstance(value, str):
55             try:
56                 ULID.from_str(value)
57             except ValueError:
58                 self.fail(f"Invalid ULID value: {value}")
59 
60         return value
61 
62 
63 class ProjectsCloudClient(MeltanoCloudClient):
64     """A Meltano Cloud client with extensions for projects."""
65 
66     async def get_projects(
67         self,
68         *,
69         project_id: str | None = None,
70         project_name: str | None = None,
71         page_size: int | None = None,
72         page_token: str | None = None,
73     ):
74         """Use GET to get Meltano Cloud project projects.
75 
76         Args:
77             project_id: The Meltano Cloud ID for the project.
78             project_name: The name of the project.
79             page_size: The number of items to request per page.
80             page_token: The page token.
81         """
82         async with self.authenticated():
83             return await self._json_request(
84                 "GET",
85                 f"/projects/v1/{self.config.tenant_resource_key}",
86                 params=self.clean_params(
87                     {
88                         "project_id": project_id,
89                         "project_name": project_name,
90                         "page_size": page_size,
91                         "page_token": page_token,
92                     },
93                 ),
94             )
95 
96 
97 @click.group("project")
98 def project_group() -> None:
99     """Interact with Meltano Cloud projects."""
100 
101 
102 def _safe_get_internal_project_id(config: MeltanoCloudConfig) -> str | None:
103     """Get the internal project ID, or `None` if it could not be obtained."""
104     try:
105         return config.internal_project_id
106     except Exception:
107         logger.debug(
108             "Could not get internal project ID from config; using `None` instead.",
109         )
110         return None
111 
112 
113 async def _get_projects(
114     config: MeltanoCloudConfig,
115     *,
116     project_id: str | None = None,
117     project_name: str | None = None,
118     limit: int = DEFAULT_GET_PROJECTS_LIMIT,
119 ) -> LimitedResult[CloudProject]:
120     async with ProjectsCloudClient(config=config) as client:
121         results = await get_paginated(
122             lambda page_size, page_token: client.get_projects(
123                 project_id=project_id,
124                 project_name=project_name,
125                 page_size=page_size,
126                 page_token=page_token,
127             ),
128             limit,
129             MAX_PAGE_SIZE,
130         )
131 
132     results.items = [
133         {
134             **x,
135             "default": x["project_id"] == _safe_get_internal_project_id(config),
136         }
137         for x in results.items
138     ]
139     return results
140 
141 
142 def _format_project(project: dict[str, t.Any]) -> tuple[str, ...]:
143     return (
144         "X" if project["default"] else "",
145         project["project_name"],
146         project["git_repository"],
147     )
148 
149 
150 private_project_attributes = {"tenant_resource_key", "project_id"}
151 
152 
153 def _remove_private_project_attributes(project: CloudProject) -> dict[str, t.Any]:
154     return {k: v for k, v in project.items() if k not in private_project_attributes}
155 
156 
157 @project_group.command("list")
158 @click.option(
159     "--limit",
160     required=False,
161     type=int,
162     default=DEFAULT_GET_PROJECTS_LIMIT,
163     help="The maximum number of projects to display.",
164 )
165 @click.option(
166     "--format",
167     "output_format",
168     required=False,
169     default="terminal",
170     type=click.Choice(("terminal", "markdown", "json")),
171     help="The output format to use.",
172 )
173 @pass_context
174 @run_async
175 async def list_projects(
176     context: MeltanoCloudCLIContext,
177     output_format: str,
178     limit: int,
179 ) -> None:
180     """List Meltano Cloud projects."""
181     results = await _get_projects(config=context.config, limit=limit)
182     stripped_results = LimitedResult(
183         items=[_remove_private_project_attributes(x) for x in results.items],
184         truncated=results.truncated,
185     )
186     print_formatted_list(
187         stripped_results,
188         output_format,
189         _format_project,
190         ("Default", "Name", "Git Repository"),
191         ("center", "left", "left"),
192     )
193 
194 
195 def _print_projects(projects: list[CloudProject]) -> None:
196     for project in projects:
197         click.echo(
198             f"{project['project_id']}: {project['project_name']} "
199             f"({project['git_repository']!r})",
200         )
201 
202 
203 def _check_for_duplicate_project_names(projects: list[CloudProject]) -> None:
204     project_names = [x["project_name"] for x in projects]
205     if len(set(project_names)) != len(project_names):
206         click.secho(
207             "Error: Multiple Meltano Cloud projects have the same name. If you are "
208             "trying to use a project with an unambiguous name, please select it with "
209             "the `--name` option. Otherwise, please specify the project using the "
210             "`--id` option with its internal ID, shown below. Note that these IDs may "
211             "change at any time. To avoid this issue, please use unique project names.",
212             fg="red",
213         )
214         _print_projects(projects)
215         sys.exit(1)
216 
217 
218 def _check_for_project_name_conflict(
219     projects: list[CloudProject],
220     project_name: str,
221 ) -> None:
222     if [x["project_name"] for x in projects].count(project_name) > 1:
223         click.secho(
224             "Error: Multiple Meltano Cloud projects have the specified name. "
225             "Please specify the project using the `--id` option with its "
226             "internal ID, shown below. Note that these IDs may change at any "
227             "time. To avoid this issue, please use unique project names.",
228             fg="red",
229         )
230         _print_projects(projects)
231         sys.exit(1)
232 
233 
234 class ProjectChoicesQuestionaryOption(click.Option):
235     """Click option that provides an interactive prompt for Cloud Project names."""
236 
237     def prompt_for_value(self, ctx: click.Context) -> t.Any:
238         """Prompt the user to interactively select a Meltano Cloud project by name.
239 
240         Args:
241             ctx: The Click context.
242 
243         Returns:
244             The name of the selected project, or `None` if the project was
245             selected using the `--id` option.
246         """
247         if "project_id" in ctx.params:
248             # The project has been specified by ID - don't prompt for a name
249             return None
250 
251         context: MeltanoCloudCLIContext = ctx.obj
252         context.projects = asyncio.run(_get_projects(context.config)).items
253         _check_for_duplicate_project_names(context.projects)
254         default_project_name = next(
255             (
256                 x
257                 for x in context.projects
258                 if x["project_id"]
259                 == context.config.internal_organization_default["default_project_id"]
260             ),
261             {"project_name": None},
262         )["project_name"]
263         if not context.projects:
264             raise click.ClickException(
265                 "No Meltano Cloud projects available to use. Please create a "
266                 "project before running 'meltano cloud project use'.",
267             )
268         return questionary.select(
269             message="",
270             qmark="Use Meltano Cloud project",
271             choices=[x["project_name"] for x in context.projects],
272             default=default_project_name,
273         ).unsafe_ask()  # Use Click's Ctrl-C handling instead of Questionary's
274 
275 
276 @project_group.command("use")
277 @click.option(
278     "--name",
279     "project_name",
280     cls=ProjectChoicesQuestionaryOption,
281     help=(
282         "The name of a Meltano Cloud project - "
283         "see `meltano cloud project list` for the available options."
284     ),
285     prompt=True,
286 )
287 @click.option(
288     "--id",
289     "project_id",
290     help=(
291         "The internal ID of a Meltano Cloud project - this ID is unstable and "
292         "should only be used if necessary to disambiguate when multiple "
293         "projects share a name."
294     ),
295     default=None,
296     type=ULIDType(),
297 )
298 @pass_context
299 @run_async
300 async def use_project(
301     context: MeltanoCloudCLIContext,
302     project_name: str | None,
303     project_id: str | None,
304 ) -> None:
305     """Set a project as the default to use for Meltano Cloud CLI commands."""
306     if project_id is not None and project_name is not None:
307         raise click.UsageError("The '--name' and '--id' options are mutually exclusive")
308     if project_id is not None:
309         context.config.internal_project_id = project_id
310         click.secho(
311             (
312                 f"Set the project with ID {project_id!r} as the default "
313                 "Meltano Cloud project for future commands"
314             ),
315             fg="green",
316         )
317         return
318 
319     if context.projects is None:  # Interactive config was not used
320         context.projects = (await _get_projects(context.config)).items
321         _check_for_project_name_conflict(context.projects, t.cast(str, project_name))
322         if project_name not in {x["project_name"] for x in context.projects}:
323             raise click.ClickException(
324                 f"Unable to use project named {project_name!r} - no available "
325                 "project matches name.",
326             )
327     context.config.internal_project_id = next(
328         x for x in context.projects if x["project_name"] == project_name
329     )["project_id"]
330     click.secho(
331         (
332             f"Set {project_name!r} as the default Meltano Cloud project for "
333             "future commands"
334         ),
335         fg="green",
336     )
337
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/meltano/cloud/cli/project.py b/src/meltano/cloud/cli/project.py
--- a/src/meltano/cloud/cli/project.py
+++ b/src/meltano/cloud/cli/project.py
@@ -6,12 +6,16 @@
 import logging
 import sys
 import typing as t
+from http import HTTPStatus
 
 import click
 import questionary
+import requests
+from slugify import slugify
 from ulid import ULID
+from yaspin import yaspin  # type: ignore
 
-from meltano.cloud.api.client import MeltanoCloudClient
+from meltano.cloud.api.client import MeltanoCloudClient, MeltanoCloudError
 from meltano.cloud.cli.base import (
     LimitedResult,
     get_paginated,
@@ -93,6 +97,26 @@
                 ),
             )
 
+    async def create_project(
+        self,
+        project_name: str,
+        git_repository: str,
+        project_root_path: str | None = None,
+    ):
+        """Use POST to create new Meltano Cloud project."""
+        async with self.authenticated():
+            payload = {"project_name": project_name, "git_repository": git_repository}
+            if project_root_path:
+                payload["project_root_path"] = project_root_path
+            prepared_request = await self._json_request(
+                "POST",
+                f"/projects/v1/{self.config.tenant_resource_key}",
+                json=payload,
+            )
+            response = requests.request(**t.cast(t.Dict[str, t.Any], prepared_request))
+            response.raise_for_status()
+            return response
+
 
 @click.group("project")
 def project_group() -> None:
@@ -334,3 +358,41 @@
         ),
         fg="green",
     )
+
+
+@project_group.command("create")
+@click.option("--name", type=str, prompt=True)
+@click.option("--repo-url", type=str, prompt=True)
+@click.option("--root-path", type=str, required=False)
+@pass_context
+@run_async
+async def create_project(
+    context: MeltanoCloudCLIContext,
+    name: str,
+    repo_url: str,
+    root_path: str | None = None,
+):
+    """Create a project to your Meltano Cloud."""
+    async with ProjectsCloudClient(config=context.config) as client:
+        try:
+            with yaspin(
+                text="Creating project - this may take several minutes...",
+            ):
+                response = await client.create_project(
+                    project_name=name,
+                    git_repository=repo_url,
+                    project_root_path=root_path,
+                )
+        except MeltanoCloudError as e:
+            if e.response.status == HTTPStatus.CONFLICT:
+                click.secho(
+                    (
+                        f"A project named {name!r} (normalized to "
+                        f"{slugify(name)!r}) already exists."
+                    ),
+                    fg="yellow",
+                )
+            return None
+        click.echo(f"Project {name!r} created successfully.")
+        if response.status_code == HTTPStatus.NO_CONTENT:
+            return None

{"golden_diff": "diff --git a/src/meltano/cloud/cli/project.py b/src/meltano/cloud/cli/project.py\n--- a/src/meltano/cloud/cli/project.py\n+++ b/src/meltano/cloud/cli/project.py\n@@ -6,12 +6,16 @@\n import logging\n import sys\n import typing as t\n+from http import HTTPStatus\n \n import click\n import questionary\n+import requests\n+from slugify import slugify\n from ulid import ULID\n+from yaspin import yaspin  # type: ignore\n \n-from meltano.cloud.api.client import MeltanoCloudClient\n+from meltano.cloud.api.client import MeltanoCloudClient, MeltanoCloudError\n from meltano.cloud.cli.base import (\n     LimitedResult,\n     get_paginated,\n@@ -93,6 +97,26 @@\n                 ),\n             )\n \n+    async def create_project(\n+        self,\n+        project_name: str,\n+        git_repository: str,\n+        project_root_path: str | None = None,\n+    ):\n+        \"\"\"Use POST to create new Meltano Cloud project.\"\"\"\n+        async with self.authenticated():\n+            payload = {\"project_name\": project_name, \"git_repository\": git_repository}\n+            if project_root_path:\n+                payload[\"project_root_path\"] = project_root_path\n+            prepared_request = await self._json_request(\n+                \"POST\",\n+                f\"/projects/v1/{self.config.tenant_resource_key}\",\n+                json=payload,\n+            )\n+            response = requests.request(**t.cast(t.Dict[str, t.Any], prepared_request))\n+            response.raise_for_status()\n+            return response\n+\n \n @click.group(\"project\")\n def project_group() -> None:\n@@ -334,3 +358,41 @@\n         ),\n         fg=\"green\",\n     )\n+\n+\n+@project_group.command(\"create\")\n+@click.option(\"--name\", type=str, prompt=True)\n+@click.option(\"--repo-url\", type=str, prompt=True)\n+@click.option(\"--root-path\", type=str, required=False)\n+@pass_context\n+@run_async\n+async def create_project(\n+    context: MeltanoCloudCLIContext,\n+    name: str,\n+    repo_url: str,\n+    root_path: str | None = None,\n+):\n+    \"\"\"Create a project to your Meltano Cloud.\"\"\"\n+    async with ProjectsCloudClient(config=context.config) as client:\n+        try:\n+            with yaspin(\n+                text=\"Creating project - this may take several minutes...\",\n+            ):\n+                response = await client.create_project(\n+                    project_name=name,\n+                    git_repository=repo_url,\n+                    project_root_path=root_path,\n+                )\n+        except MeltanoCloudError as e:\n+            if e.response.status == HTTPStatus.CONFLICT:\n+                click.secho(\n+                    (\n+                        f\"A project named {name!r} (normalized to \"\n+                        f\"{slugify(name)!r}) already exists.\"\n+                    ),\n+                    fg=\"yellow\",\n+                )\n+            return None\n+        click.echo(f\"Project {name!r} created successfully.\")\n+        if response.status_code == HTTPStatus.NO_CONTENT:\n+            return None\n", "issue": "Cloud CLI to register a new project in Meltano Cloud\ncc @tayloramurphy\r\n\r\nBlocked by:\r\n- https://github.com/meltano/infra/issues/514\r\n\r\nRelated to:\r\n\r\n- https://github.com/meltano/infra/issues/513 (internal)\r\n- https://github.com/meltano/meltano/issues/7411\r\n\r\n\n", "before_files": [{"content": "\"\"\"Meltano Cloud `project` command.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nimport sys\nimport typing as t\n\nimport click\nimport questionary\nfrom ulid import ULID\n\nfrom meltano.cloud.api.client import MeltanoCloudClient\nfrom meltano.cloud.cli.base import (\n    LimitedResult,\n    get_paginated,\n    pass_context,\n    print_formatted_list,\n)\nfrom meltano.core.utils import run_async\n\nif t.TYPE_CHECKING:\n    from meltano.cloud.api.config import MeltanoCloudConfig\n    from meltano.cloud.api.types import CloudProject\n    from meltano.cloud.cli.base import MeltanoCloudCLIContext\n\nDEFAULT_GET_PROJECTS_LIMIT = 125\nMAX_PAGE_SIZE = 250\n\nlogger = logging.getLogger()\n\n\nclass ULIDType(click.ParamType):\n    \"\"\"A ULID input type.\n\n    Examples:\n        01BX5ZZKBKACTAV9WEVGEMMVRY\n        01BX5ZZKBKACTAV9WEVGEMMVS1\n    \"\"\"\n\n    name = \"ulid\"\n\n    def convert(\n        self,\n        value: str | ULID,\n        param: click.Parameter | None,  # noqa: ARG002\n        ctx: click.Context | None,  # noqa: ARG002\n    ) -> str:\n        \"\"\"Try converting value to a ULID object.\"\"\"\n        if isinstance(value, ULID):\n            return str(value)\n\n        if isinstance(value, str):\n            try:\n                ULID.from_str(value)\n            except ValueError:\n                self.fail(f\"Invalid ULID value: {value}\")\n\n        return value\n\n\nclass ProjectsCloudClient(MeltanoCloudClient):\n    \"\"\"A Meltano Cloud client with extensions for projects.\"\"\"\n\n    async def get_projects(\n        self,\n        *,\n        project_id: str | None = None,\n        project_name: str | None = None,\n        page_size: int | None = None,\n        page_token: str | None = None,\n    ):\n        \"\"\"Use GET to get Meltano Cloud project projects.\n\n        Args:\n            project_id: The Meltano Cloud ID for the project.\n            project_name: The name of the project.\n            page_size: The number of items to request per page.\n            page_token: The page token.\n        \"\"\"\n        async with self.authenticated():\n            return await self._json_request(\n                \"GET\",\n                f\"/projects/v1/{self.config.tenant_resource_key}\",\n                params=self.clean_params(\n                    {\n                        \"project_id\": project_id,\n                        \"project_name\": project_name,\n                        \"page_size\": page_size,\n                        \"page_token\": page_token,\n                    },\n                ),\n            )\n\n\n@click.group(\"project\")\ndef project_group() -> None:\n    \"\"\"Interact with Meltano Cloud projects.\"\"\"\n\n\ndef _safe_get_internal_project_id(config: MeltanoCloudConfig) -> str | None:\n    \"\"\"Get the internal project ID, or `None` if it could not be obtained.\"\"\"\n    try:\n        return config.internal_project_id\n    except Exception:\n        logger.debug(\n            \"Could not get internal project ID from config; using `None` instead.\",\n        )\n        return None\n\n\nasync def _get_projects(\n    config: MeltanoCloudConfig,\n    *,\n    project_id: str | None = None,\n    project_name: str | None = None,\n    limit: int = DEFAULT_GET_PROJECTS_LIMIT,\n) -> LimitedResult[CloudProject]:\n    async with ProjectsCloudClient(config=config) as client:\n        results = await get_paginated(\n            lambda page_size, page_token: client.get_projects(\n                project_id=project_id,\n                project_name=project_name,\n                page_size=page_size,\n                page_token=page_token,\n            ),\n            limit,\n            MAX_PAGE_SIZE,\n        )\n\n    results.items = [\n        {\n            **x,\n            \"default\": x[\"project_id\"] == _safe_get_internal_project_id(config),\n        }\n        for x in results.items\n    ]\n    return results\n\n\ndef _format_project(project: dict[str, t.Any]) -> tuple[str, ...]:\n    return (\n        \"X\" if project[\"default\"] else \"\",\n        project[\"project_name\"],\n        project[\"git_repository\"],\n    )\n\n\nprivate_project_attributes = {\"tenant_resource_key\", \"project_id\"}\n\n\ndef _remove_private_project_attributes(project: CloudProject) -> dict[str, t.Any]:\n    return {k: v for k, v in project.items() if k not in private_project_attributes}\n\n\n@project_group.command(\"list\")\n@click.option(\n    \"--limit\",\n    required=False,\n    type=int,\n    default=DEFAULT_GET_PROJECTS_LIMIT,\n    help=\"The maximum number of projects to display.\",\n)\n@click.option(\n    \"--format\",\n    \"output_format\",\n    required=False,\n    default=\"terminal\",\n    type=click.Choice((\"terminal\", \"markdown\", \"json\")),\n    help=\"The output format to use.\",\n)\n@pass_context\n@run_async\nasync def list_projects(\n    context: MeltanoCloudCLIContext,\n    output_format: str,\n    limit: int,\n) -> None:\n    \"\"\"List Meltano Cloud projects.\"\"\"\n    results = await _get_projects(config=context.config, limit=limit)\n    stripped_results = LimitedResult(\n        items=[_remove_private_project_attributes(x) for x in results.items],\n        truncated=results.truncated,\n    )\n    print_formatted_list(\n        stripped_results,\n        output_format,\n        _format_project,\n        (\"Default\", \"Name\", \"Git Repository\"),\n        (\"center\", \"left\", \"left\"),\n    )\n\n\ndef _print_projects(projects: list[CloudProject]) -> None:\n    for project in projects:\n        click.echo(\n            f\"{project['project_id']}: {project['project_name']} \"\n            f\"({project['git_repository']!r})\",\n        )\n\n\ndef _check_for_duplicate_project_names(projects: list[CloudProject]) -> None:\n    project_names = [x[\"project_name\"] for x in projects]\n    if len(set(project_names)) != len(project_names):\n        click.secho(\n            \"Error: Multiple Meltano Cloud projects have the same name. If you are \"\n            \"trying to use a project with an unambiguous name, please select it with \"\n            \"the `--name` option. Otherwise, please specify the project using the \"\n            \"`--id` option with its internal ID, shown below. Note that these IDs may \"\n            \"change at any time. To avoid this issue, please use unique project names.\",\n            fg=\"red\",\n        )\n        _print_projects(projects)\n        sys.exit(1)\n\n\ndef _check_for_project_name_conflict(\n    projects: list[CloudProject],\n    project_name: str,\n) -> None:\n    if [x[\"project_name\"] for x in projects].count(project_name) > 1:\n        click.secho(\n            \"Error: Multiple Meltano Cloud projects have the specified name. \"\n            \"Please specify the project using the `--id` option with its \"\n            \"internal ID, shown below. Note that these IDs may change at any \"\n            \"time. To avoid this issue, please use unique project names.\",\n            fg=\"red\",\n        )\n        _print_projects(projects)\n        sys.exit(1)\n\n\nclass ProjectChoicesQuestionaryOption(click.Option):\n    \"\"\"Click option that provides an interactive prompt for Cloud Project names.\"\"\"\n\n    def prompt_for_value(self, ctx: click.Context) -> t.Any:\n        \"\"\"Prompt the user to interactively select a Meltano Cloud project by name.\n\n        Args:\n            ctx: The Click context.\n\n        Returns:\n            The name of the selected project, or `None` if the project was\n            selected using the `--id` option.\n        \"\"\"\n        if \"project_id\" in ctx.params:\n            # The project has been specified by ID - don't prompt for a name\n            return None\n\n        context: MeltanoCloudCLIContext = ctx.obj\n        context.projects = asyncio.run(_get_projects(context.config)).items\n        _check_for_duplicate_project_names(context.projects)\n        default_project_name = next(\n            (\n                x\n                for x in context.projects\n                if x[\"project_id\"]\n                == context.config.internal_organization_default[\"default_project_id\"]\n            ),\n            {\"project_name\": None},\n        )[\"project_name\"]\n        if not context.projects:\n            raise click.ClickException(\n                \"No Meltano Cloud projects available to use. Please create a \"\n                \"project before running 'meltano cloud project use'.\",\n            )\n        return questionary.select(\n            message=\"\",\n            qmark=\"Use Meltano Cloud project\",\n            choices=[x[\"project_name\"] for x in context.projects],\n            default=default_project_name,\n        ).unsafe_ask()  # Use Click's Ctrl-C handling instead of Questionary's\n\n\n@project_group.command(\"use\")\n@click.option(\n    \"--name\",\n    \"project_name\",\n    cls=ProjectChoicesQuestionaryOption,\n    help=(\n        \"The name of a Meltano Cloud project - \"\n        \"see `meltano cloud project list` for the available options.\"\n    ),\n    prompt=True,\n)\n@click.option(\n    \"--id\",\n    \"project_id\",\n    help=(\n        \"The internal ID of a Meltano Cloud project - this ID is unstable and \"\n        \"should only be used if necessary to disambiguate when multiple \"\n        \"projects share a name.\"\n    ),\n    default=None,\n    type=ULIDType(),\n)\n@pass_context\n@run_async\nasync def use_project(\n    context: MeltanoCloudCLIContext,\n    project_name: str | None,\n    project_id: str | None,\n) -> None:\n    \"\"\"Set a project as the default to use for Meltano Cloud CLI commands.\"\"\"\n    if project_id is not None and project_name is not None:\n        raise click.UsageError(\"The '--name' and '--id' options are mutually exclusive\")\n    if project_id is not None:\n        context.config.internal_project_id = project_id\n        click.secho(\n            (\n                f\"Set the project with ID {project_id!r} as the default \"\n                \"Meltano Cloud project for future commands\"\n            ),\n            fg=\"green\",\n        )\n        return\n\n    if context.projects is None:  # Interactive config was not used\n        context.projects = (await _get_projects(context.config)).items\n        _check_for_project_name_conflict(context.projects, t.cast(str, project_name))\n        if project_name not in {x[\"project_name\"] for x in context.projects}:\n            raise click.ClickException(\n                f\"Unable to use project named {project_name!r} - no available \"\n                \"project matches name.\",\n            )\n    context.config.internal_project_id = next(\n        x for x in context.projects if x[\"project_name\"] == project_name\n    )[\"project_id\"]\n    click.secho(\n        (\n            f\"Set {project_name!r} as the default Meltano Cloud project for \"\n            \"future commands\"\n        ),\n        fg=\"green\",\n    )\n", "path": "src/meltano/cloud/cli/project.py"}], "after_files": [{"content": "\"\"\"Meltano Cloud `project` command.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nimport sys\nimport typing as t\nfrom http import HTTPStatus\n\nimport click\nimport questionary\nimport requests\nfrom slugify import slugify\nfrom ulid import ULID\nfrom yaspin import yaspin  # type: ignore\n\nfrom meltano.cloud.api.client import MeltanoCloudClient, MeltanoCloudError\nfrom meltano.cloud.cli.base import (\n    LimitedResult,\n    get_paginated,\n    pass_context,\n    print_formatted_list,\n)\nfrom meltano.core.utils import run_async\n\nif t.TYPE_CHECKING:\n    from meltano.cloud.api.config import MeltanoCloudConfig\n    from meltano.cloud.api.types import CloudProject\n    from meltano.cloud.cli.base import MeltanoCloudCLIContext\n\nDEFAULT_GET_PROJECTS_LIMIT = 125\nMAX_PAGE_SIZE = 250\n\nlogger = logging.getLogger()\n\n\nclass ULIDType(click.ParamType):\n    \"\"\"A ULID input type.\n\n    Examples:\n        01BX5ZZKBKACTAV9WEVGEMMVRY\n        01BX5ZZKBKACTAV9WEVGEMMVS1\n    \"\"\"\n\n    name = \"ulid\"\n\n    def convert(\n        self,\n        value: str | ULID,\n        param: click.Parameter | None,  # noqa: ARG002\n        ctx: click.Context | None,  # noqa: ARG002\n    ) -> str:\n        \"\"\"Try converting value to a ULID object.\"\"\"\n        if isinstance(value, ULID):\n            return str(value)\n\n        if isinstance(value, str):\n            try:\n                ULID.from_str(value)\n            except ValueError:\n                self.fail(f\"Invalid ULID value: {value}\")\n\n        return value\n\n\nclass ProjectsCloudClient(MeltanoCloudClient):\n    \"\"\"A Meltano Cloud client with extensions for projects.\"\"\"\n\n    async def get_projects(\n        self,\n        *,\n        project_id: str | None = None,\n        project_name: str | None = None,\n        page_size: int | None = None,\n        page_token: str | None = None,\n    ):\n        \"\"\"Use GET to get Meltano Cloud project projects.\n\n        Args:\n            project_id: The Meltano Cloud ID for the project.\n            project_name: The name of the project.\n            page_size: The number of items to request per page.\n            page_token: The page token.\n        \"\"\"\n        async with self.authenticated():\n            return await self._json_request(\n                \"GET\",\n                f\"/projects/v1/{self.config.tenant_resource_key}\",\n                params=self.clean_params(\n                    {\n                        \"project_id\": project_id,\n                        \"project_name\": project_name,\n                        \"page_size\": page_size,\n                        \"page_token\": page_token,\n                    },\n                ),\n            )\n\n    async def create_project(\n        self,\n        project_name: str,\n        git_repository: str,\n        project_root_path: str | None = None,\n    ):\n        \"\"\"Use POST to create new Meltano Cloud project.\"\"\"\n        async with self.authenticated():\n            payload = {\"project_name\": project_name, \"git_repository\": git_repository}\n            if project_root_path:\n                payload[\"project_root_path\"] = project_root_path\n            prepared_request = await self._json_request(\n                \"POST\",\n                f\"/projects/v1/{self.config.tenant_resource_key}\",\n                json=payload,\n            )\n            response = requests.request(**t.cast(t.Dict[str, t.Any], prepared_request))\n            response.raise_for_status()\n            return response\n\n\n@click.group(\"project\")\ndef project_group() -> None:\n    \"\"\"Interact with Meltano Cloud projects.\"\"\"\n\n\ndef _safe_get_internal_project_id(config: MeltanoCloudConfig) -> str | None:\n    \"\"\"Get the internal project ID, or `None` if it could not be obtained.\"\"\"\n    try:\n        return config.internal_project_id\n    except Exception:\n        logger.debug(\n            \"Could not get internal project ID from config; using `None` instead.\",\n        )\n        return None\n\n\nasync def _get_projects(\n    config: MeltanoCloudConfig,\n    *,\n    project_id: str | None = None,\n    project_name: str | None = None,\n    limit: int = DEFAULT_GET_PROJECTS_LIMIT,\n) -> LimitedResult[CloudProject]:\n    async with ProjectsCloudClient(config=config) as client:\n        results = await get_paginated(\n            lambda page_size, page_token: client.get_projects(\n                project_id=project_id,\n                project_name=project_name,\n                page_size=page_size,\n                page_token=page_token,\n            ),\n            limit,\n            MAX_PAGE_SIZE,\n        )\n\n    results.items = [\n        {\n            **x,\n            \"default\": x[\"project_id\"] == _safe_get_internal_project_id(config),\n        }\n        for x in results.items\n    ]\n    return results\n\n\ndef _format_project(project: dict[str, t.Any]) -> tuple[str, ...]:\n    return (\n        \"X\" if project[\"default\"] else \"\",\n        project[\"project_name\"],\n        project[\"git_repository\"],\n    )\n\n\nprivate_project_attributes = {\"tenant_resource_key\", \"project_id\"}\n\n\ndef _remove_private_project_attributes(project: CloudProject) -> dict[str, t.Any]:\n    return {k: v for k, v in project.items() if k not in private_project_attributes}\n\n\n@project_group.command(\"list\")\n@click.option(\n    \"--limit\",\n    required=False,\n    type=int,\n    default=DEFAULT_GET_PROJECTS_LIMIT,\n    help=\"The maximum number of projects to display.\",\n)\n@click.option(\n    \"--format\",\n    \"output_format\",\n    required=False,\n    default=\"terminal\",\n    type=click.Choice((\"terminal\", \"markdown\", \"json\")),\n    help=\"The output format to use.\",\n)\n@pass_context\n@run_async\nasync def list_projects(\n    context: MeltanoCloudCLIContext,\n    output_format: str,\n    limit: int,\n) -> None:\n    \"\"\"List Meltano Cloud projects.\"\"\"\n    results = await _get_projects(config=context.config, limit=limit)\n    stripped_results = LimitedResult(\n        items=[_remove_private_project_attributes(x) for x in results.items],\n        truncated=results.truncated,\n    )\n    print_formatted_list(\n        stripped_results,\n        output_format,\n        _format_project,\n        (\"Default\", \"Name\", \"Git Repository\"),\n        (\"center\", \"left\", \"left\"),\n    )\n\n\ndef _print_projects(projects: list[CloudProject]) -> None:\n    for project in projects:\n        click.echo(\n            f\"{project['project_id']}: {project['project_name']} \"\n            f\"({project['git_repository']!r})\",\n        )\n\n\ndef _check_for_duplicate_project_names(projects: list[CloudProject]) -> None:\n    project_names = [x[\"project_name\"] for x in projects]\n    if len(set(project_names)) != len(project_names):\n        click.secho(\n            \"Error: Multiple Meltano Cloud projects have the same name. If you are \"\n            \"trying to use a project with an unambiguous name, please select it with \"\n            \"the `--name` option. Otherwise, please specify the project using the \"\n            \"`--id` option with its internal ID, shown below. Note that these IDs may \"\n            \"change at any time. To avoid this issue, please use unique project names.\",\n            fg=\"red\",\n        )\n        _print_projects(projects)\n        sys.exit(1)\n\n\ndef _check_for_project_name_conflict(\n    projects: list[CloudProject],\n    project_name: str,\n) -> None:\n    if [x[\"project_name\"] for x in projects].count(project_name) > 1:\n        click.secho(\n            \"Error: Multiple Meltano Cloud projects have the specified name. \"\n            \"Please specify the project using the `--id` option with its \"\n            \"internal ID, shown below. Note that these IDs may change at any \"\n            \"time. To avoid this issue, please use unique project names.\",\n            fg=\"red\",\n        )\n        _print_projects(projects)\n        sys.exit(1)\n\n\nclass ProjectChoicesQuestionaryOption(click.Option):\n    \"\"\"Click option that provides an interactive prompt for Cloud Project names.\"\"\"\n\n    def prompt_for_value(self, ctx: click.Context) -> t.Any:\n        \"\"\"Prompt the user to interactively select a Meltano Cloud project by name.\n\n        Args:\n            ctx: The Click context.\n\n        Returns:\n            The name of the selected project, or `None` if the project was\n            selected using the `--id` option.\n        \"\"\"\n        if \"project_id\" in ctx.params:\n            # The project has been specified by ID - don't prompt for a name\n            return None\n\n        context: MeltanoCloudCLIContext = ctx.obj\n        context.projects = asyncio.run(_get_projects(context.config)).items\n        _check_for_duplicate_project_names(context.projects)\n        default_project_name = next(\n            (\n                x\n                for x in context.projects\n                if x[\"project_id\"]\n                == context.config.internal_organization_default[\"default_project_id\"]\n            ),\n            {\"project_name\": None},\n        )[\"project_name\"]\n        if not context.projects:\n            raise click.ClickException(\n                \"No Meltano Cloud projects available to use. Please create a \"\n                \"project before running 'meltano cloud project use'.\",\n            )\n        return questionary.select(\n            message=\"\",\n            qmark=\"Use Meltano Cloud project\",\n            choices=[x[\"project_name\"] for x in context.projects],\n            default=default_project_name,\n        ).unsafe_ask()  # Use Click's Ctrl-C handling instead of Questionary's\n\n\n@project_group.command(\"use\")\n@click.option(\n    \"--name\",\n    \"project_name\",\n    cls=ProjectChoicesQuestionaryOption,\n    help=(\n        \"The name of a Meltano Cloud project - \"\n        \"see `meltano cloud project list` for the available options.\"\n    ),\n    prompt=True,\n)\n@click.option(\n    \"--id\",\n    \"project_id\",\n    help=(\n        \"The internal ID of a Meltano Cloud project - this ID is unstable and \"\n        \"should only be used if necessary to disambiguate when multiple \"\n        \"projects share a name.\"\n    ),\n    default=None,\n    type=ULIDType(),\n)\n@pass_context\n@run_async\nasync def use_project(\n    context: MeltanoCloudCLIContext,\n    project_name: str | None,\n    project_id: str | None,\n) -> None:\n    \"\"\"Set a project as the default to use for Meltano Cloud CLI commands.\"\"\"\n    if project_id is not None and project_name is not None:\n        raise click.UsageError(\"The '--name' and '--id' options are mutually exclusive\")\n    if project_id is not None:\n        context.config.internal_project_id = project_id\n        click.secho(\n            (\n                f\"Set the project with ID {project_id!r} as the default \"\n                \"Meltano Cloud project for future commands\"\n            ),\n            fg=\"green\",\n        )\n        return\n\n    if context.projects is None:  # Interactive config was not used\n        context.projects = (await _get_projects(context.config)).items\n        _check_for_project_name_conflict(context.projects, t.cast(str, project_name))\n        if project_name not in {x[\"project_name\"] for x in context.projects}:\n            raise click.ClickException(\n                f\"Unable to use project named {project_name!r} - no available \"\n                \"project matches name.\",\n            )\n    context.config.internal_project_id = next(\n        x for x in context.projects if x[\"project_name\"] == project_name\n    )[\"project_id\"]\n    click.secho(\n        (\n            f\"Set {project_name!r} as the default Meltano Cloud project for \"\n            \"future commands\"\n        ),\n        fg=\"green\",\n    )\n\n\n@project_group.command(\"create\")\n@click.option(\"--name\", type=str, prompt=True)\n@click.option(\"--repo-url\", type=str, prompt=True)\n@click.option(\"--root-path\", type=str, required=False)\n@pass_context\n@run_async\nasync def create_project(\n    context: MeltanoCloudCLIContext,\n    name: str,\n    repo_url: str,\n    root_path: str | None = None,\n):\n    \"\"\"Create a project to your Meltano Cloud.\"\"\"\n    async with ProjectsCloudClient(config=context.config) as client:\n        try:\n            with yaspin(\n                text=\"Creating project - this may take several minutes...\",\n            ):\n                response = await client.create_project(\n                    project_name=name,\n                    git_repository=repo_url,\n                    project_root_path=root_path,\n                )\n        except MeltanoCloudError as e:\n            if e.response.status == HTTPStatus.CONFLICT:\n                click.secho(\n                    (\n                        f\"A project named {name!r} (normalized to \"\n                        f\"{slugify(name)!r}) already exists.\"\n                    ),\n                    fg=\"yellow\",\n                )\n            return None\n        click.echo(f\"Project {name!r} created successfully.\")\n        if response.status_code == HTTPStatus.NO_CONTENT:\n            return None\n", "path": "src/meltano/cloud/cli/project.py"}]}