dc-up 0.1.2__tar.gz → 0.2.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.
- {dc_up-0.1.2 → dc_up-0.2.0}/.gitignore +6 -3
- {dc_up-0.1.2 → dc_up-0.2.0}/CHANGELOG.md +13 -1
- {dc_up-0.1.2 → dc_up-0.2.0}/CITATION.cff +2 -2
- {dc_up-0.1.2 → dc_up-0.2.0}/PKG-INFO +1 -1
- {dc_up-0.1.2 → dc_up-0.2.0}/pyproject.toml +2 -1
- {dc_up-0.1.2 → dc_up-0.2.0}/src/dc_up/_version.py +2 -2
- dc_up-0.2.0/src/dc_up/baseline.py +50 -0
- dc_up-0.2.0/src/dc_up/baseline_utils.py +136 -0
- {dc_up-0.1.2 → dc_up-0.2.0}/src/dc_up/commands/update.py +1 -0
- dc_up-0.2.0/src/dc_up/data/defaults.toml +13 -0
- {dc_up-0.1.2 → dc_up-0.2.0}/src/dc_up/data/todo-surfaces.toml +3 -4
- {dc_up-0.1.2 → dc_up-0.2.0}/src/dc_up/plan.py +6 -1
- {dc_up-0.1.2 → dc_up-0.2.0}/src/dc_up/types.py +1 -0
- dc_up-0.1.2/src/dc_up/baseline.py +0 -205
- {dc_up-0.1.2 → dc_up-0.2.0}/LICENSE +0 -0
- {dc_up-0.1.2 → dc_up-0.2.0}/README.md +0 -0
- {dc_up-0.1.2 → dc_up-0.2.0}/src/dc_up/__init__.py +0 -0
- {dc_up-0.1.2 → dc_up-0.2.0}/src/dc_up/__main__.py +0 -0
- {dc_up-0.1.2 → dc_up-0.2.0}/src/dc_up/cli.py +0 -0
- {dc_up-0.1.2 → dc_up-0.2.0}/src/dc_up/commands/__init__.py +0 -0
- {dc_up-0.1.2 → dc_up-0.2.0}/src/dc_up/commands/todo.py +0 -0
- {dc_up-0.1.2 → dc_up-0.2.0}/src/dc_up/detect.py +0 -0
- {dc_up-0.1.2 → dc_up-0.2.0}/src/dc_up/errors.py +0 -0
- {dc_up-0.1.2 → dc_up-0.2.0}/src/dc_up/fetch.py +0 -0
- {dc_up-0.1.2 → dc_up-0.2.0}/src/dc_up/py.typed +0 -0
- {dc_up-0.1.2 → dc_up-0.2.0}/src/dc_up/render.py +0 -0
- {dc_up-0.1.2 → dc_up-0.2.0}/src/dc_up/todo.py +0 -0
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
# ============================================================
|
|
2
2
|
# .gitignore (ALL-REPOS)
|
|
3
3
|
# ============================================================
|
|
4
|
-
# Updated: 2026-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
|
-
|
|
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,17 @@ and this project adheres to **[Semantic Versioning](https://semver.org/spec/v2.0
|
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
+
## [0.2.0] - 2026-06-27
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Data-driven course prefixes
|
|
21
|
+
- And a more flexible course repo detection if repo is named like this:
|
|
22
|
+
`prefix-NN-post`
|
|
23
|
+
- better logic for ALL-PY (just tooling) vs ALL-PY-SRC with Ruff and Pyright
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
16
27
|
## [0.1.2] - 2026-06-10
|
|
17
28
|
|
|
18
29
|
### Changed
|
|
@@ -128,7 +139,8 @@ git push origin :refs/tags/vX.Z.Y
|
|
|
128
139
|
|
|
129
140
|
## Links
|
|
130
141
|
|
|
131
|
-
[Unreleased]: https://github.com/denisecase/dc-up/compare/v0.
|
|
142
|
+
[Unreleased]: https://github.com/denisecase/dc-up/compare/v0.2.0...HEAD
|
|
143
|
+
[0.2.0]: https://github.com/denisecase/dc-up/releases/tag/v0.2.0
|
|
132
144
|
[0.1.2]: https://github.com/denisecase/dc-up/releases/tag/v0.1.2
|
|
133
145
|
[0.1.1]: https://github.com/denisecase/dc-up/releases/tag/v0.1.1
|
|
134
146
|
[0.1.0]: https://github.com/denisecase/dc-up/releases/tag/v0.1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dc-up
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.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
|
|
@@ -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.
|
|
230
|
+
fallback-version = "0.2.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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.2.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
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",
|
|
@@ -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"
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|