django-angular3 0.1.0__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_angular3/__init__.py +5 -0
- django_angular3/__main__.py +4 -0
- django_angular3/admin.py +4 -0
- django_angular3/angular.py +335 -0
- django_angular3/apps.py +7 -0
- django_angular3/build.py +66 -0
- django_angular3/cli.py +313 -0
- django_angular3/config.py +110 -0
- django_angular3/documents.py +52 -0
- django_angular3/examples/01_simple_crm/django-angular3.json +10 -0
- django_angular3/examples/01_simple_crm/manage.py +23 -0
- django_angular3/examples/01_simple_crm/shop/__init__.py +0 -0
- django_angular3/examples/01_simple_crm/shop/admin.py +17 -0
- django_angular3/examples/01_simple_crm/shop/apps.py +6 -0
- django_angular3/examples/01_simple_crm/shop/models.py +20 -0
- django_angular3/examples/01_simple_crm/shop/serializers.py +15 -0
- django_angular3/examples/01_simple_crm/shop/tests.py +1 -0
- django_angular3/examples/01_simple_crm/shop/views.py +24 -0
- django_angular3/examples/01_simple_crm/simple_crm/__init__.py +0 -0
- django_angular3/examples/01_simple_crm/simple_crm/asgi.py +7 -0
- django_angular3/examples/01_simple_crm/simple_crm/settings.py +107 -0
- django_angular3/examples/01_simple_crm/simple_crm/urls.py +14 -0
- django_angular3/examples/01_simple_crm/simple_crm/wsgi.py +7 -0
- django_angular3/examples/01_simple_crm/ui.json +13 -0
- django_angular3/examples/__init__.py +0 -0
- django_angular3/management/__init__.py +1 -0
- django_angular3/management/commands/__init__.py +1 -0
- django_angular3/management/commands/_base.py +57 -0
- django_angular3/management/commands/build_app.py +483 -0
- django_angular3/management/commands/export_schema.py +106 -0
- django_angular3/management/commands/ng_add.py +19 -0
- django_angular3/management/commands/ng_build.py +6 -0
- django_angular3/management/commands/ng_config.py +6 -0
- django_angular3/management/commands/ng_gen_app.py +22 -0
- django_angular3/management/commands/ng_new.py +6 -0
- django_angular3/management/commands/ng_openapi_gen.py +6 -0
- django_angular3/management/commands/ng_workspace.py +6 -0
- django_angular3/management/commands/ng_workspace_delete.py +6 -0
- django_angular3/management/commands/ng_workspace_modify.py +6 -0
- django_angular3/migrations/__init__.py +2 -0
- django_angular3/models.py +2 -0
- django_angular3/settings.py +123 -0
- django_angular3/static/django_angular3/.gitkeep +0 -0
- django_angular3/templates/django_angular3/.gitkeep +0 -0
- django_angular3/tools.py +134 -0
- django_angular3/urls.py +6 -0
- django_angular3/validation.py +169 -0
- django_angular3/views.py +2 -0
- django_angular3-0.1.0.dist-info/METADATA +296 -0
- django_angular3-0.1.0.dist-info/RECORD +54 -0
- django_angular3-0.1.0.dist-info/WHEEL +5 -0
- django_angular3-0.1.0.dist-info/entry_points.txt +2 -0
- django_angular3-0.1.0.dist-info/licenses/LICENSE +21 -0
- django_angular3-0.1.0.dist-info/top_level.txt +1 -0
django_angular3/cli.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .angular import (
|
|
10
|
+
AngularCommandError,
|
|
11
|
+
execute_invocations,
|
|
12
|
+
format_invocations,
|
|
13
|
+
resolve_angular_command,
|
|
14
|
+
)
|
|
15
|
+
from .build import create_build_plan, write_build_plan
|
|
16
|
+
from .config import ConfigError, load_project_config
|
|
17
|
+
from .validation import validate_openapi_file, validate_project_config, validate_ui_file
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
21
|
+
parser = argparse.ArgumentParser(prog="django-angular3")
|
|
22
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
23
|
+
|
|
24
|
+
validate_openapi = subparsers.add_parser(
|
|
25
|
+
"validate-openapi", help="Validate an OpenAPI source document."
|
|
26
|
+
)
|
|
27
|
+
validate_openapi.add_argument("path", help="Path to the OpenAPI document.")
|
|
28
|
+
|
|
29
|
+
validate_ui = subparsers.add_parser(
|
|
30
|
+
"validate-ui", help="Validate a UI definition document."
|
|
31
|
+
)
|
|
32
|
+
validate_ui.add_argument("path", help="Path to the UI definition document.")
|
|
33
|
+
|
|
34
|
+
validate_project = subparsers.add_parser(
|
|
35
|
+
"validate-project", help="Validate a django-angular3 project configuration."
|
|
36
|
+
)
|
|
37
|
+
validate_project.add_argument(
|
|
38
|
+
"path",
|
|
39
|
+
nargs="?",
|
|
40
|
+
default="django-angular3.json",
|
|
41
|
+
help="Path to the project config.",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
build = subparsers.add_parser(
|
|
45
|
+
"build", help="Validate a project and emit a deterministic build plan."
|
|
46
|
+
)
|
|
47
|
+
build.add_argument(
|
|
48
|
+
"path", nargs="?", default="django-angular3.json", help="Path to the config."
|
|
49
|
+
)
|
|
50
|
+
build.add_argument(
|
|
51
|
+
"--output",
|
|
52
|
+
default="build",
|
|
53
|
+
help="Directory where the build plan should be written.",
|
|
54
|
+
)
|
|
55
|
+
build.add_argument(
|
|
56
|
+
"--dry-run",
|
|
57
|
+
action="store_true",
|
|
58
|
+
help="Print the build plan instead of writing it to disk.",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
ng_new = subparsers.add_parser("ng_new", help="Create an empty Angular workspace.")
|
|
62
|
+
ng_new.add_argument(
|
|
63
|
+
"path", nargs="?", default=None, help="Path to the project config."
|
|
64
|
+
)
|
|
65
|
+
ng_new.add_argument(
|
|
66
|
+
"--dry-run",
|
|
67
|
+
action="store_true",
|
|
68
|
+
help=(
|
|
69
|
+
"Print the resolved Angular subprocess call list instead of "
|
|
70
|
+
"invoking Angular tooling."
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
ng_workspace = subparsers.add_parser(
|
|
75
|
+
"ng_workspace",
|
|
76
|
+
help="Bootstrap the configured Angular workspace with angular-django2.",
|
|
77
|
+
)
|
|
78
|
+
ng_workspace.add_argument(
|
|
79
|
+
"path", nargs="?", default=None, help="Path to the project config."
|
|
80
|
+
)
|
|
81
|
+
ng_workspace.add_argument(
|
|
82
|
+
"--dry-run",
|
|
83
|
+
action="store_true",
|
|
84
|
+
help=(
|
|
85
|
+
"Print the resolved Angular subprocess call list instead of "
|
|
86
|
+
"invoking Angular tooling."
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
ng_config = subparsers.add_parser(
|
|
91
|
+
"ng_config", help="Configure Angular workspace defaults."
|
|
92
|
+
)
|
|
93
|
+
ng_config.add_argument(
|
|
94
|
+
"path", nargs="?", default=None, help="Path to the project config."
|
|
95
|
+
)
|
|
96
|
+
ng_config.add_argument(
|
|
97
|
+
"--dry-run",
|
|
98
|
+
action="store_true",
|
|
99
|
+
help=(
|
|
100
|
+
"Print the resolved Angular subprocess call list instead of "
|
|
101
|
+
"invoking Angular tooling."
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
ng_build = subparsers.add_parser(
|
|
106
|
+
"ng_build", help="Build the configured Angular application."
|
|
107
|
+
)
|
|
108
|
+
ng_build.add_argument(
|
|
109
|
+
"path", nargs="?", default=None, help="Path to the project config."
|
|
110
|
+
)
|
|
111
|
+
ng_build.add_argument(
|
|
112
|
+
"--dry-run",
|
|
113
|
+
action="store_true",
|
|
114
|
+
help=(
|
|
115
|
+
"Print the resolved Angular subprocess call list instead of "
|
|
116
|
+
"invoking Angular tooling."
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
ng_gen_app = subparsers.add_parser(
|
|
121
|
+
"ng_gen_app",
|
|
122
|
+
help="Generate an Angular application in the configured workspace.",
|
|
123
|
+
)
|
|
124
|
+
ng_gen_app.add_argument(
|
|
125
|
+
"path", nargs="?", default=None, help="Path to the project config."
|
|
126
|
+
)
|
|
127
|
+
ng_gen_app.add_argument(
|
|
128
|
+
"--app-name", default=None, help="Optional Angular application name."
|
|
129
|
+
)
|
|
130
|
+
ng_gen_app.add_argument(
|
|
131
|
+
"--dry-run",
|
|
132
|
+
action="store_true",
|
|
133
|
+
help=(
|
|
134
|
+
"Print the resolved Angular subprocess call list instead of "
|
|
135
|
+
"invoking Angular tooling."
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
ng_openapi_gen = subparsers.add_parser(
|
|
140
|
+
"ng_openapi_gen", help="Run ng-openapi-gen for the configured OpenAPI source."
|
|
141
|
+
)
|
|
142
|
+
ng_openapi_gen.add_argument(
|
|
143
|
+
"path", nargs="?", default=None, help="Path to the project config."
|
|
144
|
+
)
|
|
145
|
+
ng_openapi_gen.add_argument(
|
|
146
|
+
"--dry-run",
|
|
147
|
+
action="store_true",
|
|
148
|
+
help=(
|
|
149
|
+
"Print the resolved Angular subprocess call list instead of "
|
|
150
|
+
"invoking Angular tooling."
|
|
151
|
+
),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
ng_add = subparsers.add_parser("ng_add", help="Run ng add for an Angular package.")
|
|
155
|
+
ng_add.add_argument(
|
|
156
|
+
"path", nargs="?", default=None, help="Path to the project config."
|
|
157
|
+
)
|
|
158
|
+
ng_add.add_argument(
|
|
159
|
+
"--package",
|
|
160
|
+
default=None,
|
|
161
|
+
help="Package to add (defaults to setting: ng_add_package).",
|
|
162
|
+
)
|
|
163
|
+
ng_add.add_argument(
|
|
164
|
+
"--dry-run",
|
|
165
|
+
action="store_true",
|
|
166
|
+
help=(
|
|
167
|
+
"Print the resolved Angular subprocess call list instead of "
|
|
168
|
+
"invoking Angular tooling."
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
install_tutorial = subparsers.add_parser(
|
|
173
|
+
"install-tutorial",
|
|
174
|
+
help="Install the simple_crm tutorial project to a local directory.",
|
|
175
|
+
)
|
|
176
|
+
install_tutorial.add_argument(
|
|
177
|
+
"dest",
|
|
178
|
+
nargs="?",
|
|
179
|
+
default="simple_crm",
|
|
180
|
+
help="Destination directory (default: simple_crm).",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return parser
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
187
|
+
parser = build_parser()
|
|
188
|
+
args = parser.parse_args(argv)
|
|
189
|
+
|
|
190
|
+
if args.command == "validate-openapi":
|
|
191
|
+
return _run_validation(
|
|
192
|
+
validate_openapi_file(args.path), f"OpenAPI document {args.path}"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
if args.command == "validate-ui":
|
|
196
|
+
return _run_validation(validate_ui_file(args.path), f"UI document {args.path}")
|
|
197
|
+
|
|
198
|
+
if args.command == "validate-project":
|
|
199
|
+
try:
|
|
200
|
+
config = load_project_config(args.path)
|
|
201
|
+
except ConfigError as exc:
|
|
202
|
+
print(f"Configuration error: {exc}", file=sys.stderr)
|
|
203
|
+
return 1
|
|
204
|
+
return _run_validation(
|
|
205
|
+
validate_project_config(config), f"Project configuration {Path(args.path)}"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if args.command == "build":
|
|
209
|
+
try:
|
|
210
|
+
config = load_project_config(args.path)
|
|
211
|
+
except ConfigError as exc:
|
|
212
|
+
print(f"Configuration error: {exc}", file=sys.stderr)
|
|
213
|
+
return 1
|
|
214
|
+
|
|
215
|
+
errors = validate_project_config(config)
|
|
216
|
+
if errors:
|
|
217
|
+
return _run_validation(errors, f"Project configuration {Path(args.path)}")
|
|
218
|
+
|
|
219
|
+
plan = create_build_plan(config)
|
|
220
|
+
if args.dry_run:
|
|
221
|
+
print(json.dumps(plan.to_dict(), indent=2))
|
|
222
|
+
return 0
|
|
223
|
+
|
|
224
|
+
plan_path = write_build_plan(plan, args.output)
|
|
225
|
+
print(f"Wrote build plan to {plan_path}")
|
|
226
|
+
return 0
|
|
227
|
+
|
|
228
|
+
if args.command == "install-tutorial":
|
|
229
|
+
return _run_install_tutorial(args.dest)
|
|
230
|
+
|
|
231
|
+
if args.command in {
|
|
232
|
+
"ng_new",
|
|
233
|
+
"ng_workspace",
|
|
234
|
+
"ng_config",
|
|
235
|
+
"ng_build",
|
|
236
|
+
"ng_gen_app",
|
|
237
|
+
"ng_openapi_gen",
|
|
238
|
+
"ng_add",
|
|
239
|
+
}:
|
|
240
|
+
plan_options = {}
|
|
241
|
+
if args.command == "ng_gen_app":
|
|
242
|
+
plan_options["app_name"] = args.app_name
|
|
243
|
+
if args.command == "ng_add":
|
|
244
|
+
plan_options["package"] = args.package
|
|
245
|
+
return _run_angular_command(
|
|
246
|
+
args.command, args.path, dry_run=args.dry_run, **plan_options
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
parser.error("Unknown command")
|
|
250
|
+
return 2
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _run_validation(errors: list[str], label: str) -> int:
|
|
254
|
+
if errors:
|
|
255
|
+
print(f"{label} is invalid.", file=sys.stderr)
|
|
256
|
+
for error in errors:
|
|
257
|
+
print(f" - {error}", file=sys.stderr)
|
|
258
|
+
return 1
|
|
259
|
+
|
|
260
|
+
print(f"{label} is valid.")
|
|
261
|
+
return 0
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _run_angular_command(
|
|
265
|
+
command_name: str, path: str | Path | None, *, dry_run: bool, **options: str | None
|
|
266
|
+
) -> int:
|
|
267
|
+
try:
|
|
268
|
+
invocations = resolve_angular_command(command_name, path, **options)
|
|
269
|
+
except (AngularCommandError, ConfigError, TypeError, ValueError) as exc:
|
|
270
|
+
print(exc, file=sys.stderr)
|
|
271
|
+
return 1
|
|
272
|
+
|
|
273
|
+
if dry_run:
|
|
274
|
+
print(format_invocations(invocations))
|
|
275
|
+
return 0
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
execute_invocations(invocations)
|
|
279
|
+
except AngularCommandError as exc:
|
|
280
|
+
print(exc, file=sys.stderr)
|
|
281
|
+
return 1
|
|
282
|
+
|
|
283
|
+
print(f"Executed {len(invocations)} command(s).")
|
|
284
|
+
return 0
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _run_install_tutorial(dest: str) -> int:
|
|
288
|
+
import shutil
|
|
289
|
+
|
|
290
|
+
src = Path(__file__).parent / "examples" / "01_simple_crm"
|
|
291
|
+
dest_path = Path(dest)
|
|
292
|
+
|
|
293
|
+
if dest_path.exists():
|
|
294
|
+
print(f"Error: destination '{dest_path}' already exists.", file=sys.stderr)
|
|
295
|
+
return 1
|
|
296
|
+
|
|
297
|
+
shutil.copytree(
|
|
298
|
+
src,
|
|
299
|
+
dest_path,
|
|
300
|
+
ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "*.pyo"),
|
|
301
|
+
)
|
|
302
|
+
print(f"Tutorial project installed to '{dest_path}'.")
|
|
303
|
+
print()
|
|
304
|
+
print("Next steps:")
|
|
305
|
+
print(f" cd {dest_path}")
|
|
306
|
+
print(" python manage.py migrate")
|
|
307
|
+
print(" python manage.py createsuperuser")
|
|
308
|
+
print(" python manage.py runserver")
|
|
309
|
+
return 0
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
if __name__ == "__main__":
|
|
313
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .documents import DocumentError, load_document
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConfigError(ValueError):
|
|
11
|
+
"""Raised when the project configuration is invalid."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class ProjectConfig:
|
|
16
|
+
config_path: Path
|
|
17
|
+
project_name: str
|
|
18
|
+
openapi_source: Path
|
|
19
|
+
ui_source: Path
|
|
20
|
+
angular_output: Path
|
|
21
|
+
openapi_generator_config: Path | None = None
|
|
22
|
+
ng_openapi_gen_config: Path | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_project_config(path: str | Path) -> ProjectConfig:
|
|
26
|
+
config_path = Path(path).resolve()
|
|
27
|
+
try:
|
|
28
|
+
document: dict[str, Any] | None = load_document(config_path)
|
|
29
|
+
except DocumentError as exc:
|
|
30
|
+
raise ConfigError(str(exc)) from exc
|
|
31
|
+
|
|
32
|
+
if not isinstance(document, dict):
|
|
33
|
+
raise ConfigError("Project configuration must be a mapping.")
|
|
34
|
+
|
|
35
|
+
project: dict[str, Any] = _require_mapping(document, "project")
|
|
36
|
+
openapi: dict[str, Any] = _require_mapping(document, "openapi")
|
|
37
|
+
ui: dict[str, Any] = _require_mapping(document, "ui")
|
|
38
|
+
angular: dict[str, Any] = _require_mapping(document, "angular")
|
|
39
|
+
|
|
40
|
+
root = config_path.parent
|
|
41
|
+
project_name = _require_string(project, "name", section="project")
|
|
42
|
+
openapi_source = _resolve_path(
|
|
43
|
+
root, _require_string(openapi, "source", section="openapi")
|
|
44
|
+
)
|
|
45
|
+
ui_source = _resolve_path(root, _require_string(ui, "source", section="ui"))
|
|
46
|
+
angular_output_value = angular.get("output", angular.get("package"))
|
|
47
|
+
if not isinstance(angular_output_value, str) or not angular_output_value.strip():
|
|
48
|
+
raise ConfigError(
|
|
49
|
+
"Configuration value 'angular.output' must be a non-empty string."
|
|
50
|
+
)
|
|
51
|
+
angular_output = _resolve_path(root, angular_output_value)
|
|
52
|
+
|
|
53
|
+
openapi_generator_config = _optional_path(
|
|
54
|
+
root, openapi.get("openapiGeneratorConfig"), "openapi.openapiGeneratorConfig"
|
|
55
|
+
)
|
|
56
|
+
ng_openapi_gen_config = _optional_path(
|
|
57
|
+
root, openapi.get("ngOpenApiGenConfig"), "openapi.ngOpenApiGenConfig"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return ProjectConfig(
|
|
61
|
+
config_path=config_path,
|
|
62
|
+
project_name=project_name,
|
|
63
|
+
openapi_source=openapi_source,
|
|
64
|
+
ui_source=ui_source,
|
|
65
|
+
angular_output=angular_output,
|
|
66
|
+
openapi_generator_config=openapi_generator_config,
|
|
67
|
+
ng_openapi_gen_config=ng_openapi_gen_config,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _require_mapping(document: dict[str, Any], key: str) -> dict[str, Any]:
|
|
72
|
+
value: dict[str, Any] | None = document.get(key)
|
|
73
|
+
if not isinstance(value, dict):
|
|
74
|
+
raise ConfigError(f"Configuration section '{key}' must be a mapping.")
|
|
75
|
+
return value
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _require_string(document: dict[str, Any], key: str, *, section: str) -> str:
|
|
79
|
+
value = document.get(key)
|
|
80
|
+
if not isinstance(value, str) or not value.strip():
|
|
81
|
+
raise ConfigError(
|
|
82
|
+
f"Configuration value '{section}.{key}' must be a non-empty string."
|
|
83
|
+
)
|
|
84
|
+
return value
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _resolve_path(root: Path, raw_path: str) -> Path:
|
|
88
|
+
return (root / raw_path).resolve()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _optional_path(root: Path, value: Any, label: str) -> Path | None:
|
|
92
|
+
if value is None:
|
|
93
|
+
return None
|
|
94
|
+
if not isinstance(value, str) or not value.strip():
|
|
95
|
+
raise ConfigError(f"Configuration value '{label}' must be a non-empty string.")
|
|
96
|
+
return (root / value).resolve()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_previous_schema_path(source: Path) -> Path:
|
|
100
|
+
"""Return the conventional path for the previous schema artifact.
|
|
101
|
+
|
|
102
|
+
The previous schema is stored alongside the current schema with
|
|
103
|
+
``.previous`` inserted before the file extension. For example::
|
|
104
|
+
|
|
105
|
+
spec/openapi/source/api.json → spec/openapi/source/api.previous.json
|
|
106
|
+
|
|
107
|
+
This path is written by ``export_schema`` before the current schema is
|
|
108
|
+
overwritten, and consumed by ``build_app`` for change detection.
|
|
109
|
+
"""
|
|
110
|
+
return source.parent / (source.stem + ".previous" + source.suffix)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import tomllib
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import yaml # type: ignore
|
|
10
|
+
except ImportError: # pragma: no cover - exercised when optional dep absent
|
|
11
|
+
yaml = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DocumentError(ValueError):
|
|
15
|
+
"""Raised when a structured input document cannot be loaded."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_document(path: str | Path) -> Any:
|
|
19
|
+
file_path = Path(path)
|
|
20
|
+
if not file_path.exists():
|
|
21
|
+
raise DocumentError(f"Document does not exist: {file_path}")
|
|
22
|
+
|
|
23
|
+
suffix = file_path.suffix.lower()
|
|
24
|
+
text = file_path.read_text(encoding="utf-8")
|
|
25
|
+
|
|
26
|
+
if suffix == ".json":
|
|
27
|
+
try:
|
|
28
|
+
return json.loads(text)
|
|
29
|
+
except json.JSONDecodeError as exc:
|
|
30
|
+
raise DocumentError(f"Invalid JSON in {file_path}: {exc}") from exc
|
|
31
|
+
|
|
32
|
+
if suffix == ".toml":
|
|
33
|
+
try:
|
|
34
|
+
return tomllib.loads(text)
|
|
35
|
+
except tomllib.TOMLDecodeError as exc:
|
|
36
|
+
raise DocumentError(f"Invalid TOML in {file_path}: {exc}") from exc
|
|
37
|
+
|
|
38
|
+
if suffix in {".yaml", ".yml"}:
|
|
39
|
+
if yaml is None:
|
|
40
|
+
raise DocumentError(
|
|
41
|
+
"YAML support requires the optional 'yaml' extra. "
|
|
42
|
+
"Install with `pip install -e .[yaml]` or use JSON/TOML."
|
|
43
|
+
)
|
|
44
|
+
try:
|
|
45
|
+
return yaml.safe_load(text)
|
|
46
|
+
except yaml.YAMLError as exc:
|
|
47
|
+
raise DocumentError(f"Invalid YAML in {file_path}: {exc}") from exc
|
|
48
|
+
|
|
49
|
+
raise DocumentError(
|
|
50
|
+
f"Unsupported document format for {file_path}. "
|
|
51
|
+
"Use .json, .toml, .yaml, or .yml."
|
|
52
|
+
)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"project": { "name": "simple_crm" },
|
|
3
|
+
"app": { "name": "shop" },
|
|
4
|
+
"openapi": { "source": "schema.yaml" },
|
|
5
|
+
"ui": { "source": "ui.json" },
|
|
6
|
+
"angular": {
|
|
7
|
+
"output": "build/angular",
|
|
8
|
+
"workspace": { "packageManager": "pnpm", "style": "scss", "routing": true }
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Django's command-line utility for administrative tasks."""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
"""Run administrative tasks."""
|
|
10
|
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "simple_crm.settings")
|
|
11
|
+
try:
|
|
12
|
+
from django.core.management import execute_from_command_line
|
|
13
|
+
except ImportError as exc:
|
|
14
|
+
raise ImportError(
|
|
15
|
+
"Couldn't import Django. Are you sure it's installed and "
|
|
16
|
+
"available on your PYTHONPATH environment variable? Did you "
|
|
17
|
+
"forget to activate a virtual environment?"
|
|
18
|
+
) from exc
|
|
19
|
+
execute_from_command_line(sys.argv)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
|
|
3
|
+
from .models import Customer, Product
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CustomerAdmin(admin.ModelAdmin):
|
|
7
|
+
list_display = ["id", "name", "email", "active"]
|
|
8
|
+
list_editable = ["name", "email", "active"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ProductAdmin(admin.ModelAdmin):
|
|
12
|
+
list_display = ["id", "name", "price", "sku"]
|
|
13
|
+
list_editable = ["name", "price"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
admin.site.register(Customer, CustomerAdmin)
|
|
17
|
+
admin.site.register(Product, ProductAdmin)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Customer(models.Model):
|
|
5
|
+
name = models.CharField(max_length=120)
|
|
6
|
+
email = models.EmailField()
|
|
7
|
+
phone = models.CharField(max_length=50, blank=True)
|
|
8
|
+
active = models.BooleanField(default=True)
|
|
9
|
+
|
|
10
|
+
class Meta:
|
|
11
|
+
ordering = ["id"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Product(models.Model):
|
|
15
|
+
name = models.CharField(max_length=200)
|
|
16
|
+
price = models.FloatField()
|
|
17
|
+
sku = models.CharField(max_length=100, blank=True)
|
|
18
|
+
|
|
19
|
+
class Meta:
|
|
20
|
+
ordering = ["id"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from rest_framework import serializers
|
|
2
|
+
|
|
3
|
+
from .models import Customer, Product
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CustomerSerializer(serializers.ModelSerializer):
|
|
7
|
+
class Meta:
|
|
8
|
+
model = Customer
|
|
9
|
+
fields = ["id", "name", "email", "phone", "active"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ProductSerializer(serializers.ModelSerializer):
|
|
13
|
+
class Meta:
|
|
14
|
+
model = Product
|
|
15
|
+
fields = ["id", "name", "price", "sku"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Create your tests here.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from rest_framework import mixins, permissions, viewsets
|
|
2
|
+
|
|
3
|
+
from .models import Customer, Product
|
|
4
|
+
from .serializers import CustomerSerializer, ProductSerializer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CustomerViewSet(
|
|
8
|
+
mixins.ListModelMixin,
|
|
9
|
+
mixins.CreateModelMixin,
|
|
10
|
+
mixins.RetrieveModelMixin,
|
|
11
|
+
mixins.UpdateModelMixin,
|
|
12
|
+
mixins.DestroyModelMixin,
|
|
13
|
+
viewsets.GenericViewSet,
|
|
14
|
+
):
|
|
15
|
+
queryset = Customer.objects.all()
|
|
16
|
+
serializer_class = CustomerSerializer
|
|
17
|
+
permission_classes = [permissions.AllowAny]
|
|
18
|
+
http_method_names = ["get", "post", "patch", "delete", "head", "options"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ProductViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
|
|
22
|
+
queryset = Product.objects.all()
|
|
23
|
+
serializer_class = ProductSerializer
|
|
24
|
+
permission_classes = [permissions.AllowAny]
|
|
File without changes
|