django-compose 0.1.0.dev1__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.
- django_compose/__init__.py +0 -0
- django_compose/__main__.py +5 -0
- django_compose/addons.py +205 -0
- django_compose/apps.py +103 -0
- django_compose/artifacts.py +224 -0
- django_compose/asgi.py +110 -0
- django_compose/autoreload.py +110 -0
- django_compose/build.py +282 -0
- django_compose/checks.py +11 -0
- django_compose/cli.py +12 -0
- django_compose/conf.py +322 -0
- django_compose/declarations.py +168 -0
- django_compose/history.py +36 -0
- django_compose/management/__init__.py +221 -0
- django_compose/management/commands/__init__.py +1 -0
- django_compose/management/commands/check_migrations.py +57 -0
- django_compose/management/commands/compose.py +42 -0
- django_compose/management/commands/makemigrations.py +136 -0
- django_compose/management/commands/migrate.py +18 -0
- django_compose/management/commands/reset_migration_history.py +34 -0
- django_compose/management/commands/retire_history.py +24 -0
- django_compose/migration_imports.py +474 -0
- django_compose/migrations.py +843 -0
- django_compose/model_composition.py +292 -0
- django_compose/models.py +65 -0
- django_compose/py.typed +0 -0
- django_compose/rendering.py +92 -0
- django_compose/serving.py +405 -0
- django_compose/snapshot.py +419 -0
- django_compose/sources.py +147 -0
- django_compose/typing.py +633 -0
- django_compose/urls.py +58 -0
- django_compose-0.1.0.dev1.dist-info/METADATA +101 -0
- django_compose-0.1.0.dev1.dist-info/RECORD +39 -0
- django_compose-0.1.0.dev1.dist-info/WHEEL +4 -0
- django_compose-0.1.0.dev1.dist-info/entry_points.txt +2 -0
- django_compose-0.1.0.dev1.dist-info/licenses/COPYING +674 -0
- django_compose-0.1.0.dev1.dist-info/licenses/LICENSE +165 -0
- django_compose-0.1.0.dev1.dist-info/licenses/NOTICE +10 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Compiler input evidence and adapters to Django's native reload backends."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from collections.abc import Iterable
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
12
|
+
from django.utils import autoreload
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InputReport:
|
|
16
|
+
"""Declared candidates survive failed reads and empty glob expansion."""
|
|
17
|
+
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
self.files: set[Path] = set()
|
|
20
|
+
self.globs: set[tuple[Path, str]] = set()
|
|
21
|
+
|
|
22
|
+
def add(self, *, files: Iterable[str | Path] = (), globs: Iterable[tuple[str | Path, str]] = ()) -> None:
|
|
23
|
+
self.files.update(Path(path).absolute() for path in files)
|
|
24
|
+
self.globs.update((Path(path).absolute(), pattern) for path, pattern in globs)
|
|
25
|
+
|
|
26
|
+
def payload(self) -> dict[str, Any]:
|
|
27
|
+
return {
|
|
28
|
+
"files": sorted(map(str, self.files)),
|
|
29
|
+
"globs": sorted((str(path), pattern) for path, pattern in self.globs),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
def load(self, payload: dict[str, Any]) -> None:
|
|
33
|
+
self.add(files=payload["files"], globs=payload["globs"])
|
|
34
|
+
|
|
35
|
+
def state(self) -> tuple[dict[Path, bool], dict[tuple[Path, str], frozenset[Path]]]:
|
|
36
|
+
return (
|
|
37
|
+
{path: path.exists() for path in self.files},
|
|
38
|
+
{item: frozenset(item[0].glob(item[1])) for item in self.globs},
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def attach(self, reloader: Any) -> None:
|
|
42
|
+
"""Extend native tick polling, retaining native content-change detection."""
|
|
43
|
+
reloader.extra_files.update(self.files)
|
|
44
|
+
for directory, pattern in sorted(self.globs):
|
|
45
|
+
reloader.watch_dir(directory, pattern)
|
|
46
|
+
previous_files, previous_globs = self.state()
|
|
47
|
+
native_tick = reloader.tick
|
|
48
|
+
|
|
49
|
+
def tick() -> Any:
|
|
50
|
+
nonlocal previous_files, previous_globs
|
|
51
|
+
for result in native_tick():
|
|
52
|
+
current_files, current_globs = self.state()
|
|
53
|
+
changed = {
|
|
54
|
+
path for path, exists in previous_files.items() if current_files.get(path) != exists
|
|
55
|
+
}
|
|
56
|
+
for item, members in previous_globs.items():
|
|
57
|
+
changed.update(members ^ current_globs.get(item, frozenset()))
|
|
58
|
+
previous_files, previous_globs = current_files, current_globs
|
|
59
|
+
for path in sorted(changed):
|
|
60
|
+
reloader.notify_file_changed(path)
|
|
61
|
+
yield result
|
|
62
|
+
|
|
63
|
+
reloader.tick = tick
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
inputs = InputReport()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def report_inputs(*, files: Iterable[str | Path] = (), globs: Iterable[tuple[str | Path, str]] = ()) -> None:
|
|
70
|
+
"""Publish settings-reader candidates BEFORE reading, even when absent."""
|
|
71
|
+
inputs.add(files=files, globs=globs)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def report_watch_paths(paths: Iterable[str | Path | tuple[str | Path, str]]) -> tuple[Any, ...]:
|
|
75
|
+
"""Validate/report the mixed COMPOSE_WATCH_PATHS setting once at its owner."""
|
|
76
|
+
if isinstance(paths, (str, Path)):
|
|
77
|
+
raise ImproperlyConfigured(
|
|
78
|
+
"COMPOSE_WATCH_PATHS must be a sequence of files or (directory, glob) pairs"
|
|
79
|
+
)
|
|
80
|
+
normalized: list[Any] = []
|
|
81
|
+
for item in paths:
|
|
82
|
+
if isinstance(item, (str, Path)):
|
|
83
|
+
report_inputs(files=[item])
|
|
84
|
+
normalized.append(str(item))
|
|
85
|
+
elif (
|
|
86
|
+
isinstance(item, (tuple, list))
|
|
87
|
+
and len(item) == 2
|
|
88
|
+
and isinstance(item[0], (str, Path))
|
|
89
|
+
and isinstance(item[1], str)
|
|
90
|
+
and item[1]
|
|
91
|
+
):
|
|
92
|
+
report_inputs(globs=[(item[0], item[1])])
|
|
93
|
+
normalized.append((str(item[0]), item[1]))
|
|
94
|
+
else:
|
|
95
|
+
raise ImproperlyConfigured("COMPOSE_WATCH_PATHS requires files or (directory, glob) pairs")
|
|
96
|
+
return tuple(normalized)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def write_report(fd: int) -> None:
|
|
100
|
+
"""Write a report to a caller-owned regular inherited descriptor."""
|
|
101
|
+
with os.fdopen(os.dup(fd), "w") as stream:
|
|
102
|
+
json.dump(inputs.payload(), stream)
|
|
103
|
+
stream.write("\n")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _started(sender: Any, **kwargs: Any) -> None:
|
|
107
|
+
inputs.attach(sender)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
autoreload.autoreload_started.connect(_started, weak=False, dispatch_uid="django_compose.inputs")
|
django_compose/build.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""Compose-only model emission, concrete imports, artifacts and final publication."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import os
|
|
7
|
+
from collections.abc import Iterable
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Literal
|
|
11
|
+
|
|
12
|
+
from django.apps import AppConfig, apps
|
|
13
|
+
from django.apps.registry import Apps
|
|
14
|
+
from django.conf import settings
|
|
15
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
16
|
+
|
|
17
|
+
from django_compose import serving
|
|
18
|
+
from django_compose.artifacts import GeneratedTree, atomic_write, receipt_lock
|
|
19
|
+
from django_compose.declarations import ArtifactHook, ModelDeclarationHook, ModelsView, model_declaration
|
|
20
|
+
from django_compose.model_composition import ModelComposition
|
|
21
|
+
from django_compose.rendering import render_models
|
|
22
|
+
from django_compose.serving import Receipt, read_receipt, verify
|
|
23
|
+
from django_compose.snapshot import Snapshot
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def discover(configs: Iterable[AppConfig]) -> ModelComposition:
|
|
27
|
+
return ModelComposition.discover(tuple(configs))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class ConcreteModels:
|
|
32
|
+
composition: ModelComposition
|
|
33
|
+
|
|
34
|
+
def get_model(self, label: str, name: str) -> Any:
|
|
35
|
+
return apps.get_model(label, name, require_ready=False)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BuildPlan:
|
|
39
|
+
"""One phase-2 build, retaining its writer lock until final publication."""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
snap: Snapshot,
|
|
44
|
+
configs: Iterable[AppConfig],
|
|
45
|
+
*,
|
|
46
|
+
mode: Literal["write", "check"],
|
|
47
|
+
integrations: tuple[object, ...],
|
|
48
|
+
transitions: Iterable[Any],
|
|
49
|
+
output_dir: Path,
|
|
50
|
+
output_module: str,
|
|
51
|
+
) -> None:
|
|
52
|
+
if mode not in ("write", "check"):
|
|
53
|
+
raise ImproperlyConfigured("BuildPlan requires write or check mode")
|
|
54
|
+
if (
|
|
55
|
+
not all(part.isidentifier() for part in output_module.split("."))
|
|
56
|
+
or not output_dir.name.isidentifier()
|
|
57
|
+
):
|
|
58
|
+
raise ImproperlyConfigured("Output package must have Python identifiers")
|
|
59
|
+
self.snapshot = snap
|
|
60
|
+
selected = {entry.name for entry in snap.entries}
|
|
61
|
+
self.configs = tuple(c for c in configs if c.name in selected)
|
|
62
|
+
self.mode = mode
|
|
63
|
+
self.integrations = integrations
|
|
64
|
+
self.transitions = tuple(transitions)
|
|
65
|
+
self.output_dir = output_dir
|
|
66
|
+
self.output_module = output_module
|
|
67
|
+
self.previous = read_receipt(output_dir)
|
|
68
|
+
self.sources: dict[Path, str | bytes] = {}
|
|
69
|
+
self._lock = receipt_lock(
|
|
70
|
+
output_dir,
|
|
71
|
+
write=mode == "write",
|
|
72
|
+
wait=float(os.environ["DJANGO_COMPOSE_WAIT"]) if "DJANGO_COMPOSE_WAIT" in os.environ else 5.0,
|
|
73
|
+
)
|
|
74
|
+
self._locked = False
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def labels(self) -> tuple[str, ...]:
|
|
78
|
+
return self.composition.labels
|
|
79
|
+
|
|
80
|
+
def _receipt(self, *, progress: bool, hashes: dict[str, str] | None = None) -> Receipt:
|
|
81
|
+
previous = self.previous
|
|
82
|
+
retired = previous.RETIRED_LABELS if previous else ()
|
|
83
|
+
history = set(previous.HISTORY_LABELS if previous else ())
|
|
84
|
+
history.update(getattr(self, "labels", ()))
|
|
85
|
+
history.update(path.parent.name for path in self.output_dir.glob("*/migrations") if path.is_dir())
|
|
86
|
+
history.update(transition["app_label"] for transition in self.transitions)
|
|
87
|
+
return Receipt(
|
|
88
|
+
tuple(sorted(history - set(retired))),
|
|
89
|
+
retired,
|
|
90
|
+
self.snapshot.digest(),
|
|
91
|
+
hashes if hashes is not None else (dict(previous.OWNED_FILES) if previous else {}),
|
|
92
|
+
self.snapshot.compiler,
|
|
93
|
+
tuple(getattr(self, "labels", ())),
|
|
94
|
+
progress,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def emit(self) -> BuildPlan:
|
|
98
|
+
self._lock.__enter__()
|
|
99
|
+
self._locked = True
|
|
100
|
+
try:
|
|
101
|
+
serving._register_build(self.close)
|
|
102
|
+
if self.mode == "check":
|
|
103
|
+
self.previous = verify(self.snapshot, self.output_module)
|
|
104
|
+
# Guard ownership before invalidation. A failed discovery remains invalid.
|
|
105
|
+
self.previous = GeneratedTree(self.output_dir, {})._validate()
|
|
106
|
+
if self.mode == "write":
|
|
107
|
+
receipt = self._receipt(progress=True)
|
|
108
|
+
if self.previous is None:
|
|
109
|
+
self.sources.update({Path(label) / "__init__.py": "" for label in receipt.HISTORY_LABELS})
|
|
110
|
+
receipt = self._receipt(
|
|
111
|
+
progress=True, hashes=GeneratedTree(self.output_dir, self.sources).hashes()
|
|
112
|
+
)
|
|
113
|
+
atomic_write(self.output_dir / "__init__.py", receipt.source().encode())
|
|
114
|
+
self.composition = discover(self.configs)
|
|
115
|
+
models = {model for config in self.configs for model in (config.models or {}).values()}
|
|
116
|
+
models.update(self.composition.ordered_models)
|
|
117
|
+
for source in self.composition.ordered_models:
|
|
118
|
+
models.update(self.composition.donors(source))
|
|
119
|
+
for model in sorted(models, key=lambda model: model._meta.label):
|
|
120
|
+
swappable = model._meta.swappable
|
|
121
|
+
if swappable and swappable not in self.snapshot.settings:
|
|
122
|
+
raise ImproperlyConfigured(
|
|
123
|
+
f"Swappable setting {swappable!r} for {model._meta.label} must be declared "
|
|
124
|
+
"in COMPOSE_FINGERPRINT_SETTINGS"
|
|
125
|
+
)
|
|
126
|
+
declarations = {
|
|
127
|
+
source: model_declaration(
|
|
128
|
+
self.composition,
|
|
129
|
+
source,
|
|
130
|
+
tuple(h for h in self.integrations if isinstance(h, ModelDeclarationHook)),
|
|
131
|
+
)
|
|
132
|
+
for source in self.composition.ordered_models
|
|
133
|
+
}
|
|
134
|
+
history_labels = self._receipt(progress=True).HISTORY_LABELS
|
|
135
|
+
for label in history_labels:
|
|
136
|
+
self.sources[Path(label) / "__init__.py"] = ""
|
|
137
|
+
config = next((c for c in self.configs if c.label == label), None)
|
|
138
|
+
if config and label in self.labels:
|
|
139
|
+
config_path = f"{type(config).__module__}.{type(config).__qualname__}"
|
|
140
|
+
module, _, name = config_path.rpartition(".")
|
|
141
|
+
model_module = f"{self.output_module}.{label}.models"
|
|
142
|
+
body = (
|
|
143
|
+
f"from {module} import {name} as SourceConfig\n\n"
|
|
144
|
+
"class GeneratedConfig(SourceConfig):\n"
|
|
145
|
+
f" name = {config.name!r}\n label = {label!r}\n\n"
|
|
146
|
+
" def import_models(self):\n"
|
|
147
|
+
" super().import_models()\n"
|
|
148
|
+
" from importlib import import_module\n"
|
|
149
|
+
f" self.models_module = import_module({model_module!r})\n"
|
|
150
|
+
)
|
|
151
|
+
self.sources[Path(label) / "apps.py"] = body
|
|
152
|
+
if label in self.labels:
|
|
153
|
+
self.sources[Path(label) / "models.py"] = render_models(
|
|
154
|
+
self.composition,
|
|
155
|
+
label,
|
|
156
|
+
runtime_module=self.output_module,
|
|
157
|
+
declarations=declarations,
|
|
158
|
+
)
|
|
159
|
+
migration_init = self.output_dir / label / "migrations" / "__init__.py"
|
|
160
|
+
effective = getattr(settings, "MIGRATION_MODULES", {}).get(
|
|
161
|
+
label, f"{self.output_module}.{label}.migrations"
|
|
162
|
+
)
|
|
163
|
+
if effective == f"{self.output_module}.{label}.migrations" and not migration_init.exists():
|
|
164
|
+
self.sources[Path(label) / "migrations" / "__init__.py"] = ""
|
|
165
|
+
if self.mode == "check":
|
|
166
|
+
changed = [
|
|
167
|
+
p
|
|
168
|
+
for p, value in self.sources.items()
|
|
169
|
+
if not (self.output_dir / p).is_file()
|
|
170
|
+
or (self.output_dir / p).read_bytes() != GeneratedTree._bytes(value)
|
|
171
|
+
]
|
|
172
|
+
if changed:
|
|
173
|
+
raise ImproperlyConfigured(f"Generated models are stale: {changed}; run compose")
|
|
174
|
+
else:
|
|
175
|
+
GeneratedTree(self.output_dir, self.sources).reconcile(prune=False)
|
|
176
|
+
provisional = dict(self.previous.OWNED_FILES) if self.previous else {}
|
|
177
|
+
provisional.update(GeneratedTree(self.output_dir, self.sources).hashes())
|
|
178
|
+
atomic_write(
|
|
179
|
+
self.output_dir / "__init__.py",
|
|
180
|
+
self._receipt(progress=True, hashes=provisional).source().encode(),
|
|
181
|
+
)
|
|
182
|
+
importlib.invalidate_caches()
|
|
183
|
+
for label in self.labels:
|
|
184
|
+
importlib.import_module(f"{self.output_module}.{label}.models")
|
|
185
|
+
return self
|
|
186
|
+
except BaseException:
|
|
187
|
+
self.close()
|
|
188
|
+
raise
|
|
189
|
+
|
|
190
|
+
def render_artifacts(self, models_view: ModelsView | Apps, hooks: Iterable[ArtifactHook]) -> None:
|
|
191
|
+
external = set(getattr(settings, "COMPOSE_EXTERNAL_ARTIFACT_ROOTS", ()))
|
|
192
|
+
receipt = self._receipt(progress=True)
|
|
193
|
+
reserved = {*receipt.MODEL_LABELS, *receipt.HISTORY_LABELS, *receipt.RETIRED_LABELS}
|
|
194
|
+
for hook in hooks:
|
|
195
|
+
root = Path(hook.external_root)
|
|
196
|
+
if (
|
|
197
|
+
root.is_absolute()
|
|
198
|
+
or len(root.parts) != 1
|
|
199
|
+
or root.name in {*reserved, "migrations", "__pycache__", ".."}
|
|
200
|
+
or root.as_posix() not in external
|
|
201
|
+
):
|
|
202
|
+
raise ImproperlyConfigured(f"Undeclared artifact root: {hook.external_root!r}")
|
|
203
|
+
rendered = hook.render(self.composition, models_view)
|
|
204
|
+
GeneratedTree.validate_artifacts(self.output_dir, rendered)
|
|
205
|
+
for path, content in sorted(rendered.items()):
|
|
206
|
+
if (
|
|
207
|
+
path in self.sources
|
|
208
|
+
or len(path.parts) < 2
|
|
209
|
+
or path.parts[0] != root.as_posix()
|
|
210
|
+
or "migrations" in path.parts
|
|
211
|
+
):
|
|
212
|
+
raise ImproperlyConfigured(f"Competing or undeclared artifact: {path}")
|
|
213
|
+
if not isinstance(content, bytes):
|
|
214
|
+
raise ImproperlyConfigured(f"ArtifactHook must render bytes: {path}")
|
|
215
|
+
self.sources[path] = content
|
|
216
|
+
tree = GeneratedTree(self.output_dir, self.sources)
|
|
217
|
+
tree._validate()
|
|
218
|
+
if self.mode == "check":
|
|
219
|
+
if any(
|
|
220
|
+
not (self.output_dir / path).is_file()
|
|
221
|
+
or (self.output_dir / path).read_bytes() != tree._bytes(value)
|
|
222
|
+
for path, value in self.sources.items()
|
|
223
|
+
):
|
|
224
|
+
raise ImproperlyConfigured("Generated artifacts are stale; run compose")
|
|
225
|
+
else:
|
|
226
|
+
current = read_receipt(self.output_dir)
|
|
227
|
+
provisional = dict(current.OWNED_FILES) if current else {}
|
|
228
|
+
provisional.update(tree.hashes())
|
|
229
|
+
atomic_write(
|
|
230
|
+
self.output_dir / "__init__.py",
|
|
231
|
+
self._receipt(progress=True, hashes=provisional).source().encode(),
|
|
232
|
+
)
|
|
233
|
+
tree.reconcile(prune=False)
|
|
234
|
+
serving._publish_build_artifacts(
|
|
235
|
+
self.close, self.output_dir, self._receipt(progress=True, hashes=tree.hashes())
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
def finalize(self) -> Receipt:
|
|
239
|
+
try:
|
|
240
|
+
receipt = self._receipt(
|
|
241
|
+
progress=False, hashes=GeneratedTree(self.output_dir, self.sources).hashes()
|
|
242
|
+
)
|
|
243
|
+
tree = GeneratedTree(self.output_dir, self.sources)
|
|
244
|
+
if self.mode == "write":
|
|
245
|
+
tree.reconcile()
|
|
246
|
+
atomic_write(self.output_dir / "__init__.py", receipt.source().encode())
|
|
247
|
+
elif tree.drift() or self.previous != receipt:
|
|
248
|
+
raise ImproperlyConfigured("Generated receipt or artifacts are stale; run compose")
|
|
249
|
+
return receipt
|
|
250
|
+
finally:
|
|
251
|
+
self.close()
|
|
252
|
+
|
|
253
|
+
def close(self) -> None:
|
|
254
|
+
serving._clear_build(self.close)
|
|
255
|
+
if self._locked:
|
|
256
|
+
self._locked = False
|
|
257
|
+
self._lock.__exit__(None, None, None)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def emit(
|
|
261
|
+
snap: Snapshot,
|
|
262
|
+
configs: Iterable[AppConfig],
|
|
263
|
+
*,
|
|
264
|
+
mode: Literal["write", "check"],
|
|
265
|
+
integrations: tuple[object, ...],
|
|
266
|
+
transitions: Iterable[Any],
|
|
267
|
+
output_dir: Path | str,
|
|
268
|
+
output_module: str,
|
|
269
|
+
) -> BuildPlan:
|
|
270
|
+
return BuildPlan(
|
|
271
|
+
snap,
|
|
272
|
+
configs,
|
|
273
|
+
mode=mode,
|
|
274
|
+
integrations=integrations,
|
|
275
|
+
transitions=transitions,
|
|
276
|
+
output_dir=Path(output_dir),
|
|
277
|
+
output_module=output_module,
|
|
278
|
+
).emit()
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def clean(output_dir: Path | str) -> tuple[Path, ...]:
|
|
282
|
+
return GeneratedTree(Path(output_dir), {}).clean()
|
django_compose/checks.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Bootstrap-safe registration of deferred native serving checks."""
|
|
2
|
+
|
|
3
|
+
from django.core.checks import Error, Tags, register
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@register(Tags.urls)
|
|
7
|
+
def check_url_collisions(**kwargs: object) -> list[Error]:
|
|
8
|
+
"""Load URL policy only when native checks run after setup."""
|
|
9
|
+
from django_compose.urls import check_url_collisions as check
|
|
10
|
+
|
|
11
|
+
return check(**kwargs)
|
django_compose/cli.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""The standalone launcher uses the same native management entrypoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main(argv: list[str] | None = None) -> int:
|
|
9
|
+
from django_compose.management import execute
|
|
10
|
+
|
|
11
|
+
execute(["django-compose", *(sys.argv[1:] if argv is None else argv)])
|
|
12
|
+
return 0
|