ndsdk-cli 1.0.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.
Files changed (56) hide show
  1. ndsdk_cli-1.0.0/MANIFEST.in +6 -0
  2. ndsdk_cli-1.0.0/PKG-INFO +11 -0
  3. ndsdk_cli-1.0.0/ndsdk/__init__.py +3 -0
  4. ndsdk_cli-1.0.0/ndsdk/config_normalizer.py +306 -0
  5. ndsdk_cli-1.0.0/ndsdk/generators/__init__.py +8 -0
  6. ndsdk_cli-1.0.0/ndsdk/generators/base.py +93 -0
  7. ndsdk_cli-1.0.0/ndsdk/generators/engine.py +715 -0
  8. ndsdk_cli-1.0.0/ndsdk/main.py +536 -0
  9. ndsdk_cli-1.0.0/ndsdk/naming.py +112 -0
  10. ndsdk_cli-1.0.0/ndsdk/packages/client_provider/Program.fragment.cs.j2 +1 -0
  11. ndsdk_cli-1.0.0/ndsdk/packages/client_provider/appsettings.fragment.json.j2 +14 -0
  12. ndsdk_cli-1.0.0/ndsdk/packages/client_provider/package.yaml +13 -0
  13. ndsdk_cli-1.0.0/ndsdk/packages/db_provider/Program.fragment.cs.j2 +3 -0
  14. ndsdk_cli-1.0.0/ndsdk/packages/db_provider/appsettings.fragment.json.j2 +30 -0
  15. ndsdk_cli-1.0.0/ndsdk/packages/db_provider/package.yaml +123 -0
  16. ndsdk_cli-1.0.0/ndsdk/packages/exception_handling/Program.app.fragment.cs.j2 +1 -0
  17. ndsdk_cli-1.0.0/ndsdk/packages/exception_handling/Program.fragment.cs.j2 +1 -0
  18. ndsdk_cli-1.0.0/ndsdk/packages/exception_handling/package.yaml +7 -0
  19. ndsdk_cli-1.0.0/ndsdk/packages/file_storage_azure/Program.fragment.cs.j2 +1 -0
  20. ndsdk_cli-1.0.0/ndsdk/packages/file_storage_azure/appsettings.fragment.json.j2 +6 -0
  21. ndsdk_cli-1.0.0/ndsdk/packages/file_storage_azure/package.yaml +9 -0
  22. ndsdk_cli-1.0.0/ndsdk/packages/observability/Program.fragment.cs.j2 +2 -0
  23. ndsdk_cli-1.0.0/ndsdk/packages/observability/appsettings.fragment.json.j2 +39 -0
  24. ndsdk_cli-1.0.0/ndsdk/packages/observability/package.yaml +16 -0
  25. ndsdk_cli-1.0.0/ndsdk/registry.py +192 -0
  26. ndsdk_cli-1.0.0/ndsdk/templates/_common/BO.cs.j2 +10 -0
  27. ndsdk_cli-1.0.0/ndsdk/templates/_common/Controller.cs.j2 +131 -0
  28. ndsdk_cli-1.0.0/ndsdk/templates/_common/DbProviderBase.cs.j2 +26 -0
  29. ndsdk_cli-1.0.0/ndsdk/templates/_common/Dockerfile.j2 +15 -0
  30. ndsdk_cli-1.0.0/ndsdk/templates/_common/Dto.cs.j2 +16 -0
  31. ndsdk_cli-1.0.0/ndsdk/templates/_common/Entity.cs.j2 +16 -0
  32. ndsdk_cli-1.0.0/ndsdk/templates/_common/IService.cs.j2 +25 -0
  33. ndsdk_cli-1.0.0/ndsdk/templates/_common/Model.cs.j2 +16 -0
  34. ndsdk_cli-1.0.0/ndsdk/templates/_common/Provider.cs.j2 +151 -0
  35. ndsdk_cli-1.0.0/ndsdk/templates/_common/ProviderBase.cs.j2 +10 -0
  36. ndsdk_cli-1.0.0/ndsdk/templates/_common/Service.cs.j2 +49 -0
  37. ndsdk_cli-1.0.0/ndsdk/templates/_common/ServiceManager.cs.j2 +28 -0
  38. ndsdk_cli-1.0.0/ndsdk/templates/_common/Solution.sln.j2 +22 -0
  39. ndsdk_cli-1.0.0/ndsdk/templates/_common/_csproj_refs.inc.j2 +14 -0
  40. ndsdk_cli-1.0.0/ndsdk/templates/_common/launchSettings.json.j2 +17 -0
  41. ndsdk_cli-1.0.0/ndsdk/templates/microservice/clean/Api.csproj.j2 +12 -0
  42. ndsdk_cli-1.0.0/ndsdk/templates/microservice/clean/ClassLib.csproj.j2 +11 -0
  43. ndsdk_cli-1.0.0/ndsdk/templates/microservice/clean/Program.cs.j2 +35 -0
  44. ndsdk_cli-1.0.0/ndsdk/templates/microservice/clean/layout.yaml +64 -0
  45. ndsdk_cli-1.0.0/ndsdk/templates/microservice/layered/Program.cs.j2 +61 -0
  46. ndsdk_cli-1.0.0/ndsdk/templates/microservice/layered/Project.csproj.j2 +12 -0
  47. ndsdk_cli-1.0.0/ndsdk/templates/microservice/layered/layout.yaml +41 -0
  48. ndsdk_cli-1.0.0/ndsdk/validators.py +213 -0
  49. ndsdk_cli-1.0.0/ndsdk_cli.egg-info/PKG-INFO +11 -0
  50. ndsdk_cli-1.0.0/ndsdk_cli.egg-info/SOURCES.txt +54 -0
  51. ndsdk_cli-1.0.0/ndsdk_cli.egg-info/dependency_links.txt +1 -0
  52. ndsdk_cli-1.0.0/ndsdk_cli.egg-info/entry_points.txt +2 -0
  53. ndsdk_cli-1.0.0/ndsdk_cli.egg-info/requires.txt +3 -0
  54. ndsdk_cli-1.0.0/ndsdk_cli.egg-info/top_level.txt +1 -0
  55. ndsdk_cli-1.0.0/pyproject.toml +35 -0
  56. ndsdk_cli-1.0.0/setup.cfg +4 -0
