ChatPyPI 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chatpypi/__init__.py +43 -0
- chatpypi/cli.py +541 -0
- chatpypi/main.py +1779 -0
- chatpypi-0.1.1.dist-info/METADATA +77 -0
- chatpypi-0.1.1.dist-info/RECORD +9 -0
- chatpypi-0.1.1.dist-info/WHEEL +5 -0
- chatpypi-0.1.1.dist-info/entry_points.txt +2 -0
- chatpypi-0.1.1.dist-info/licenses/LICENSE +21 -0
- chatpypi-0.1.1.dist-info/top_level.txt +1 -0
chatpypi/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""ChatPyPI package lifecycle helpers.
|
|
2
|
+
|
|
3
|
+
ChatPyPI provides importable Python APIs for scaffolding, building, checking,
|
|
4
|
+
probing, and uploading Python package distributions. The ``chatpypi`` CLI is a
|
|
5
|
+
thin adapter over these APIs.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .main import (
|
|
9
|
+
CommandResult,
|
|
10
|
+
DoctorCheck,
|
|
11
|
+
ProjectMetadata,
|
|
12
|
+
PyPICommandError,
|
|
13
|
+
RepositoryCheck,
|
|
14
|
+
ScaffoldResult,
|
|
15
|
+
build_package,
|
|
16
|
+
check_distributions,
|
|
17
|
+
check_repository_conflicts,
|
|
18
|
+
normalize_module_name,
|
|
19
|
+
read_project_metadata,
|
|
20
|
+
resolve_dist_dir,
|
|
21
|
+
scaffold_package,
|
|
22
|
+
upload_distributions,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"__version__",
|
|
27
|
+
"CommandResult",
|
|
28
|
+
"DoctorCheck",
|
|
29
|
+
"ProjectMetadata",
|
|
30
|
+
"PyPICommandError",
|
|
31
|
+
"RepositoryCheck",
|
|
32
|
+
"ScaffoldResult",
|
|
33
|
+
"build_package",
|
|
34
|
+
"check_distributions",
|
|
35
|
+
"check_repository_conflicts",
|
|
36
|
+
"normalize_module_name",
|
|
37
|
+
"read_project_metadata",
|
|
38
|
+
"resolve_dist_dir",
|
|
39
|
+
"scaffold_package",
|
|
40
|
+
"upload_distributions",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
__version__ = "0.1.1"
|
chatpypi/cli.py
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from chatstyle import INTERACTIVE_OPTION_HELP
|
|
10
|
+
from chatstyle import (
|
|
11
|
+
abort_if_force_without_tty,
|
|
12
|
+
abort_if_missing_without_tty,
|
|
13
|
+
ask_confirm,
|
|
14
|
+
ask_select,
|
|
15
|
+
ask_text,
|
|
16
|
+
resolve_interactive_mode,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from chatpypi.main import (
|
|
20
|
+
PyPICommandError,
|
|
21
|
+
_ensure_empty_or_missing,
|
|
22
|
+
build_package,
|
|
23
|
+
check_distributions,
|
|
24
|
+
check_repository_conflicts,
|
|
25
|
+
read_project_metadata,
|
|
26
|
+
resolve_dist_dir,
|
|
27
|
+
scaffold_package,
|
|
28
|
+
upload_distributions,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _project_options(func):
|
|
33
|
+
func = click.option(
|
|
34
|
+
"--dist-dir",
|
|
35
|
+
type=click.Path(path_type=Path, file_okay=False),
|
|
36
|
+
default=None,
|
|
37
|
+
help="Distribution directory. Defaults to <project-dir>/dist.",
|
|
38
|
+
)(func)
|
|
39
|
+
func = click.option(
|
|
40
|
+
"--project-dir",
|
|
41
|
+
type=click.Path(path_type=Path, file_okay=False),
|
|
42
|
+
default=Path("."),
|
|
43
|
+
show_default=True,
|
|
44
|
+
help="Project directory containing pyproject.toml.",
|
|
45
|
+
)(func)
|
|
46
|
+
return func
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _echo_result_output(result) -> None:
|
|
50
|
+
stdout = result.stdout.strip()
|
|
51
|
+
stderr = result.stderr.strip()
|
|
52
|
+
if stdout:
|
|
53
|
+
click.echo(stdout)
|
|
54
|
+
if stderr:
|
|
55
|
+
click.echo(stderr, err=True)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _print_files(files: list[Path], title: str) -> None:
|
|
59
|
+
click.echo(title)
|
|
60
|
+
for path in files:
|
|
61
|
+
click.echo(f"- {path}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _raise_click_error(exc: Exception) -> None:
|
|
65
|
+
raise click.ClickException(str(exc)) from exc
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _normalize_optional_text(value: str | None) -> str | None:
|
|
69
|
+
if value is None:
|
|
70
|
+
return None
|
|
71
|
+
value = value.strip()
|
|
72
|
+
return value or None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _resolve_init_inputs(
|
|
76
|
+
*,
|
|
77
|
+
name: str | None,
|
|
78
|
+
description: str | None,
|
|
79
|
+
initial_version: str,
|
|
80
|
+
requires_python: str,
|
|
81
|
+
license_name: str,
|
|
82
|
+
author: str | None,
|
|
83
|
+
email: str | None,
|
|
84
|
+
project_dir: Path | None,
|
|
85
|
+
template: str = "default",
|
|
86
|
+
) -> tuple[str, str | None, str, str, str, str | None, str | None, Path]:
|
|
87
|
+
package_name = _normalize_optional_text(name)
|
|
88
|
+
if not package_name and project_dir is not None and project_dir.name:
|
|
89
|
+
package_name = project_dir.name
|
|
90
|
+
if not package_name:
|
|
91
|
+
raise click.ClickException(
|
|
92
|
+
"Package name is required. Pass NAME or --project-dir."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
target_dir = (project_dir or Path(package_name)).resolve()
|
|
96
|
+
return (
|
|
97
|
+
package_name,
|
|
98
|
+
_normalize_optional_text(description) or f"{package_name} package",
|
|
99
|
+
initial_version or "0.1.0",
|
|
100
|
+
requires_python or _default_requires_python(template),
|
|
101
|
+
license_name or "MIT",
|
|
102
|
+
_normalize_optional_text(author),
|
|
103
|
+
_normalize_optional_text(email),
|
|
104
|
+
target_dir,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _default_requires_python(template: str) -> str:
|
|
109
|
+
if template == "chatarch":
|
|
110
|
+
return ">=3.10"
|
|
111
|
+
return ">=3.9"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _option_was_default(name: str) -> bool:
|
|
115
|
+
ctx = click.get_current_context(silent=True)
|
|
116
|
+
if not ctx:
|
|
117
|
+
return False
|
|
118
|
+
try:
|
|
119
|
+
return ctx.get_parameter_source(name) == click.core.ParameterSource.DEFAULT
|
|
120
|
+
except Exception:
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _is_name_missing(name: str | None, project_dir: Path | None) -> bool:
|
|
125
|
+
package_name = _normalize_optional_text(name)
|
|
126
|
+
return not package_name and not (project_dir is not None and project_dir.name)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _read_git_config(key: str) -> str | None:
|
|
130
|
+
try:
|
|
131
|
+
result = subprocess.run(
|
|
132
|
+
["git", "config", "--get", key],
|
|
133
|
+
check=False,
|
|
134
|
+
capture_output=True,
|
|
135
|
+
text=True,
|
|
136
|
+
)
|
|
137
|
+
except Exception:
|
|
138
|
+
return None
|
|
139
|
+
if result.returncode != 0:
|
|
140
|
+
return None
|
|
141
|
+
value = result.stdout.strip()
|
|
142
|
+
return value or None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _resolve_project_dir(project_dir: Path) -> Path:
|
|
146
|
+
return project_dir.resolve()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _resolve_project_and_dist_dirs(
|
|
150
|
+
project_dir: Path, dist_dir: Path | None
|
|
151
|
+
) -> tuple[Path, Path]:
|
|
152
|
+
resolved_project_dir = _resolve_project_dir(project_dir)
|
|
153
|
+
resolved_dist_dir = resolve_dist_dir(
|
|
154
|
+
resolved_project_dir,
|
|
155
|
+
dist_dir.resolve() if dist_dir else None,
|
|
156
|
+
)
|
|
157
|
+
return resolved_project_dir, resolved_dist_dir
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@click.group(name="chatpypi")
|
|
161
|
+
def cli():
|
|
162
|
+
"""Python package scaffold/build/check/upload helpers."""
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@cli.command(name="init")
|
|
167
|
+
@click.argument("name", required=False)
|
|
168
|
+
@click.option(
|
|
169
|
+
"-t",
|
|
170
|
+
"--template",
|
|
171
|
+
type=click.Choice(["default", "chatarch"]),
|
|
172
|
+
default="default",
|
|
173
|
+
show_default=True,
|
|
174
|
+
help="Project scaffold template.",
|
|
175
|
+
)
|
|
176
|
+
@click.option("--email", default=None, help="Author email to record in pyproject.toml.")
|
|
177
|
+
@click.option("--author", default=None, help="Author name to record in pyproject.toml.")
|
|
178
|
+
@click.option(
|
|
179
|
+
"--license",
|
|
180
|
+
"license_name",
|
|
181
|
+
default="MIT",
|
|
182
|
+
show_default=True,
|
|
183
|
+
help="Project license template: MIT, Apache-2.0, BSD-3-Clause, GPL-3.0-only, or Proprietary.",
|
|
184
|
+
)
|
|
185
|
+
@click.option(
|
|
186
|
+
"--version",
|
|
187
|
+
"initial_version",
|
|
188
|
+
default="0.1.0",
|
|
189
|
+
show_default=True,
|
|
190
|
+
help="Initial package version written to src/<module>/__init__.py.",
|
|
191
|
+
)
|
|
192
|
+
@click.option(
|
|
193
|
+
"--python",
|
|
194
|
+
"requires_python",
|
|
195
|
+
default=">=3.9",
|
|
196
|
+
show_default=True,
|
|
197
|
+
help="Supported Python version specifier.",
|
|
198
|
+
)
|
|
199
|
+
@click.option("--description", default=None, help="Project description.")
|
|
200
|
+
@click.option(
|
|
201
|
+
"--project-dir",
|
|
202
|
+
type=click.Path(path_type=Path, file_okay=False),
|
|
203
|
+
default=None,
|
|
204
|
+
help="Target directory to create. Defaults to ./{name}.",
|
|
205
|
+
)
|
|
206
|
+
@click.option(
|
|
207
|
+
"--with-mkdocs/--without-mkdocs",
|
|
208
|
+
"include_mkdocs",
|
|
209
|
+
default=None,
|
|
210
|
+
help="Create mkdocs files for chatarch template. Defaults to on for chatarch.",
|
|
211
|
+
)
|
|
212
|
+
@click.option(
|
|
213
|
+
"--with-workflows/--without-workflows",
|
|
214
|
+
"include_workflows",
|
|
215
|
+
default=None,
|
|
216
|
+
help="Create GitHub workflow files for chatarch template. Defaults to on for chatarch.",
|
|
217
|
+
)
|
|
218
|
+
@click.option(
|
|
219
|
+
"--with-chatenv-provider/--without-chatenv-provider",
|
|
220
|
+
"include_chatenv_provider",
|
|
221
|
+
default=False,
|
|
222
|
+
show_default=True,
|
|
223
|
+
help="Create ChatEnv provider config.py and chatenv.configs entry point for chatarch template.",
|
|
224
|
+
)
|
|
225
|
+
@click.option(
|
|
226
|
+
"--chatenv-provider-name",
|
|
227
|
+
default=None,
|
|
228
|
+
help="Entry point name for --with-chatenv-provider. Defaults to the module name.",
|
|
229
|
+
)
|
|
230
|
+
@click.option(
|
|
231
|
+
"--interactive/--no-interactive",
|
|
232
|
+
"interactive",
|
|
233
|
+
"-i/-I",
|
|
234
|
+
default=None,
|
|
235
|
+
help=INTERACTIVE_OPTION_HELP,
|
|
236
|
+
)
|
|
237
|
+
def init(
|
|
238
|
+
name: str | None,
|
|
239
|
+
template: str,
|
|
240
|
+
description: str | None,
|
|
241
|
+
initial_version: str,
|
|
242
|
+
requires_python: str,
|
|
243
|
+
license_name: str,
|
|
244
|
+
author: str | None,
|
|
245
|
+
email: str | None,
|
|
246
|
+
project_dir: Path | None,
|
|
247
|
+
include_mkdocs: bool | None,
|
|
248
|
+
include_workflows: bool | None,
|
|
249
|
+
include_chatenv_provider: bool,
|
|
250
|
+
chatenv_provider_name: str | None,
|
|
251
|
+
interactive: bool | None,
|
|
252
|
+
):
|
|
253
|
+
"""Scaffold a minimal src-layout Python package."""
|
|
254
|
+
if _option_was_default("requires_python"):
|
|
255
|
+
requires_python = _default_requires_python(template)
|
|
256
|
+
|
|
257
|
+
missing_required = _is_name_missing(name, project_dir)
|
|
258
|
+
usage = (
|
|
259
|
+
"Usage: chatpypi init [NAME] [-t default|chatarch] [--project-dir PATH] "
|
|
260
|
+
"[--description TEXT] [--version TEXT] [--python TEXT] [--license TEXT] "
|
|
261
|
+
"[--author TEXT] [--email TEXT] [-i|-I]"
|
|
262
|
+
)
|
|
263
|
+
resolution = resolve_interactive_mode(
|
|
264
|
+
interactive=interactive,
|
|
265
|
+
auto_prompt_condition=missing_required,
|
|
266
|
+
)
|
|
267
|
+
interactive = resolution.interactive
|
|
268
|
+
can_prompt = resolution.can_prompt
|
|
269
|
+
force_interactive = resolution.force_interactive
|
|
270
|
+
need_prompt = resolution.need_prompt
|
|
271
|
+
abort_if_force_without_tty(force_interactive, can_prompt, usage)
|
|
272
|
+
abort_if_missing_without_tty(
|
|
273
|
+
missing_required=missing_required,
|
|
274
|
+
interactive=interactive,
|
|
275
|
+
can_prompt=can_prompt,
|
|
276
|
+
message="Package name is required. Pass NAME or --project-dir.",
|
|
277
|
+
usage=usage,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if need_prompt:
|
|
281
|
+
template = ask_select(
|
|
282
|
+
"选择模板",
|
|
283
|
+
choices=[
|
|
284
|
+
"default - minimal Python package",
|
|
285
|
+
"chatarch - ChatArch CLI/docs/tests/automation scaffold",
|
|
286
|
+
],
|
|
287
|
+
).split(" - ", 1)[0]
|
|
288
|
+
name_default = _normalize_optional_text(name) or (
|
|
289
|
+
project_dir.name if project_dir is not None and project_dir.name else ""
|
|
290
|
+
)
|
|
291
|
+
name = ask_text("package_name", default=name_default)
|
|
292
|
+
normalized_name = _normalize_optional_text(name)
|
|
293
|
+
if not normalized_name:
|
|
294
|
+
raise click.ClickException(
|
|
295
|
+
"Package name is required. Pass NAME or --project-dir."
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
project_dir_default = str(project_dir or Path(normalized_name))
|
|
299
|
+
project_dir = Path(
|
|
300
|
+
ask_text("project_dir", default=project_dir_default)
|
|
301
|
+
).expanduser()
|
|
302
|
+
try:
|
|
303
|
+
_ensure_empty_or_missing(project_dir)
|
|
304
|
+
except PyPICommandError as exc:
|
|
305
|
+
_raise_click_error(exc)
|
|
306
|
+
description = ask_text(
|
|
307
|
+
"description",
|
|
308
|
+
default=_normalize_optional_text(description)
|
|
309
|
+
or f"{normalized_name} package",
|
|
310
|
+
)
|
|
311
|
+
initial_version = ask_text("version", default=initial_version or "0.1.0")
|
|
312
|
+
if _option_was_default("requires_python"):
|
|
313
|
+
requires_python = _default_requires_python(template)
|
|
314
|
+
requires_python = ask_text("requires_python", default=requires_python)
|
|
315
|
+
license_name = ask_text("license", default=license_name or "MIT")
|
|
316
|
+
if include_mkdocs is None and template == "chatarch":
|
|
317
|
+
include_mkdocs = ask_confirm(
|
|
318
|
+
"Create mkdocs documentation files?",
|
|
319
|
+
default=True,
|
|
320
|
+
)
|
|
321
|
+
elif include_mkdocs is None:
|
|
322
|
+
include_mkdocs = False
|
|
323
|
+
if include_workflows is None and template == "chatarch":
|
|
324
|
+
include_workflows = ask_confirm(
|
|
325
|
+
"Create GitHub workflow files?",
|
|
326
|
+
default=True,
|
|
327
|
+
)
|
|
328
|
+
elif include_workflows is None:
|
|
329
|
+
include_workflows = False
|
|
330
|
+
author = ask_text(
|
|
331
|
+
"author",
|
|
332
|
+
default=_normalize_optional_text(author)
|
|
333
|
+
or _read_git_config("user.name")
|
|
334
|
+
or "",
|
|
335
|
+
)
|
|
336
|
+
email = ask_text(
|
|
337
|
+
"email",
|
|
338
|
+
default=_normalize_optional_text(email)
|
|
339
|
+
or _read_git_config("user.email")
|
|
340
|
+
or "",
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
(
|
|
344
|
+
package_name,
|
|
345
|
+
description,
|
|
346
|
+
initial_version,
|
|
347
|
+
requires_python,
|
|
348
|
+
license_name,
|
|
349
|
+
author,
|
|
350
|
+
email,
|
|
351
|
+
target_dir,
|
|
352
|
+
) = _resolve_init_inputs(
|
|
353
|
+
name=name,
|
|
354
|
+
description=description,
|
|
355
|
+
initial_version=initial_version,
|
|
356
|
+
requires_python=requires_python,
|
|
357
|
+
license_name=license_name,
|
|
358
|
+
author=author,
|
|
359
|
+
email=email,
|
|
360
|
+
project_dir=project_dir,
|
|
361
|
+
template=template,
|
|
362
|
+
)
|
|
363
|
+
if chatenv_provider_name and not include_chatenv_provider:
|
|
364
|
+
raise click.ClickException(
|
|
365
|
+
"--chatenv-provider-name requires --with-chatenv-provider."
|
|
366
|
+
)
|
|
367
|
+
if include_chatenv_provider and template != "chatarch":
|
|
368
|
+
raise click.ClickException(
|
|
369
|
+
"--with-chatenv-provider is only supported by the chatarch template."
|
|
370
|
+
)
|
|
371
|
+
try:
|
|
372
|
+
result = scaffold_package(
|
|
373
|
+
package_name=package_name,
|
|
374
|
+
project_dir=target_dir,
|
|
375
|
+
initial_version=initial_version,
|
|
376
|
+
description=description,
|
|
377
|
+
requires_python=requires_python,
|
|
378
|
+
license_name=license_name,
|
|
379
|
+
author=author,
|
|
380
|
+
email=email,
|
|
381
|
+
template=template,
|
|
382
|
+
include_mkdocs=include_mkdocs,
|
|
383
|
+
include_workflows=include_workflows,
|
|
384
|
+
include_chatenv_provider=include_chatenv_provider,
|
|
385
|
+
chatenv_provider_name=chatenv_provider_name,
|
|
386
|
+
)
|
|
387
|
+
except PyPICommandError as exc:
|
|
388
|
+
_raise_click_error(exc)
|
|
389
|
+
|
|
390
|
+
click.echo(f"Created Python package scaffold: {result.package_name}")
|
|
391
|
+
click.echo(f"project_dir={result.project_dir}")
|
|
392
|
+
click.echo(f"module_name={result.module_name}")
|
|
393
|
+
_print_files(result.created_files, "Created files:")
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@cli.command(name="build")
|
|
397
|
+
@click.option("--wheel", is_flag=True, help="Build wheel only.")
|
|
398
|
+
@click.option("--sdist", is_flag=True, help="Build source distribution only.")
|
|
399
|
+
@click.option(
|
|
400
|
+
"--clean/--no-clean",
|
|
401
|
+
default=True,
|
|
402
|
+
show_default=True,
|
|
403
|
+
help="Clean old files in dist directory first.",
|
|
404
|
+
)
|
|
405
|
+
@_project_options
|
|
406
|
+
def build(
|
|
407
|
+
project_dir: Path, dist_dir: Path | None, clean: bool, sdist: bool, wheel: bool
|
|
408
|
+
):
|
|
409
|
+
"""Build wheel and/or source distribution with python -m build."""
|
|
410
|
+
project_dir, dist_dir = _resolve_project_and_dist_dirs(project_dir, dist_dir)
|
|
411
|
+
click.echo(f"Building distributions from {project_dir} into {dist_dir}...")
|
|
412
|
+
try:
|
|
413
|
+
result, files = build_package(
|
|
414
|
+
project_dir,
|
|
415
|
+
dist_dir,
|
|
416
|
+
clean=clean,
|
|
417
|
+
sdist=sdist,
|
|
418
|
+
wheel=wheel,
|
|
419
|
+
)
|
|
420
|
+
except PyPICommandError as exc:
|
|
421
|
+
_raise_click_error(exc)
|
|
422
|
+
_echo_result_output(result)
|
|
423
|
+
_print_files(files, "Built distributions:")
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@cli.command(name="check")
|
|
427
|
+
@click.option(
|
|
428
|
+
"--strict", is_flag=True, help="Fail on warnings reported by twine check."
|
|
429
|
+
)
|
|
430
|
+
@_project_options
|
|
431
|
+
def check(project_dir: Path, dist_dir: Path | None, strict: bool):
|
|
432
|
+
"""Validate built distributions with twine check."""
|
|
433
|
+
project_dir, dist_dir = _resolve_project_and_dist_dirs(project_dir, dist_dir)
|
|
434
|
+
try:
|
|
435
|
+
result, files = check_distributions(
|
|
436
|
+
project_dir,
|
|
437
|
+
dist_dir,
|
|
438
|
+
strict=strict,
|
|
439
|
+
)
|
|
440
|
+
except PyPICommandError as exc:
|
|
441
|
+
_raise_click_error(exc)
|
|
442
|
+
_echo_result_output(result)
|
|
443
|
+
_print_files(files, "Checked distributions:")
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
@cli.command(name="upload")
|
|
447
|
+
@click.option(
|
|
448
|
+
"--skip-existing", is_flag=True, help="Pass --skip-existing to twine upload."
|
|
449
|
+
)
|
|
450
|
+
@_project_options
|
|
451
|
+
def upload(project_dir: Path, dist_dir: Path | None, skip_existing: bool):
|
|
452
|
+
"""Upload built distributions with the default twine upload behavior."""
|
|
453
|
+
project_dir, dist_dir = _resolve_project_and_dist_dirs(project_dir, dist_dir)
|
|
454
|
+
click.echo(f"Uploading distributions from {dist_dir} with `twine upload`...")
|
|
455
|
+
try:
|
|
456
|
+
result, files = upload_distributions(
|
|
457
|
+
project_dir,
|
|
458
|
+
dist_dir,
|
|
459
|
+
skip_existing=skip_existing,
|
|
460
|
+
)
|
|
461
|
+
except PyPICommandError as exc:
|
|
462
|
+
_raise_click_error(exc)
|
|
463
|
+
_echo_result_output(result)
|
|
464
|
+
_print_files(files, "Uploaded distributions:")
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
@cli.command(name="probe")
|
|
468
|
+
@click.argument("package_name", required=False)
|
|
469
|
+
@click.option(
|
|
470
|
+
"--repository-url",
|
|
471
|
+
default=None,
|
|
472
|
+
help="Custom repository URL. Overrides --repository.",
|
|
473
|
+
)
|
|
474
|
+
@click.option(
|
|
475
|
+
"--repository",
|
|
476
|
+
type=click.Choice(["testpypi", "pypi"]),
|
|
477
|
+
default="pypi",
|
|
478
|
+
show_default=True,
|
|
479
|
+
help="Target repository for exact project/version releaseability checks.",
|
|
480
|
+
)
|
|
481
|
+
@click.option(
|
|
482
|
+
"--project-dir",
|
|
483
|
+
type=click.Path(path_type=Path, file_okay=False),
|
|
484
|
+
default=Path("."),
|
|
485
|
+
show_default=True,
|
|
486
|
+
help="Project directory containing pyproject.toml for default metadata lookup.",
|
|
487
|
+
)
|
|
488
|
+
def probe(
|
|
489
|
+
project_dir: Path,
|
|
490
|
+
repository: str,
|
|
491
|
+
repository_url: str | None,
|
|
492
|
+
package_name: str | None,
|
|
493
|
+
):
|
|
494
|
+
"""Check whether an exact package name is available on PyPI."""
|
|
495
|
+
project_dir = _resolve_project_dir(project_dir)
|
|
496
|
+
try:
|
|
497
|
+
metadata = read_project_metadata(project_dir)
|
|
498
|
+
except PyPICommandError:
|
|
499
|
+
metadata = None
|
|
500
|
+
|
|
501
|
+
target_name = _normalize_optional_text(package_name) or (
|
|
502
|
+
metadata.name if metadata else None
|
|
503
|
+
)
|
|
504
|
+
if not target_name:
|
|
505
|
+
raise click.ClickException(
|
|
506
|
+
"Package name is required. Pass NAME or provide a readable pyproject.toml."
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
try:
|
|
510
|
+
repository_checks = check_repository_conflicts(
|
|
511
|
+
target_name,
|
|
512
|
+
repository=repository,
|
|
513
|
+
repository_url=repository_url,
|
|
514
|
+
)
|
|
515
|
+
except PyPICommandError as exc:
|
|
516
|
+
_raise_click_error(exc)
|
|
517
|
+
|
|
518
|
+
for item in repository_checks:
|
|
519
|
+
click.echo(f"[{item.status.upper()}] {item.label}: {item.detail}")
|
|
520
|
+
if item.hint:
|
|
521
|
+
click.echo(f" hint: {item.hint}")
|
|
522
|
+
if any(item.status == "fail" for item in repository_checks):
|
|
523
|
+
raise click.ClickException("Repository conflict checks found blocking issues.")
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
KNOWN_COMMANDS = {"init", "build", "check", "upload", "probe", "--help", "-h"}
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def main() -> None:
|
|
530
|
+
"""Console-script entry point with chatpypi shortcut routing."""
|
|
531
|
+
|
|
532
|
+
args = sys.argv[1:]
|
|
533
|
+
if args:
|
|
534
|
+
first = args[0]
|
|
535
|
+
if first not in KNOWN_COMMANDS and not first.startswith("-"):
|
|
536
|
+
args = ["init", *args]
|
|
537
|
+
cli.main(args=args, prog_name="chatpypi")
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
if __name__ == "__main__":
|
|
541
|
+
main()
|