dc-up 0.1.2__tar.gz → 0.3.0__tar.gz

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.
@@ -1,14 +1,13 @@
1
1
  # ============================================================
2
2
  # .gitignore (ALL-REPOS)
3
3
  # ============================================================
4
- # Updated: 2026-06-06
4
+ # Updated: 2026-06-27
5
5
  #
6
6
  # REQ: All professional GitHub project repositories MUST include .gitignore.
7
7
  # WHY: Keep generated artifacts, local state, secrets, and OS-specific files
8
8
  # out of the repository.
9
9
  # ALT: Repository may customize ignores, but MUST preserve universal safety rules.
10
- # CUSTOM: Logs may be committed for verification; keep ignored for
11
- # production use and security.
10
+
12
11
 
13
12
  # === Logs and generated runtime output ===
14
13
 
@@ -31,6 +30,10 @@ PRIVATE_NOTES.md
31
30
  .env
32
31
  .env.*
33
32
 
33
+ # WHY: Commit .env.example as a template showing which variables to set.
34
+ # Copy this file to .env and customize.
35
+ !.env.example
36
+
34
37
 
35
38
  # === Operating-system files ===
36
39
 
@@ -13,6 +13,25 @@ and this project adheres to **[Semantic Versioning](https://semver.org/spec/v2.0
13
13
 
14
14
  ---
15
15
 
16
+ ## [0.3.0] - 2026-06-27
17
+
18
+ ### Changed
19
+
20
+ - ALL-PY-SRC docs/api.md fills in template package name to default the docs/api.md page.
21
+
22
+ ---
23
+
24
+ ## [0.2.0] - 2026-06-27
25
+
26
+ ### Changed
27
+
28
+ - Data-driven course prefixes
29
+ - And a more flexible course repo detection if repo is named like this:
30
+ `prefix-NN-post`
31
+ - better logic for ALL-PY (just tooling) vs ALL-PY-SRC with Ruff and Pyright
32
+
33
+ ---
34
+
16
35
  ## [0.1.2] - 2026-06-10
17
36
 
18
37
  ### Changed
@@ -128,7 +147,9 @@ git push origin :refs/tags/vX.Z.Y
128
147
 
129
148
  ## Links
130
149
 
131
- [Unreleased]: https://github.com/denisecase/dc-up/compare/v0.1.2...HEAD
150
+ [Unreleased]: https://github.com/denisecase/dc-up/compare/v0.3.0...HEAD
151
+ [0.3.0]: https://github.com/denisecase/dc-up/releases/tag/v0.3.0
152
+ [0.2.0]: https://github.com/denisecase/dc-up/releases/tag/v0.2.0
132
153
  [0.1.2]: https://github.com/denisecase/dc-up/releases/tag/v0.1.2
133
154
  [0.1.1]: https://github.com/denisecase/dc-up/releases/tag/v0.1.1
134
155
  [0.1.0]: https://github.com/denisecase/dc-up/releases/tag/v0.1.0
@@ -10,8 +10,8 @@ type: software
10
10
 
11
11
  title: "dc-up"
12
12
  # Set version and date-released to match latest git tag or release.
13
- version: "0.1.2"
14
- date-released: "2026-06-10"
13
+ version: "0.3.0"
14
+ date-released: "2026-06-27"
15
15
 
16
16
  authors:
17
17
  - family-names: Case
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dc-up
3
- Version: 0.1.2
3
+ Version: 0.3.0
4
4
  Summary: Command-line tool for bringing repositories up to a managed baseline using layered canonical templates.
5
5
  Project-URL: Homepage, https://github.com/denisecase/dc-up
6
6
  Project-URL: Repository, https://github.com/denisecase/dc-up
@@ -82,7 +82,11 @@ Description-Content-Type: text/markdown
82
82
  ## Update a Repo based on Templates
83
83
 
84
84
  ```shell
85
+ # see what files the command would update (optional, force latest)
85
86
  uvx dc-up
87
+ uvx dc-up@latest
88
+
89
+ # actually add and overwrite the files listed (CAUTION: DESTRUCTIVE)
86
90
  uvx dc-up --write
87
91
  ```
88
92
 
@@ -23,7 +23,11 @@
23
23
  ## Update a Repo based on Templates
24
24
 
25
25
  ```shell
26
+ # see what files the command would update (optional, force latest)
26
27
  uvx dc-up
28
+ uvx dc-up@latest
29
+
30
+ # actually add and overwrite the files listed (CAUTION: DESTRUCTIVE)
27
31
  uvx dc-up --write
28
32
  ```
29
33
 
@@ -222,11 +222,12 @@ include = [
222
222
  packages = ["src/dc_up"]
223
223
 
224
224
  [tool.hatch.build.targets.wheel.force-include]
225
+ "src/dc_up/data/defaults.toml" = "dc_up/data/defaults.toml"
225
226
  "src/dc_up/data/todo-surfaces.toml" = "dc_up/data/todo-surfaces.toml"
226
227
 
227
228
  [tool.hatch.version]
228
229
  # Used when no git tags are present. Keep in sync with CITATION.cff.
229
- fallback-version = "0.1.2"
230
+ fallback-version = "0.3.0"
230
231
  # WHY: Version is derived from git tags at build time.
231
232
  source = "vcs"
232
233
  # WHY: Allow hatch-vcs to find the Git root from isolated build contexts.
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.1.2'
22
- __version_tuple__ = version_tuple = (0, 1, 2)
21
+ __version__ = version = '0.3.0'
22
+ __version_tuple__ = version_tuple = (0, 3, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -0,0 +1,50 @@
1
+ """Baseline layer inference and managed file declarations."""
2
+
3
+ from pathlib import Path
4
+
5
+ from .baseline_utils import load_package_defaults, looks_like_course_repo
6
+
7
+ __all__ = ["PRESERVE_PATTERNS", "infer_layers"]
8
+
9
+ PRESERVE_PATTERNS: tuple[str, ...] = (
10
+ "README.md",
11
+ "artifacts/**",
12
+ "data/**",
13
+ "docs/**",
14
+ "notebooks/**",
15
+ "sql/**",
16
+ "src/**",
17
+ "tests/**",
18
+ )
19
+
20
+
21
+ def infer_layers(*, repo_root: Path, repo_name: str, files: set[str]) -> list[str]:
22
+ """Infer additive template layers based strictly on file existence."""
23
+ # 1. Identify physical markers
24
+ has_py = "pyproject.toml" in files
25
+ has_src = (repo_root / "src").is_dir()
26
+ is_ts = "package.json" in files
27
+
28
+ # 2. Build Layers (Ordered by specificity)
29
+ layers: list[str] = ["ALL"]
30
+
31
+ # Base Tooling
32
+ if has_py:
33
+ layers.append("ALL-PY")
34
+ elif is_ts:
35
+ layers.append("ALL-TS")
36
+
37
+ # Structural Overlays
38
+ if has_py and has_src:
39
+ layers.append("ALL-PY-SRC")
40
+
41
+ # 3. Course Overlays (Highest precedence)
42
+ rules = load_package_defaults()
43
+ if looks_like_course_repo(repo_name_only=repo_name, files=files, rules=rules):
44
+ layers.append("ALL-COURSE")
45
+
46
+ # Only add the source course layer if it's a src-layout
47
+ if has_py and has_src:
48
+ layers.append("ALL-COURSE-PY-SRC")
49
+
50
+ return layers
@@ -0,0 +1,136 @@
1
+ """Internal utilities, type-casters, and metadata parsers for layout inference."""
2
+
3
+ import importlib.resources
4
+ from pathlib import Path
5
+ import re
6
+ import tomllib
7
+ from typing import BinaryIO, cast
8
+
9
+ __all__ = ["looks_like_course_repo", "looks_like_pypi_package", "load_package_defaults"]
10
+
11
+ TomlTable = dict[str, object]
12
+
13
+
14
+ def load_package_defaults() -> dict[str, object]:
15
+ """Read the data properties packaged inside this tool's installation wheel."""
16
+ try:
17
+ ref = importlib.resources.files("dc_up").joinpath("data/defaults.toml")
18
+ with ref.open("rb") as f:
19
+ config = tomllib.load(cast(BinaryIO, f))
20
+ classification = config.get("classification")
21
+ if isinstance(classification, dict):
22
+ return cast(dict[str, object], classification)
23
+ except OSError:
24
+ return {}
25
+ except tomllib.TOMLDecodeError:
26
+ return {}
27
+ return {}
28
+
29
+
30
+ def looks_like_course_repo(
31
+ *, repo_name_only: str, files: set[str], rules: dict[str, object]
32
+ ) -> bool:
33
+ """Return whether a repository matches course layouts using data-driven patterns."""
34
+ normalized_slug = repo_name_only.lower()
35
+
36
+ # Guard admin/maintenance surfaces
37
+ if normalized_slug.endswith(("-00-admin", "-admin")):
38
+ return False
39
+
40
+ # Safely extract regex string from configuration
41
+ pattern_str = rules.get("course_pattern")
42
+ if not isinstance(pattern_str, str):
43
+ pattern_str = ""
44
+
45
+ # WHY: Use our existing type-caster to safely extract and cast the list of objects to strings.
46
+ prefixes = tuple(_as_string_list(rules.get("course_prefixes")))
47
+
48
+ # 1. Match regex pattern (enforces the pre-NN-post structure using dashes)
49
+ is_pattern_match = False
50
+ if pattern_str:
51
+ is_pattern_match = bool(re.match(pattern_str, normalized_slug))
52
+
53
+ # 2. Match legacy prefixes (fallback)
54
+ is_prefix_match = False
55
+ if prefixes and normalized_slug.startswith(prefixes):
56
+ is_prefix_match = True
57
+
58
+ # 3. Must match one of the naming rules, and must contain a pyproject.toml
59
+ return bool(is_pattern_match or is_prefix_match)
60
+
61
+
62
+ def looks_like_pypi_package(repo_root: Path) -> bool:
63
+ """Return whether pyproject.toml looks like a publishable package."""
64
+ pyproject_path = repo_root / "pyproject.toml"
65
+ if not pyproject_path.exists():
66
+ return False
67
+
68
+ try:
69
+ data = _read_toml(pyproject_path)
70
+ except OSError:
71
+ return False
72
+ except tomllib.TOMLDecodeError:
73
+ return False
74
+
75
+ project = _as_table(data.get("project"))
76
+ if not project:
77
+ return False
78
+
79
+ if _has_console_scripts(project):
80
+ return True
81
+
82
+ classifiers = _as_string_list(project.get("classifiers"))
83
+ if any("PyPI" in item for item in classifiers):
84
+ return True
85
+
86
+ tool = _as_table(data.get("tool"))
87
+ uv = _as_table(tool.get("uv"))
88
+
89
+ return _as_bool(uv.get("package"))
90
+
91
+
92
+ def _has_console_scripts(project: TomlTable) -> bool:
93
+ """Return whether project metadata declares console scripts."""
94
+ scripts = _as_string_dict(project.get("scripts"))
95
+ if scripts:
96
+ return True
97
+
98
+ entry_points = _as_table(project.get("entry-points"))
99
+ console_scripts = _as_string_dict(entry_points.get("console_scripts"))
100
+
101
+ return bool(console_scripts)
102
+
103
+
104
+ def _read_toml(path: Path) -> TomlTable:
105
+ """Read a TOML file as typed object data."""
106
+ with path.open("rb") as file:
107
+ return cast(TomlTable, tomllib.load(file))
108
+
109
+
110
+ def _as_table(value: object) -> TomlTable:
111
+ """Return value as a TOML table or an empty table."""
112
+ if isinstance(value, dict):
113
+ return cast(TomlTable, value)
114
+ return {}
115
+
116
+
117
+ def _as_string_list(value: object) -> list[str]:
118
+ """Return value as a list of strings."""
119
+ if not isinstance(value, list):
120
+ return []
121
+ return [str(item) for item in cast(list[object], value)]
122
+
123
+
124
+ def _as_string_dict(value: object) -> dict[str, str]:
125
+ """Return value as a string-to-string dictionary."""
126
+ if not isinstance(value, dict):
127
+ return {}
128
+ table = cast(dict[object, object], value)
129
+ return {str(key): str(item) for key, item in table.items()}
130
+
131
+
132
+ def _as_bool(value: object) -> bool:
133
+ """Return value as a boolean."""
134
+ if isinstance(value, bool):
135
+ return value
136
+ return False
@@ -10,6 +10,7 @@ uv run dc-up todo
10
10
 
11
11
  Equivalent uvx usage after release:
12
12
  uvx dc-up
13
+ uvx dc-up@latest
13
14
  uvx dc-up --write
14
15
  uvx dc-up todo
15
16
  """
@@ -40,6 +40,7 @@ def run(
40
40
  plan = build_update_plan(
41
41
  target=repository,
42
42
  source=source,
43
+ protected_paths=frozenset({"docs/api.md", "README.md"}),
43
44
  )
44
45
 
45
46
  print_update_plan(plan, write=write)
@@ -0,0 +1,13 @@
1
+ # src/dc_up/data/defaults.toml
2
+
3
+ [classification]
4
+ course_prefixes = [
5
+ "cintel-",
6
+ "datafun-",
7
+ "insights-",
8
+ "ml-",
9
+ "nlp-",
10
+ "streaming-"
11
+ ]
12
+
13
+ course_pattern = "^[a-zA-Z]+-\\d{2}-.+$"
@@ -1,4 +1,4 @@
1
- # dc-up-todo.toml
1
+ # src/dc_up/data/todo-surfaces.toml
2
2
 
3
3
  [review_paths]
4
4
  default = [
@@ -8,6 +8,7 @@ default = [
8
8
  pyproject = [
9
9
  "pyproject.toml",
10
10
  "uv.lock",
11
+ "docs/index.md",
11
12
  ]
12
13
 
13
14
  python_src = [
@@ -15,9 +16,7 @@ python_src = [
15
16
  "tests/**",
16
17
  ]
17
18
 
18
- zensical = [
19
- "docs/index.md",
20
- ]
19
+ zensical = []
21
20
 
22
21
  course_python_src = [
23
22
  "docs/project-instructions.md",
@@ -38,6 +38,7 @@ def detect_repository(root: Path | None = None) -> RepositoryContext:
38
38
 
39
39
  repo_url = f"https://github.com/{github_handle}/{repo_name}"
40
40
  site_url = f"https://{github_handle}.github.io/{repo_name}/"
41
+ src_package = _detect_src_package(local_repo_root_directory)
41
42
 
42
43
  layers = tuple(
43
44
  infer_layers(
@@ -53,6 +54,7 @@ def detect_repository(root: Path | None = None) -> RepositoryContext:
53
54
  repo_name=repo_name,
54
55
  repo_url=repo_url,
55
56
  site_url=site_url,
57
+ src_package=src_package,
56
58
  files=frozenset(files),
57
59
  layers=layers,
58
60
  )
@@ -157,3 +159,14 @@ def _detect_github_handle(root: Path) -> str | None:
157
159
  return None
158
160
 
159
161
  return match.group("owner")
162
+
163
+
164
+ def _detect_src_package(root: Path) -> str:
165
+ """Infer the primary package name from src/."""
166
+ src = root / "src"
167
+ if not src.exists():
168
+ return ""
169
+ for candidate in sorted(src.iterdir()):
170
+ if candidate.is_dir() and (candidate / "__init__.py").exists():
171
+ return candidate.name
172
+ return ""
@@ -23,6 +23,7 @@ def build_update_plan(
23
23
  *,
24
24
  target: RepositoryContext,
25
25
  source: TemplateSource,
26
+ protected_paths: frozenset[str] = frozenset(),
26
27
  ) -> UpdatePlan:
27
28
  """Build an update plan from discovered template files."""
28
29
  planned_files: list[PlannedFile] = []
@@ -71,7 +72,8 @@ def print_update_plan(plan: UpdatePlan, *, write: bool) -> None:
71
72
  f"{counts['current']} current, "
72
73
  f"{counts['changed']} changed, "
73
74
  f"{counts['missing']} missing, "
74
- f"{counts['no-template']} no-template"
75
+ f"{counts['no-template']} no-template, "
76
+ f"{counts['protected']} protected"
75
77
  )
76
78
 
77
79
  if not write:
@@ -183,6 +185,7 @@ def _status_counts(plan: UpdatePlan) -> dict[FileStatus, int]:
183
185
  "changed": 0,
184
186
  "missing": 0,
185
187
  "no-template": 0,
188
+ "protected": 0,
186
189
  }
187
190
 
188
191
  for file in plan.files:
@@ -202,3 +205,5 @@ def _status_label(status: FileStatus, *, write: bool) -> str:
202
205
  return "ADDED" if write else "WOULD ADD"
203
206
  case "no-template":
204
207
  return "NO TEMPLATE"
208
+ case "protected":
209
+ return "PROTECTED"
@@ -22,6 +22,7 @@ def render_template(text: str, target: RepositoryContext) -> str:
22
22
  "github_handle": target.github_handle,
23
23
  "repo_url": target.repo_url,
24
24
  "site_url": target.site_url,
25
+ "src_package": target.src_package,
25
26
  }
26
27
 
27
28
  rendered = text
@@ -17,6 +17,7 @@ FileStatus = Literal[
17
17
  "changed",
18
18
  "missing",
19
19
  "no-template",
20
+ "protected",
20
21
  ]
21
22
 
22
23
 
@@ -29,6 +30,7 @@ class RepositoryContext:
29
30
  repo_name: str
30
31
  repo_url: str
31
32
  site_url: str
33
+ src_package: str
32
34
  files: frozenset[str]
33
35
  layers: tuple[str, ...]
34
36
 
@@ -1,205 +0,0 @@
1
- """Baseline layer inference and managed file declarations."""
2
-
3
- from pathlib import Path
4
- import tomllib
5
- from typing import Literal, cast
6
-
7
- __all__ = [
8
- "PRESERVE_PATTERNS",
9
- "infer_layers",
10
- ]
11
-
12
- TomlTable = dict[str, object]
13
-
14
- # Single base stack classification per repo (first match wins, see _classify_stack).
15
- StackKind = Literal["ts", "notebook", "kafka", "pypi", "src"]
16
-
17
- COURSE_PREFIXES: tuple[str, ...] = (
18
- "cintel-",
19
- "datafun-",
20
- "insights-",
21
- "ml-",
22
- "nlp-",
23
- "streaming-",
24
- )
25
-
26
-
27
- PRESERVE_PATTERNS: tuple[str, ...] = (
28
- "README.md",
29
- "artifacts/**",
30
- "data/**",
31
- "docs/**",
32
- "notebooks/**",
33
- "sql/**",
34
- "src/**",
35
- "tests/**",
36
- )
37
-
38
-
39
- # WHY: Base layers describe the repo's stack. Indexed by StackKind so the
40
- # classification and the layer list can't drift apart, and so the prefix
41
- # duplication (ALL, ALL-PY, ...) is declared once per kind, not per branch.
42
- BASE_LAYERS: dict[StackKind, tuple[str, ...]] = {
43
- "kafka": ("ALL", "ALL-PY", "ALL-PY-KAFKA"),
44
- "notebook": ("ALL", "ALL-PY", "ALL-PY-NB"),
45
- "pypi": ("ALL", "ALL-PY", "ALL-PY-SRC", "ALL-PY-SRC-PYPI"),
46
- "src": ("ALL", "ALL-PY", "ALL-PY-SRC"),
47
- "ts": ("ALL", "ALL-TS"),
48
- }
49
-
50
- # WHY: Educational ("ALL-COURSE*") overrides, appended AFTER the base so the
51
- # ed versions win (last layer wins). ALL-COURSE is always first because it
52
- # overrides ALL; deeper course layers follow parent-first.
53
- # OBS: "ts" is unreachable for courses (course repos require pyproject.toml,
54
- # which the "ts" classification excludes) - it's defensive.
55
- # OBS: "notebook" gets ALL-COURSE only; there is no ALL-COURSE-PY-NB defined.
56
- COURSE_OVERLAYS: dict[StackKind, tuple[str, ...]] = {
57
- "kafka": ("ALL-COURSE", "ALL-COURSE-PY-SRC", "ALL-COURSE-PY-SRC-KAFKA"),
58
- "notebook": ("ALL-COURSE",),
59
- "pypi": ("ALL-COURSE", "ALL-COURSE-PY-SRC"),
60
- "src": ("ALL-COURSE", "ALL-COURSE-PY-SRC"),
61
- "ts": ("ALL-COURSE",),
62
- }
63
-
64
-
65
- def infer_layers(
66
- *,
67
- repo_root: Path,
68
- repo_name: str,
69
- files: set[str],
70
- ) -> list[str]:
71
- """Infer additive template layers for a repository.
72
-
73
- The base layers describe the repo's stack. If the repo is a course repo,
74
- the matching course overlay is appended AFTER the base so the educational
75
- files override their ALL / ALL-PY counterparts (last layer wins).
76
- """
77
- slug = repo_name.lower()
78
- kind = _classify_stack(repo_root=repo_root, slug=slug, files=files)
79
-
80
- layers: list[str] = list(BASE_LAYERS[kind])
81
-
82
- if _looks_like_course_repo(repo_name_only=repo_name, files=files):
83
- layers.extend(COURSE_OVERLAYS[kind])
84
-
85
- return layers
86
-
87
-
88
- def _classify_stack(
89
- *,
90
- repo_root: Path,
91
- slug: str,
92
- files: set[str],
93
- ) -> StackKind:
94
- """Classify a repository into one base stack kind (first match wins).
95
-
96
- Precedence is preserved from the original if/elif chain. This is the one
97
- place to special-case 'insights-' or 'ml-' later if they need it.
98
- """
99
- if "package.json" in files and "pyproject.toml" not in files:
100
- return "ts"
101
- if "notebook" in slug or slug.endswith("-notebooks"):
102
- return "notebook"
103
- if "kafka" in slug:
104
- return "kafka"
105
- if slug == "dc-up" or _looks_like_pypi_package(repo_root):
106
- return "pypi"
107
- return "src"
108
-
109
-
110
- def _has_console_scripts(project: TomlTable) -> bool:
111
- """Return whether project metadata declares console scripts."""
112
- scripts = _as_string_dict(project.get("scripts"))
113
- if scripts:
114
- return True
115
-
116
- entry_points = _as_table(project.get("entry-points"))
117
- console_scripts = _as_string_dict(entry_points.get("console_scripts"))
118
-
119
- return bool(console_scripts)
120
-
121
-
122
- def _read_toml(path: Path) -> TomlTable:
123
- """Read a TOML file as typed object data."""
124
- with path.open("rb") as file:
125
- return cast(TomlTable, tomllib.load(file))
126
-
127
-
128
- def _as_table(value: object) -> TomlTable:
129
- """Return value as a TOML table or an empty table."""
130
- if isinstance(value, dict):
131
- return cast(TomlTable, value)
132
- return {}
133
-
134
-
135
- def _as_string_list(value: object) -> list[str]:
136
- """Return value as a list of strings."""
137
- if not isinstance(value, list):
138
- return []
139
-
140
- return [str(item) for item in cast(list[object], value)]
141
-
142
-
143
- def _as_string_dict(value: object) -> dict[str, str]:
144
- """Return value as a string-to-string dictionary."""
145
- if not isinstance(value, dict):
146
- return {}
147
-
148
- table = cast(dict[object, object], value)
149
- return {str(key): str(item) for key, item in table.items()}
150
-
151
-
152
- def _as_bool(value: object) -> bool:
153
- """Return value as a boolean."""
154
- if isinstance(value, bool):
155
- return value
156
- return False
157
-
158
-
159
- def _looks_like_course_repo(*, repo_name_only: str, files: set[str]) -> bool:
160
- """Return whether a repository looks like a course project repo."""
161
- normalized_slug = repo_name_only.lower()
162
-
163
- if not normalized_slug.startswith(COURSE_PREFIXES):
164
- return False
165
-
166
- # Avoid admin/maintenance repos unless you decide they should get course docs.
167
- if normalized_slug.endswith("-00-admin") or normalized_slug.endswith("-admin"):
168
- return False
169
-
170
- # Require project repo shape, not just matching name.
171
- return "pyproject.toml" in files
172
-
173
-
174
- def _looks_like_pypi_package(repo_root: Path) -> bool:
175
- """Return whether pyproject.toml looks like a publishable package.
176
-
177
- This is intentionally stricter than "has pyproject + src" so ordinary
178
- course repos do not accidentally receive PyPI release surfaces.
179
- """
180
- pyproject_path = repo_root / "pyproject.toml"
181
- if not pyproject_path.exists():
182
- return False
183
-
184
- try:
185
- data = _read_toml(pyproject_path)
186
- except OSError:
187
- return False
188
- except tomllib.TOMLDecodeError:
189
- return False
190
-
191
- project = _as_table(data.get("project"))
192
- if not project:
193
- return False
194
-
195
- if _has_console_scripts(project):
196
- return True
197
-
198
- classifiers = _as_string_list(project.get("classifiers"))
199
- if any("PyPI" in item for item in classifiers):
200
- return True
201
-
202
- tool = _as_table(data.get("tool"))
203
- uv = _as_table(tool.get("uv"))
204
-
205
- return _as_bool(uv.get("package"))
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes