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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Management command: export_schema
|
|
2
|
+
|
|
3
|
+
Export the OpenAPI schema from DRF using drf-spectacular and persist it as a
|
|
4
|
+
durable, versioned artifact at the path configured in ``openapi.source``.
|
|
5
|
+
|
|
6
|
+
Before writing, the command rotates the existing schema to its ``.previous``
|
|
7
|
+
counterpart (e.g. ``api.json`` → ``api.previous.json``) so that ``build_app``
|
|
8
|
+
can compare the two versions for change detection without requiring the caller
|
|
9
|
+
to manage file paths explicitly.
|
|
10
|
+
|
|
11
|
+
Usage::
|
|
12
|
+
|
|
13
|
+
django-admin export_schema django-angular3.json
|
|
14
|
+
django-admin export_schema django-angular3.json --format yaml
|
|
15
|
+
django-admin export_schema django-angular3.json --dry-run
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
|
|
22
|
+
from django.core.management.base import BaseCommand, CommandError
|
|
23
|
+
|
|
24
|
+
from ...config import ConfigError, get_previous_schema_path, load_project_config
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Command(BaseCommand):
|
|
28
|
+
help = (
|
|
29
|
+
"Export the OpenAPI schema from DRF (via drf-spectacular) and persist "
|
|
30
|
+
"it as a versioned artifact. The previous schema is archived alongside "
|
|
31
|
+
"the current one for use by build_app change detection."
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"config",
|
|
37
|
+
help="Path to the django-angular3.json project config file.",
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--format",
|
|
41
|
+
dest="format",
|
|
42
|
+
choices=["json", "yaml"],
|
|
43
|
+
default="json",
|
|
44
|
+
help="Serialization format for the exported schema (default: json).",
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--dry-run",
|
|
48
|
+
action="store_true",
|
|
49
|
+
help=(
|
|
50
|
+
"Generate the schema and display the artifact paths that would be "
|
|
51
|
+
"written, but do not modify any files on disk."
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def handle(self, *args, **options) -> None:
|
|
56
|
+
try:
|
|
57
|
+
config = load_project_config(options["config"])
|
|
58
|
+
except ConfigError as exc:
|
|
59
|
+
raise CommandError(str(exc)) from exc
|
|
60
|
+
|
|
61
|
+
destination = config.openapi_source
|
|
62
|
+
previous_path = get_previous_schema_path(destination)
|
|
63
|
+
|
|
64
|
+
if options["dry_run"]:
|
|
65
|
+
self.stdout.write("--- DRY RUN: export_schema ---")
|
|
66
|
+
self.stdout.write(f" destination : {destination}")
|
|
67
|
+
if destination.exists():
|
|
68
|
+
self.stdout.write(
|
|
69
|
+
f" previous : {previous_path} (would archive current)"
|
|
70
|
+
)
|
|
71
|
+
self.stdout.write(" (no files written)")
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
# Generate the schema via drf-spectacular.
|
|
75
|
+
try:
|
|
76
|
+
from drf_spectacular.generators import (
|
|
77
|
+
SchemaGenerator, # type: ignore[import-untyped]
|
|
78
|
+
)
|
|
79
|
+
from drf_spectacular.renderers import ( # type: ignore[import-untyped]
|
|
80
|
+
OpenApiJsonRenderer,
|
|
81
|
+
OpenApiYamlRenderer,
|
|
82
|
+
)
|
|
83
|
+
except ImportError as exc: # pragma: no cover
|
|
84
|
+
raise CommandError(
|
|
85
|
+
"drf-spectacular is required for schema export. "
|
|
86
|
+
"Install it with: pip install drf-spectacular"
|
|
87
|
+
) from exc
|
|
88
|
+
|
|
89
|
+
generator = SchemaGenerator()
|
|
90
|
+
schema = generator.get_schema(request=None, public=True)
|
|
91
|
+
|
|
92
|
+
renderer = (
|
|
93
|
+
OpenApiYamlRenderer()
|
|
94
|
+
if options["format"] == "yaml"
|
|
95
|
+
else OpenApiJsonRenderer()
|
|
96
|
+
)
|
|
97
|
+
schema_bytes: bytes = renderer.render(schema, renderer_context={})
|
|
98
|
+
|
|
99
|
+
# Rotate current → previous before writing the new schema.
|
|
100
|
+
if destination.exists():
|
|
101
|
+
destination.replace(previous_path)
|
|
102
|
+
self.stdout.write(f"Previous schema archived to: {previous_path}")
|
|
103
|
+
|
|
104
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
destination.write_bytes(schema_bytes)
|
|
106
|
+
self.stdout.write(self.style.SUCCESS(f"Schema exported to: {destination}"))
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
from ._base import AngularBaseCommand
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Command(AngularBaseCommand):
|
|
7
|
+
angular_command_name = "ng_add"
|
|
8
|
+
help = "Run ng add for an Angular package in the configured workspace."
|
|
9
|
+
|
|
10
|
+
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
11
|
+
super().add_arguments(parser)
|
|
12
|
+
parser.add_argument(
|
|
13
|
+
"--package",
|
|
14
|
+
default=None,
|
|
15
|
+
help="Package to add (defaults to setting: ng_add_package).",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
def get_invocation_options(self, options: dict[str, object]) -> dict[str, object]:
|
|
19
|
+
return {"package": options.get("package")}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
from ._base import AngularBaseCommand
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Command(AngularBaseCommand):
|
|
7
|
+
angular_command_name = "ng_gen_app"
|
|
8
|
+
help = "Generate an Angular application inside the configured workspace."
|
|
9
|
+
|
|
10
|
+
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
11
|
+
super().add_arguments(parser)
|
|
12
|
+
parser.add_argument(
|
|
13
|
+
"--app-name",
|
|
14
|
+
default=None,
|
|
15
|
+
help=(
|
|
16
|
+
"Optional Angular application name. Defaults to "
|
|
17
|
+
"project.name from config."
|
|
18
|
+
),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def get_invocation_options(self, options: dict[str, object]) -> dict[str, object]:
|
|
22
|
+
return {"app_name": options.get("app_name")}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from collections.abc import Mapping, Sequence
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AngularCommandError(RuntimeError):
|
|
8
|
+
"""Raised when an Angular command cannot be resolved or executed."""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_is_win = os.name == "nt"
|
|
12
|
+
|
|
13
|
+
DEFAULT_ANGULAR_SETTINGS: dict[str, Any] = {
|
|
14
|
+
"config_path": "django-angular3.json",
|
|
15
|
+
"node_executable": "node.exe" if _is_win else "node",
|
|
16
|
+
"pnpm_executable": "pnpm.cmd" if _is_win else "pnpm",
|
|
17
|
+
"ng_executable": "ng.cmd" if _is_win else "ng",
|
|
18
|
+
"command_allowlist": ("ng_openapi_gen",),
|
|
19
|
+
"package_manager": "pnpm",
|
|
20
|
+
"build_configuration": "production",
|
|
21
|
+
"style": "scss",
|
|
22
|
+
"routing": True,
|
|
23
|
+
"ng_add_package": "angular-django2",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AngularSettings(SimpleNamespace):
|
|
28
|
+
"""Configuration values used to resolve and run Angular-related commands.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
config_path (str): Default project config path.
|
|
32
|
+
node_executable (str): Node executable name or path.
|
|
33
|
+
pnpm_executable (str): pnpm executable name or path.
|
|
34
|
+
ng_executable (str): Angular CLI executable name or path.
|
|
35
|
+
command_allowlist (tuple[str, ...]): Allowed resolved
|
|
36
|
+
django-angular3 command names.
|
|
37
|
+
package_manager (str): Angular package manager setting.
|
|
38
|
+
build_configuration (str): Angular build configuration name.
|
|
39
|
+
style (str): Default Angular stylesheet format.
|
|
40
|
+
routing (bool): Whether generated applications enable routing.
|
|
41
|
+
ng_add_package (str): The default package name or path to install for ng_add.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def load_angular_settings(
|
|
46
|
+
overrides: Mapping[str, object] | None = None,
|
|
47
|
+
) -> AngularSettings:
|
|
48
|
+
data = DEFAULT_ANGULAR_SETTINGS.copy()
|
|
49
|
+
data.update(_normalize_legacy_settings(_load_django_settings()))
|
|
50
|
+
if overrides:
|
|
51
|
+
data.update(_normalize_legacy_settings(overrides))
|
|
52
|
+
data["command_allowlist"] = _normalize_command_allowlist(
|
|
53
|
+
data.get("command_allowlist")
|
|
54
|
+
)
|
|
55
|
+
return AngularSettings(**data)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _load_django_settings() -> dict[str, object]:
|
|
59
|
+
try:
|
|
60
|
+
from django.conf import settings as django_settings
|
|
61
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
62
|
+
except ImportError:
|
|
63
|
+
return {}
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
if not getattr(django_settings, "configured", False):
|
|
67
|
+
return {}
|
|
68
|
+
|
|
69
|
+
value = getattr(django_settings, "DJANGO_ANGULAR3", {})
|
|
70
|
+
except ImproperlyConfigured:
|
|
71
|
+
return {}
|
|
72
|
+
|
|
73
|
+
if not isinstance(value, Mapping):
|
|
74
|
+
raise AngularCommandError(
|
|
75
|
+
"DJANGO_ANGULAR3 must be a dictionary-like mapping, got "
|
|
76
|
+
f"{type(value).__name__}."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return dict(value)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _normalize_legacy_settings(data: Mapping[str, object]) -> dict[str, object]:
|
|
83
|
+
normalized = dict(data)
|
|
84
|
+
if "pnpm_executable" not in normalized:
|
|
85
|
+
for legacy_key in ("npx_executable", "npm_executable"):
|
|
86
|
+
if legacy_key in normalized:
|
|
87
|
+
normalized["pnpm_executable"] = normalized[legacy_key]
|
|
88
|
+
break
|
|
89
|
+
|
|
90
|
+
normalized.pop("npm_executable", None)
|
|
91
|
+
normalized.pop("npx_executable", None)
|
|
92
|
+
return normalized
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _normalize_command_allowlist(value: object) -> tuple[str, ...]:
|
|
96
|
+
if isinstance(value, str):
|
|
97
|
+
commands = (value,)
|
|
98
|
+
elif isinstance(value, Sequence):
|
|
99
|
+
commands = tuple(value)
|
|
100
|
+
else:
|
|
101
|
+
raise AngularCommandError(
|
|
102
|
+
"DJANGO_ANGULAR3['command_allowlist'] must be a string "
|
|
103
|
+
"or a sequence of strings."
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
normalized_commands: list[str] = []
|
|
107
|
+
for command in commands:
|
|
108
|
+
if not isinstance(command, str):
|
|
109
|
+
raise AngularCommandError(
|
|
110
|
+
"DJANGO_ANGULAR3['command_allowlist'] must only contain strings."
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
normalized_command = command.strip().lower()
|
|
114
|
+
if not normalized_command:
|
|
115
|
+
raise AngularCommandError(
|
|
116
|
+
"DJANGO_ANGULAR3['command_allowlist'] cannot contain "
|
|
117
|
+
"empty command names."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if normalized_command not in normalized_commands:
|
|
121
|
+
normalized_commands.append(normalized_command)
|
|
122
|
+
|
|
123
|
+
return tuple(normalized_commands)
|
|
File without changes
|
|
File without changes
|
django_angular3/tools.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import platform
|
|
4
|
+
import tarfile
|
|
5
|
+
import urllib.request
|
|
6
|
+
import zipfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
# Base directory for storing downloaded tools relative to this package
|
|
10
|
+
PKG_DIR = Path(__file__).resolve().parent
|
|
11
|
+
BIN_DIR = PKG_DIR / ".bin"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_system_info():
|
|
15
|
+
"""Returns normalized OS and architecture strings."""
|
|
16
|
+
os_name = platform.system().lower()
|
|
17
|
+
if os_name == "darwin":
|
|
18
|
+
os_name = "macos"
|
|
19
|
+
|
|
20
|
+
arch = platform.machine().lower()
|
|
21
|
+
if arch in ["x86_64", "amd64"]:
|
|
22
|
+
arch = "amd64"
|
|
23
|
+
elif arch in ["arm64", "aarch64"]:
|
|
24
|
+
arch = "arm64"
|
|
25
|
+
elif arch in ["i386", "i686", "x86"]:
|
|
26
|
+
arch = "386"
|
|
27
|
+
|
|
28
|
+
return os_name, arch
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_latest_oasdiff_release():
|
|
32
|
+
"""Fetches the latest release info from oasdiff GitHub repository."""
|
|
33
|
+
url = "https://api.github.com/repos/Tufin/oasdiff/releases/latest"
|
|
34
|
+
req = urllib.request.Request(url, headers={"User-Agent": "django-angular3"})
|
|
35
|
+
try:
|
|
36
|
+
with urllib.request.urlopen(req) as response:
|
|
37
|
+
data = json.loads(response.read().decode("utf-8"))
|
|
38
|
+
return data
|
|
39
|
+
except Exception as e:
|
|
40
|
+
raise RuntimeError(f"Failed to fetch latest oasdiff version: {e}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_download_url(release_data, os_name, arch):
|
|
44
|
+
"""Finds the correct asset URL for the current OS and architecture."""
|
|
45
|
+
# oasdiff release naming pattern: oasdiff_<version>_<os>_<arch>.tar.gz/zip
|
|
46
|
+
# e.g., oasdiff_1.21.3_linux_amd64.tar.gz
|
|
47
|
+
# e.g., oasdiff_1.21.3_windows_amd64.zip
|
|
48
|
+
|
|
49
|
+
# Map our normalized OS name to oasdiff's naming conventions
|
|
50
|
+
# sometimes they use darwin, sometimes macos.
|
|
51
|
+
search_os = "darwin" if os_name == "macos" else os_name
|
|
52
|
+
|
|
53
|
+
# In recent versions, windows is windows, linux is linux.
|
|
54
|
+
|
|
55
|
+
for asset in release_data.get("assets", []):
|
|
56
|
+
name = asset["name"].lower()
|
|
57
|
+
if search_os in name and arch in name:
|
|
58
|
+
if name.endswith(".tar.gz") or name.endswith(".zip"):
|
|
59
|
+
return asset["browser_download_url"], asset["name"]
|
|
60
|
+
|
|
61
|
+
raise RuntimeError(
|
|
62
|
+
f"Could not find a suitable oasdiff binary for {os_name} {arch}."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def extract_archive(archive_path, extract_to):
|
|
67
|
+
"""Extracts a .zip or .tar.gz archive."""
|
|
68
|
+
if archive_path.name.endswith(".zip"):
|
|
69
|
+
with zipfile.ZipFile(archive_path, "r") as zip_ref:
|
|
70
|
+
zip_ref.extractall(extract_to)
|
|
71
|
+
elif archive_path.name.endswith(".tar.gz"):
|
|
72
|
+
with tarfile.open(archive_path, "r:gz") as tar_ref:
|
|
73
|
+
if hasattr(tarfile, "data_filter"):
|
|
74
|
+
tar_ref.extractall(extract_to, filter="data")
|
|
75
|
+
else:
|
|
76
|
+
tar_ref.extractall(extract_to)
|
|
77
|
+
else:
|
|
78
|
+
raise ValueError(f"Unsupported archive format: {archive_path.name}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def ensure_oasdiff():
|
|
82
|
+
"""
|
|
83
|
+
Ensures oasdiff is installed and available.
|
|
84
|
+
Returns the absolute path to the oasdiff executable.
|
|
85
|
+
"""
|
|
86
|
+
BIN_DIR.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
|
|
88
|
+
exe_name = "oasdiff.exe" if platform.system().lower() == "windows" else "oasdiff"
|
|
89
|
+
oasdiff_path = BIN_DIR / exe_name
|
|
90
|
+
|
|
91
|
+
if oasdiff_path.exists():
|
|
92
|
+
# Check if it's executable
|
|
93
|
+
if not os.access(oasdiff_path, os.X_OK):
|
|
94
|
+
oasdiff_path.chmod(0o755)
|
|
95
|
+
return str(oasdiff_path)
|
|
96
|
+
|
|
97
|
+
print(f"oasdiff not found. Downloading to {BIN_DIR}...")
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
os_name, arch = get_system_info()
|
|
101
|
+
release_data = get_latest_oasdiff_release()
|
|
102
|
+
url, asset_name = get_download_url(release_data, os_name, arch)
|
|
103
|
+
|
|
104
|
+
archive_path = BIN_DIR / asset_name
|
|
105
|
+
|
|
106
|
+
print(f"Downloading from {url}...")
|
|
107
|
+
urllib.request.urlretrieve(url, archive_path)
|
|
108
|
+
|
|
109
|
+
print("Extracting...")
|
|
110
|
+
extract_archive(archive_path, BIN_DIR)
|
|
111
|
+
|
|
112
|
+
# Clean up archive
|
|
113
|
+
archive_path.unlink()
|
|
114
|
+
|
|
115
|
+
# Verify it was extracted properly
|
|
116
|
+
if not oasdiff_path.exists():
|
|
117
|
+
raise RuntimeError(
|
|
118
|
+
f"Extraction completed, but {exe_name} was not found in {BIN_DIR}."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if os_name != "windows":
|
|
122
|
+
oasdiff_path.chmod(0o755)
|
|
123
|
+
|
|
124
|
+
print("oasdiff downloaded and ready.")
|
|
125
|
+
return str(oasdiff_path)
|
|
126
|
+
|
|
127
|
+
except Exception as e:
|
|
128
|
+
raise RuntimeError(f"Failed to install oasdiff: {e}")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
# Test the downloader
|
|
133
|
+
path = ensure_oasdiff()
|
|
134
|
+
print(f"oasdiff is located at: {path}")
|
django_angular3/urls.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .config import ProjectConfig
|
|
7
|
+
from .documents import DocumentError, load_document
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ValidationError(ValueError):
|
|
11
|
+
"""Raised when validation cannot continue."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def validate_openapi_document(document: Any) -> list[str]:
|
|
15
|
+
errors: list[str] = []
|
|
16
|
+
if not isinstance(document, dict):
|
|
17
|
+
return ["OpenAPI document must be a mapping."]
|
|
18
|
+
|
|
19
|
+
if not any(key in document for key in ("openapi", "swagger")):
|
|
20
|
+
errors.append("OpenAPI document must declare either 'openapi' or 'swagger'.")
|
|
21
|
+
|
|
22
|
+
paths = document.get("paths")
|
|
23
|
+
if not isinstance(paths, dict):
|
|
24
|
+
errors.append("OpenAPI document must contain a 'paths' mapping.")
|
|
25
|
+
return errors
|
|
26
|
+
|
|
27
|
+
if not paths:
|
|
28
|
+
errors.append("OpenAPI document must define at least one path.")
|
|
29
|
+
return errors
|
|
30
|
+
|
|
31
|
+
allowed_methods = {
|
|
32
|
+
"get",
|
|
33
|
+
"put",
|
|
34
|
+
"post",
|
|
35
|
+
"delete",
|
|
36
|
+
"options",
|
|
37
|
+
"head",
|
|
38
|
+
"patch",
|
|
39
|
+
"trace",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for path_name, path_item in paths.items():
|
|
43
|
+
if not isinstance(path_name, str) or not path_name.startswith("/"):
|
|
44
|
+
errors.append(
|
|
45
|
+
f"OpenAPI path '{path_name}' must be a string starting with '/'."
|
|
46
|
+
)
|
|
47
|
+
continue
|
|
48
|
+
if not isinstance(path_item, dict):
|
|
49
|
+
errors.append(f"OpenAPI path '{path_name}' must map to an object.")
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
operations = [name for name in path_item if name in allowed_methods]
|
|
53
|
+
if not operations:
|
|
54
|
+
errors.append(
|
|
55
|
+
f"OpenAPI path '{path_name}' must define at least one HTTP operation."
|
|
56
|
+
)
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
for operation_name in operations:
|
|
60
|
+
operation = path_item[operation_name]
|
|
61
|
+
if not isinstance(operation, dict):
|
|
62
|
+
errors.append(
|
|
63
|
+
f"Operation '{operation_name}' on path "
|
|
64
|
+
f"'{path_name}' must be an object."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return errors
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def validate_ui_document(document: Any) -> list[str]:
|
|
71
|
+
errors: list[str] = []
|
|
72
|
+
if not isinstance(document, dict):
|
|
73
|
+
return ["UI definition document must be a mapping."]
|
|
74
|
+
|
|
75
|
+
pages = document.get("pages", [])
|
|
76
|
+
forms = document.get("forms", [])
|
|
77
|
+
|
|
78
|
+
if not isinstance(pages, list):
|
|
79
|
+
errors.append("'pages' must be a list when provided.")
|
|
80
|
+
else:
|
|
81
|
+
for index, page in enumerate(pages):
|
|
82
|
+
if not isinstance(page, dict):
|
|
83
|
+
errors.append(f"pages[{index}] must be an object.")
|
|
84
|
+
continue
|
|
85
|
+
route = page.get("route")
|
|
86
|
+
kind = page.get("kind")
|
|
87
|
+
if not isinstance(route, str) or not route.startswith("/"):
|
|
88
|
+
errors.append(
|
|
89
|
+
f"pages[{index}].route must be a string starting with '/'."
|
|
90
|
+
)
|
|
91
|
+
if not isinstance(kind, str) or not kind.strip():
|
|
92
|
+
errors.append(f"pages[{index}].kind must be a non-empty string.")
|
|
93
|
+
|
|
94
|
+
if not isinstance(forms, list):
|
|
95
|
+
errors.append("'forms' must be a list when provided.")
|
|
96
|
+
else:
|
|
97
|
+
for index, form in enumerate(forms):
|
|
98
|
+
if not isinstance(form, dict):
|
|
99
|
+
errors.append(f"forms[{index}] must be an object.")
|
|
100
|
+
continue
|
|
101
|
+
form_id = form.get("id")
|
|
102
|
+
mode = form.get("mode")
|
|
103
|
+
submit = form.get("submit")
|
|
104
|
+
if not isinstance(form_id, str) or not form_id.strip():
|
|
105
|
+
errors.append(f"forms[{index}].id must be a non-empty string.")
|
|
106
|
+
if not isinstance(mode, str) or not mode.strip():
|
|
107
|
+
errors.append(f"forms[{index}].mode must be a non-empty string.")
|
|
108
|
+
if submit is not None:
|
|
109
|
+
if not isinstance(submit, dict):
|
|
110
|
+
errors.append(
|
|
111
|
+
f"forms[{index}].submit must be an object when provided."
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
action = submit.get("action")
|
|
115
|
+
if not isinstance(action, str) or not action.strip():
|
|
116
|
+
errors.append(
|
|
117
|
+
f"forms[{index}].submit.action must be a non-empty string."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return errors
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def validate_openapi_file(path: str | Path) -> list[str]:
|
|
124
|
+
try:
|
|
125
|
+
document = load_document(path)
|
|
126
|
+
except DocumentError as exc:
|
|
127
|
+
return [str(exc)]
|
|
128
|
+
return validate_openapi_document(document)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def validate_ui_file(path: str | Path) -> list[str]:
|
|
132
|
+
try:
|
|
133
|
+
document = load_document(path)
|
|
134
|
+
except DocumentError as exc:
|
|
135
|
+
return [str(exc)]
|
|
136
|
+
return validate_ui_document(document)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def validate_project_config(config: ProjectConfig) -> list[str]:
|
|
140
|
+
errors: list[str] = []
|
|
141
|
+
|
|
142
|
+
if not config.openapi_source.exists():
|
|
143
|
+
errors.append(f"OpenAPI source does not exist: {config.openapi_source}")
|
|
144
|
+
else:
|
|
145
|
+
errors.extend(validate_openapi_file(config.openapi_source))
|
|
146
|
+
|
|
147
|
+
if not config.ui_source.exists():
|
|
148
|
+
errors.append(f"UI source does not exist: {config.ui_source}")
|
|
149
|
+
else:
|
|
150
|
+
errors.extend(validate_ui_file(config.ui_source))
|
|
151
|
+
|
|
152
|
+
if config.angular_output.exists() and not config.angular_output.is_dir():
|
|
153
|
+
errors.append(
|
|
154
|
+
"Angular output path must be a directory when it exists: "
|
|
155
|
+
f"{config.angular_output}"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if config.openapi_generator_config and not config.openapi_generator_config.exists():
|
|
159
|
+
errors.append(
|
|
160
|
+
"OpenAPI Generator config does not exist: "
|
|
161
|
+
f"{config.openapi_generator_config}"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if config.ng_openapi_gen_config and not config.ng_openapi_gen_config.exists():
|
|
165
|
+
errors.append(
|
|
166
|
+
f"ng-openapi-gen config does not exist: {config.ng_openapi_gen_config}"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return errors
|
django_angular3/views.py
ADDED