@@ -0,0 +1,6 @@
1
+ recursive-include ndsdk/templates *.j2
2
+ recursive-include ndsdk/packages *.j2 *.yaml
3
+ recursive-include ndsdk/templates *.j2
4
+ include ndsdk/*.yaml
5
+ include ndsdk/*.txt
6
+ global-exclude __pycache__ *.py[cod] .DS_Store
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: ndsdk-cli
3
+ Version: 1.0.0
4
+ Summary: ND-SDK metadata-driven Template generator
5
+ Author-email: Hari Hara Sudhan P <harihara.sudhan@novacisinnovations.com>
6
+ License: Proprietary
7
+ Keywords: code-generation,csharp,microservice,scaffolding,yaml
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: click>=8.1
10
+ Requires-Dist: PyYAML>=6.0
11
+ Requires-Dist: Jinja2>=3.1
@@ -0,0 +1,3 @@
1
+ """ND-SDK CLI — metadata-driven scaffolding generator for ASP.NET C# projects."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,306 @@
1
+ """Compatibility normalisation for ND-SDK config files.
2
+
3
+ The generator internally works with one canonical provider shape. Public config
4
+ should describe every external capability the same way:
5
+
6
+ * ``integrations`` describe external resources (database, storage, queue, API, ...)
7
+ * ``providers`` describe generated adapter classes
8
+ * ``providers[].methods`` describe provider operations using a common method shape
9
+ * ``packages`` describe capabilities that must be injected into the generated app
10
+
11
+ Older database-first keys are still accepted for compatibility, but are folded
12
+ into the same common shape instead of creating a separate public database model.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import copy
18
+ from typing import Any
19
+
20
+
21
+ _RELATIONAL_PROVIDERS = {
22
+ "sqlserver",
23
+ "sql_server",
24
+ "mssql",
25
+ "postgres",
26
+ "postgresql",
27
+ "mysql",
28
+ "oracle",
29
+ "snowflake",
30
+ }
31
+ _NON_RELATIONAL_PROVIDERS = {
32
+ "cassandra",
33
+ "dynamodb",
34
+ "mongodb",
35
+ "cosmosdb",
36
+ "cosmos",
37
+ "redis",
38
+ "elasticsearch",
39
+ }
40
+ _DB_METHOD_KEYS = {
41
+ "stored_procedure",
42
+ "target",
43
+ "table",
44
+ "inputs",
45
+ "parameters",
46
+ "cursors",
47
+ }
48
+ _METHOD_ALIAS_KEYS = {"database", "entity"}
49
+
50
+
51
+ def normalize_config(config: dict[str, Any] | None) -> dict[str, Any]:
52
+ """Return a deep-copied canonical config.
53
+
54
+ Supported compatibility aliases:
55
+ * ``defaults.observability`` -> ``defaults.packages.observability``
56
+ * old ``framework_packages`` blocks -> public ``packages`` blocks
57
+ * old ``service.database`` -> synthetic database integration only when no
58
+ equivalent integration exists
59
+ * ``endpoints[].request_dto`` / ``response_dto`` are mirrored into the older
60
+ request/response model fields only when those fields are absent
61
+ * ``providers[].methods[].entity`` -> ``input_entity``
62
+ * ``providers[].methods[].database`` and top-level DB method fields ->
63
+ ``providers[].methods[].settings``
64
+ """
65
+
66
+ if not isinstance(config, dict):
67
+ return {}
68
+ out = copy.deepcopy(config or {})
69
+ _normalize_defaults(out)
70
+ for svc in out.get("services", []) or []:
71
+ _normalize_service(svc)
72
+ return out
73
+
74
+
75
+ def _normalize_defaults(config: dict[str, Any]) -> None:
76
+ defaults = config.setdefault("defaults", {})
77
+
78
+ packages = _merge_package_blocks(defaults.get("framework_packages"), defaults.get("packages"))
79
+ defaults.pop("framework_packages", None)
80
+ defaults["packages"] = packages
81
+
82
+ observability = defaults.get("observability")
83
+ if isinstance(observability, dict):
84
+ current = packages.get("observability")
85
+ if isinstance(current, dict):
86
+ packages["observability"] = {**observability, **current}
87
+ else:
88
+ packages["observability"] = {"enabled": observability.get("enabled", True), **observability}
89
+ elif observability is True:
90
+ packages.setdefault("observability", {"enabled": True})
91
+ elif observability is False:
92
+ packages.setdefault("observability", {"enabled": False})
93
+
94
+
95
+ def _merge_package_blocks(*blocks: Any) -> dict[str, Any]:
96
+ """Normalize package declarations to one ``{key: settings}`` map.
97
+
98
+ Supported public shapes:
99
+
100
+ ``packages: { observability: { enabled: true } }``
101
+ ``packages: [ { name: observability, enabled: true } ]``
102
+ ``packages: [ observability ]``
103
+ """
104
+ merged: dict[str, Any] = {}
105
+ for block in blocks:
106
+ if not block:
107
+ continue
108
+ if isinstance(block, dict):
109
+ merged.update(block)
110
+ continue
111
+ if isinstance(block, list):
112
+ for item in block:
113
+ if isinstance(item, str):
114
+ merged[item] = {"enabled": True}
115
+ elif isinstance(item, dict):
116
+ key = item.get("name") or item.get("key") or item.get("package")
117
+ if key:
118
+ settings = {k: v for k, v in item.items() if k not in {"name", "key", "package"}}
119
+ merged[str(key)] = settings or {"enabled": True}
120
+ return merged
121
+
122
+
123
+ def _normalize_service(svc: dict[str, Any]) -> None:
124
+ svc["packages"] = _merge_package_blocks(svc.get("framework_packages"), svc.get("packages"))
125
+ svc.pop("framework_packages", None)
126
+
127
+ _normalize_integrations(svc)
128
+ _normalize_legacy_database_block(svc)
129
+
130
+ integrations = svc.get("integrations", []) or []
131
+ integration_by_name = {
132
+ i.get("name"): i for i in integrations if isinstance(i, dict) and i.get("name")
133
+ }
134
+
135
+ for ep in svc.get("endpoints", []) or []:
136
+ if "request_dto" in ep and "request_model" not in ep:
137
+ ep["request_model"] = ep["request_dto"]
138
+ if "response_dto" in ep and "response_model" not in ep:
139
+ ep["response_model"] = ep["response_dto"]
140
+ ep.setdefault(
141
+ "consumes",
142
+ "application/json" if str(ep.get("http_method", "GET")).upper() in {"POST", "PUT", "PATCH"} else "",
143
+ )
144
+
145
+ for prov in svc.get("providers", []) or []:
146
+ integration_name = prov.get("integration")
147
+ integration = integration_by_name.get(integration_name)
148
+ provider_kind = _provider_kind(prov, integration)
149
+ prov["kind"] = provider_kind
150
+
151
+ # Backward compatibility: old `db: true` providers become database kind.
152
+ if prov.get("db") is True:
153
+ prov["kind"] = "database"
154
+ provider_kind = "database"
155
+ prov.pop("db", None)
156
+
157
+ if not prov.get("integration") and provider_kind == "database":
158
+ db_integration = next(
159
+ (i for i in integrations if isinstance(i, dict) and str(i.get("kind", "")).lower() == "database"),
160
+ None,
161
+ )
162
+ if db_integration and db_integration.get("name"):
163
+ prov["integration"] = db_integration["name"]
164
+ integration_name = prov["integration"]
165
+ integration = db_integration
166
+
167
+ for method in prov.get("methods", []) or []:
168
+ _normalize_provider_method(method, provider_kind, integration_name)
169
+
170
+
171
+ def _normalize_integrations(svc: dict[str, Any]) -> None:
172
+ integrations = svc.get("integrations")
173
+ if not isinstance(integrations, list):
174
+ integrations = []
175
+ svc["integrations"] = integrations
176
+
177
+ for integration in integrations:
178
+ if not isinstance(integration, dict):
179
+ continue
180
+ integration["kind"] = str(integration.get("kind") or "custom").lower()
181
+ settings = integration.get("settings") if isinstance(integration.get("settings"), dict) else {}
182
+
183
+ # Keep all provider-specific details under the common `settings` object.
184
+ for key in ("database_type", "connection_string_name", "container_name", "queue_name", "base_url_config_key"):
185
+ if key in integration and key not in settings:
186
+ settings[key] = integration.pop(key)
187
+ if "type" in integration and integration["kind"] == "database" and "database_type" not in settings:
188
+ settings["database_type"] = integration.pop("type")
189
+
190
+ provider = integration.get("provider") or integration.get("store")
191
+ if provider and "provider" not in integration:
192
+ integration["provider"] = provider
193
+ integration.pop("store", None)
194
+
195
+ if integration["kind"] == "database" and "database_type" not in settings:
196
+ settings["database_type"] = _infer_db_type(str(provider or ""))
197
+
198
+ integration["settings"] = settings
199
+
200
+
201
+ def _normalize_legacy_database_block(svc: dict[str, Any]) -> None:
202
+ """Fold an old service.database block into a database integration.
203
+
204
+ The normalized public copy should use integrations/providers/settings. The
205
+ legacy block is removed after an equivalent integration is present.
206
+ """
207
+ db = svc.get("database")
208
+ if not isinstance(db, dict):
209
+ return
210
+
211
+ integrations = svc.setdefault("integrations", [])
212
+ has_database_integration = any(
213
+ isinstance(i, dict) and str(i.get("kind", "")).lower() == "database"
214
+ for i in integrations
215
+ )
216
+ if not has_database_integration:
217
+ provider = db.get("provider") or db.get("store")
218
+ integrations.append({
219
+ "name": db.get("integration") or "database",
220
+ "kind": "database",
221
+ "provider": provider,
222
+ "settings": {
223
+ "database_type": db.get("type") or _infer_db_type(str(provider or "")),
224
+ **({"connection_string_name": db["connection_string_name"]} if db.get("connection_string_name") else {}),
225
+ },
226
+ })
227
+ svc.pop("database", None)
228
+ _normalize_integrations(svc)
229
+
230
+
231
+ def _provider_kind(prov: dict[str, Any], integration: dict[str, Any] | None) -> str:
232
+ if prov.get("kind"):
233
+ return str(prov["kind"]).lower()
234
+ if integration and integration.get("kind"):
235
+ return str(integration["kind"]).lower()
236
+ if prov.get("db") is True or isinstance(prov.get("database"), dict):
237
+ return "database"
238
+
239
+ # Compatibility: older configs expressed database providers by putting
240
+ # database-shaped method fields directly on provider methods. Treat that as
241
+ # database, then normalize those fields into method.settings.
242
+ for method in prov.get("methods", []) or []:
243
+ if not isinstance(method, dict):
244
+ continue
245
+ if isinstance(method.get("database"), dict):
246
+ return "database"
247
+ if method.get("kind") in {"relational", "non_relational"}:
248
+ return "database"
249
+ if any(key in method for key in _DB_METHOD_KEYS):
250
+ return "database"
251
+ return "custom"
252
+
253
+
254
+ def _normalize_provider_method(method: dict[str, Any], provider_kind: str, integration_name: str | None) -> None:
255
+ if method.get("entity") and not method.get("input_entity"):
256
+ method["input_entity"] = method["entity"]
257
+ method.pop("entity", None)
258
+
259
+ settings: dict[str, Any] = {}
260
+ if isinstance(method.get("settings"), dict):
261
+ settings.update(method["settings"])
262
+
263
+ # Old database method block is now just provider-specific method settings.
264
+ db_block = method.get("database") if isinstance(method.get("database"), dict) else {}
265
+ settings = {**db_block, **settings}
266
+ method.pop("database", None)
267
+
268
+ for key in list(_DB_METHOD_KEYS):
269
+ if key in method and key not in settings:
270
+ settings[key] = method.pop(key)
271
+
272
+ op = str(method.get("operation", "") or "").strip()
273
+ if op:
274
+ method["operation"] = _normalize_operation(op)
275
+ elif provider_kind == "database":
276
+ if settings.get("stored_procedure"):
277
+ method["operation"] = "stored_procedure"
278
+ else:
279
+ method["operation"] = "query"
280
+
281
+ if provider_kind == "database" and method.get("kind") in {"relational", "non_relational"}:
282
+ settings.setdefault("method_type", method.pop("kind"))
283
+
284
+ method["settings"] = settings
285
+ method.setdefault("integration", integration_name)
286
+
287
+ for alias in _METHOD_ALIAS_KEYS:
288
+ method.pop(alias, None)
289
+
290
+
291
+ def _normalize_operation(op: str) -> str:
292
+ normalized = op.strip().replace(" ", "_").replace("-", "_").lower()
293
+ if normalized in {"storedprocedure", "stored_proc", "stored_procedure", "sp"}:
294
+ return "stored_procedure"
295
+ if normalized in {"query", "getresults", "get_results"}:
296
+ return "query"
297
+ return normalized
298
+
299
+
300
+ def _infer_db_type(provider: str) -> str:
301
+ p = provider.strip().lower()
302
+ if p in _RELATIONAL_PROVIDERS:
303
+ return "relational"
304
+ if p in _NON_RELATIONAL_PROVIDERS:
305
+ return "non_relational"
306
+ return "non_relational"
@@ -0,0 +1,8 @@
1
+ """Generators package — manifest-driven engine + variant discovery."""
2
+
3
+ from .engine import available_variants, get_generator, resolve_variant # noqa: F401
4
+
5
+
6
+ def available_architectures() -> list[str]:
7
+ """Back-compat: 'type/style' strings."""
8
+ return [f"{t}/{s}" for t, s in available_variants()]
@@ -0,0 +1,93 @@
1
+ """Low-level generator helpers: Jinja env, file IO, namespace + nuget resolution.
2
+
3
+ Shared by the manifest engine. No architecture-specific logic lives here.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from jinja2 import ChoiceLoader, Environment, FileSystemLoader, StrictUndefined
12
+
13
+ from .. import naming
14
+
15
+ TEMPLATES_DIR = Path(__file__).parent.parent / "templates"
16
+ COMMON_DIR = TEMPLATES_DIR / "_common"
17
+
18
+
19
+ def make_env(variant_dir: Path) -> Environment:
20
+ """Jinja env that searches the variant folder first, then _common."""
21
+ env = Environment(
22
+ loader=ChoiceLoader([
23
+ FileSystemLoader(str(variant_dir)),
24
+ FileSystemLoader(str(COMMON_DIR)),
25
+ ]),
26
+ trim_blocks=True,
27
+ lstrip_blocks=True,
28
+ keep_trailing_newline=True,
29
+ undefined=StrictUndefined,
30
+ )
31
+ naming.register_filters(env)
32
+ return env
33
+
34
+
35
+ def write_file(path: Path, content: str, *, owner: str = "framework") -> tuple[Path, bool]:
36
+ """Write a file. Returns (path, written).
37
+
38
+ ``owner='user'`` files are written once and never overwritten, so business
39
+ logic survives regeneration after a framework/controller change.
40
+ """
41
+ if owner == "user" and path.exists():
42
+ return path, False
43
+ path.parent.mkdir(parents=True, exist_ok=True)
44
+ if not content.endswith("\n"):
45
+ content += "\n"
46
+ path.write_text(content, encoding="utf-8")
47
+ return path, True
48
+
49
+
50
+ def resolve_namespaces(ns_templates: dict[str, str], base_ns: str) -> dict[str, str]:
51
+ """Resolve a layout's namespace map (e.g. ``{ns}.Domain.Entities``) against a
52
+ service's base namespace."""
53
+ out = {"base": base_ns}
54
+ for key, tmpl in (ns_templates or {}).items():
55
+ out[key] = tmpl.format(ns=base_ns)
56
+ return out
57
+
58
+
59
+ def resolve_nuget_refs(
60
+ package_refs: list,
61
+ *,
62
+ config_versions: dict[str, str],
63
+ version_strategy: str = "floating",
64
+ ) -> list[dict]:
65
+ """De-duplicate NuGet refs and apply version precedence.
66
+
67
+ Precedence (highest first):
68
+ 1. config ``package_versions[name]`` override
69
+ 2. explicit/default version from package or layout YAML
70
+ 3. ``nuget_version_strategy`` fallback
71
+
72
+ This keeps package YAML versions as shipped defaults, not hardcoded locks.
73
+ """
74
+ resolved: dict[str, str] = {}
75
+ for ref in package_refs:
76
+ if isinstance(ref, str):
77
+ name, version = (ref.split("@", 1) + [""])[:2]
78
+ name, version = name.strip(), version.strip()
79
+ else:
80
+ name = str(ref.get("name"))
81
+ version = str(ref.get("version", "") or "")
82
+ if name in config_versions:
83
+ version = str(config_versions[name])
84
+ # later refs override earlier (service overrides defaults), while the
85
+ # config-level package_versions override remains authoritative.
86
+ resolved[name] = version
87
+
88
+ out = []
89
+ for name, version in resolved.items():
90
+ if not version:
91
+ version = "*" if version_strategy == "floating" else None
92
+ out.append({"name": name, "version": version})
93
+ return out