onyx-database 0.0.4__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.
- onyx_database/__init__.py +107 -0
- onyx_database/cli.py +285 -0
- onyx_database/codegen.py +166 -0
- onyx_database/config.py +230 -0
- onyx_database/errors.py +58 -0
- onyx_database/helpers/__init__.py +1 -0
- onyx_database/helpers/aggregates.py +54 -0
- onyx_database/helpers/conditions.py +110 -0
- onyx_database/helpers/sort.py +13 -0
- onyx_database/http.py +252 -0
- onyx_database/onyx.py +345 -0
- onyx_database/onyx_async.py +294 -0
- onyx_database/query_builder.py +329 -0
- onyx_database/query_builder_async.py +261 -0
- onyx_database/query_results.py +34 -0
- onyx_database/query_results_async.py +39 -0
- onyx_database/stream.py +112 -0
- onyx_database/types.py +33 -0
- onyx_database-0.0.4.dist-info/METADATA +727 -0
- onyx_database-0.0.4.dist-info/RECORD +23 -0
- onyx_database-0.0.4.dist-info/WHEEL +4 -0
- onyx_database-0.0.4.dist-info/entry_points.txt +2 -0
- onyx_database-0.0.4.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Public entrypoint for the Onyx Database Python SDK."""
|
|
2
|
+
|
|
3
|
+
from .errors import (
|
|
4
|
+
OnyxConfigError,
|
|
5
|
+
OnyxHTTPError,
|
|
6
|
+
OnyxUnauthorizedError,
|
|
7
|
+
OnyxNotFoundError,
|
|
8
|
+
OnyxRateLimitedError,
|
|
9
|
+
OnyxClientError,
|
|
10
|
+
OnyxServerError,
|
|
11
|
+
OnyxTimeoutError,
|
|
12
|
+
)
|
|
13
|
+
from .onyx import OnyxFacade, onyx
|
|
14
|
+
from .helpers.conditions import (
|
|
15
|
+
eq,
|
|
16
|
+
neq,
|
|
17
|
+
within,
|
|
18
|
+
not_within,
|
|
19
|
+
in_op,
|
|
20
|
+
not_in,
|
|
21
|
+
between,
|
|
22
|
+
gt,
|
|
23
|
+
gte,
|
|
24
|
+
lt,
|
|
25
|
+
lte,
|
|
26
|
+
like,
|
|
27
|
+
not_like,
|
|
28
|
+
contains,
|
|
29
|
+
not_contains,
|
|
30
|
+
starts_with,
|
|
31
|
+
not_starts_with,
|
|
32
|
+
matches,
|
|
33
|
+
not_matches,
|
|
34
|
+
is_null,
|
|
35
|
+
not_null,
|
|
36
|
+
)
|
|
37
|
+
from .helpers.sort import asc, desc
|
|
38
|
+
from .onyx_async import OnyxAsyncFacade, onyx_async
|
|
39
|
+
from .helpers.aggregates import (
|
|
40
|
+
avg,
|
|
41
|
+
sum,
|
|
42
|
+
count,
|
|
43
|
+
min,
|
|
44
|
+
max,
|
|
45
|
+
std,
|
|
46
|
+
variance,
|
|
47
|
+
median,
|
|
48
|
+
upper,
|
|
49
|
+
lower,
|
|
50
|
+
substring,
|
|
51
|
+
replace,
|
|
52
|
+
percentile,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"OnyxFacade",
|
|
57
|
+
"OnyxAsyncFacade",
|
|
58
|
+
"onyx",
|
|
59
|
+
"onyx_async",
|
|
60
|
+
"OnyxConfigError",
|
|
61
|
+
"OnyxHTTPError",
|
|
62
|
+
"OnyxUnauthorizedError",
|
|
63
|
+
"OnyxNotFoundError",
|
|
64
|
+
"OnyxRateLimitedError",
|
|
65
|
+
"OnyxClientError",
|
|
66
|
+
"OnyxServerError",
|
|
67
|
+
"OnyxTimeoutError",
|
|
68
|
+
# conditions
|
|
69
|
+
"eq",
|
|
70
|
+
"neq",
|
|
71
|
+
"within",
|
|
72
|
+
"not_within",
|
|
73
|
+
"in_op",
|
|
74
|
+
"not_in",
|
|
75
|
+
"between",
|
|
76
|
+
"gt",
|
|
77
|
+
"gte",
|
|
78
|
+
"lt",
|
|
79
|
+
"lte",
|
|
80
|
+
"like",
|
|
81
|
+
"not_like",
|
|
82
|
+
"contains",
|
|
83
|
+
"not_contains",
|
|
84
|
+
"starts_with",
|
|
85
|
+
"not_starts_with",
|
|
86
|
+
"matches",
|
|
87
|
+
"not_matches",
|
|
88
|
+
"is_null",
|
|
89
|
+
"not_null",
|
|
90
|
+
# sort
|
|
91
|
+
"asc",
|
|
92
|
+
"desc",
|
|
93
|
+
# aggregates
|
|
94
|
+
"avg",
|
|
95
|
+
"sum",
|
|
96
|
+
"count",
|
|
97
|
+
"min",
|
|
98
|
+
"max",
|
|
99
|
+
"std",
|
|
100
|
+
"variance",
|
|
101
|
+
"median",
|
|
102
|
+
"upper",
|
|
103
|
+
"lower",
|
|
104
|
+
"substring",
|
|
105
|
+
"replace",
|
|
106
|
+
"percentile",
|
|
107
|
+
]
|
onyx_database/cli.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""CLI entrypoint for `onyx-py` (schema + codegen)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict
|
|
11
|
+
|
|
12
|
+
from . import onyx
|
|
13
|
+
from .config import resolve_config, _candidate_paths, OnyxConfigError
|
|
14
|
+
from .codegen import generate_models
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _load_json(path: Path) -> Dict[str, Any]:
|
|
18
|
+
text = path.read_text(encoding="utf-8")
|
|
19
|
+
return json.loads(text)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _write_json(path: Path, data: Dict[str, Any]) -> None:
|
|
23
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
path.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _default_schema_path() -> Path:
|
|
28
|
+
candidates = [Path("schema/onyx.schema.json"), Path("onyx.schema.json")]
|
|
29
|
+
for c in candidates:
|
|
30
|
+
if c.exists():
|
|
31
|
+
return c
|
|
32
|
+
return candidates[0]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _detect_config_path() -> Path | None:
|
|
36
|
+
env_path = os.environ.get("ONYX_CONFIG_PATH")
|
|
37
|
+
if env_path:
|
|
38
|
+
p = Path(env_path)
|
|
39
|
+
if not p.is_absolute():
|
|
40
|
+
p = Path.cwd() / p
|
|
41
|
+
return p
|
|
42
|
+
for c in _candidate_paths(None):
|
|
43
|
+
if c.exists():
|
|
44
|
+
return c
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def handle_info(args: argparse.Namespace) -> int:
|
|
49
|
+
config_path = _detect_config_path()
|
|
50
|
+
schema_env = os.environ.get("ONYX_SCHEMA_PATH")
|
|
51
|
+
schema_path = Path(schema_env) if schema_env else _default_schema_path()
|
|
52
|
+
|
|
53
|
+
env_present = any(
|
|
54
|
+
os.environ.get(k)
|
|
55
|
+
for k in ["ONYX_DATABASE_ID", "ONYX_DATABASE_API_KEY", "ONYX_DATABASE_API_SECRET", "ONYX_DATABASE_BASE_URL"]
|
|
56
|
+
)
|
|
57
|
+
source_guess = "file" if (config_path and Path(config_path).exists()) else ("env" if env_present else "default")
|
|
58
|
+
|
|
59
|
+
def _mask(value: str | None) -> str | None:
|
|
60
|
+
if not value:
|
|
61
|
+
return None
|
|
62
|
+
if len(value) <= 4:
|
|
63
|
+
return "*" * len(value)
|
|
64
|
+
return f"{value[:2]}...{value[-2:]}"
|
|
65
|
+
|
|
66
|
+
info: Dict[str, Any] = {
|
|
67
|
+
"configPath": str(config_path) if config_path else None,
|
|
68
|
+
"configPathExists": bool(config_path and Path(config_path).exists()),
|
|
69
|
+
"schemaPath": str(schema_path),
|
|
70
|
+
"schemaPathExists": schema_path.exists(),
|
|
71
|
+
"source": source_guess,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
resolved = resolve_config()
|
|
76
|
+
info.update(
|
|
77
|
+
{
|
|
78
|
+
"baseUrl": resolved.base_url,
|
|
79
|
+
"databaseId": resolved.database_id,
|
|
80
|
+
"partition": resolved.partition,
|
|
81
|
+
"requestLoggingEnabled": resolved.request_logging_enabled,
|
|
82
|
+
"responseLoggingEnabled": resolved.response_logging_enabled,
|
|
83
|
+
"apiKeyMasked": _mask(resolved.api_key),
|
|
84
|
+
"apiSecretMasked": _mask(resolved.api_secret),
|
|
85
|
+
"connection": "ok",
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
except OnyxConfigError as exc:
|
|
89
|
+
info["configError"] = str(exc)
|
|
90
|
+
info["connection"] = f"error: {exc}"
|
|
91
|
+
|
|
92
|
+
if getattr(args, "json", False):
|
|
93
|
+
print(json.dumps(info, indent=2))
|
|
94
|
+
else:
|
|
95
|
+
if "configError" in info:
|
|
96
|
+
print(f"Config error: {info['configError']}")
|
|
97
|
+
else:
|
|
98
|
+
print(f"Database ID: {info.get('databaseId')} (source: {info.get('source')})")
|
|
99
|
+
print(f"Base URL : {info.get('baseUrl')} (source: {info.get('source')})")
|
|
100
|
+
print(f"API Key : {info.get('apiKeyMasked')} (source: {info.get('source')})")
|
|
101
|
+
print(f"API Secret : {info.get('apiSecretMasked')} (source: {info.get('source')})")
|
|
102
|
+
print(f"Config file: {info.get('configPath')}")
|
|
103
|
+
print(f"Connection : {info.get('connection')}")
|
|
104
|
+
return 0
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def handle_schema(args: argparse.Namespace) -> int:
|
|
108
|
+
action: str = args.action
|
|
109
|
+
try:
|
|
110
|
+
db = onyx.init(
|
|
111
|
+
request_timeout_seconds=args.timeout,
|
|
112
|
+
max_retries=args.max_retries,
|
|
113
|
+
retry_backoff_seconds=args.retry_backoff,
|
|
114
|
+
)
|
|
115
|
+
except Exception as exc:
|
|
116
|
+
print(f"Config error: {exc}")
|
|
117
|
+
return 1
|
|
118
|
+
|
|
119
|
+
if action == "get":
|
|
120
|
+
tables: list[str] = []
|
|
121
|
+
if args.tables:
|
|
122
|
+
tables = [t.strip() for t in args.tables.split(",") if t.strip()]
|
|
123
|
+
schema = db.get_schema(tables=tables if tables else None)
|
|
124
|
+
if args.print_only or args.tables:
|
|
125
|
+
print(json.dumps(schema, indent=2))
|
|
126
|
+
return 0
|
|
127
|
+
out_path = Path(args.out or _default_schema_path())
|
|
128
|
+
_write_json(out_path, schema)
|
|
129
|
+
print(f"schema written to {out_path}")
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
schema_path = Path(args.schema or _default_schema_path())
|
|
133
|
+
if not schema_path.exists():
|
|
134
|
+
raise FileNotFoundError(f"Schema file not found: {schema_path}")
|
|
135
|
+
local_schema = _load_json(schema_path)
|
|
136
|
+
|
|
137
|
+
if action == "publish":
|
|
138
|
+
res = db.update_schema(local_schema, publish=True)
|
|
139
|
+
print(json.dumps(res, indent=2))
|
|
140
|
+
return 0
|
|
141
|
+
|
|
142
|
+
if action == "validate":
|
|
143
|
+
try:
|
|
144
|
+
res = db.validate_schema(local_schema)
|
|
145
|
+
except Exception as exc: # validation API should respond 200 + errors list; if not, treat as invalid
|
|
146
|
+
print(f"Schema at {schema_path} is INVALID:")
|
|
147
|
+
print(f" validation call failed: {exc}")
|
|
148
|
+
return 1
|
|
149
|
+
|
|
150
|
+
errors = []
|
|
151
|
+
valid_flag = None
|
|
152
|
+
if isinstance(res, dict):
|
|
153
|
+
errors = res.get("errors") or res.get("validationErrors") or []
|
|
154
|
+
valid_flag = res.get("valid")
|
|
155
|
+
|
|
156
|
+
if errors or valid_flag is False:
|
|
157
|
+
print(f"Schema at {schema_path} is INVALID:")
|
|
158
|
+
if errors:
|
|
159
|
+
print(json.dumps(errors, indent=2))
|
|
160
|
+
elif valid_flag is False:
|
|
161
|
+
print(" validation returned valid=false")
|
|
162
|
+
return 1
|
|
163
|
+
|
|
164
|
+
print(f"Schema at {schema_path} is valid.")
|
|
165
|
+
return 0
|
|
166
|
+
|
|
167
|
+
if action == "diff":
|
|
168
|
+
res = db.diff_schema(local_schema)
|
|
169
|
+
# Pretty print in a human-friendly, non-JSON style
|
|
170
|
+
print("newTables:", res.get("added_tables", []))
|
|
171
|
+
print("removedTables:", res.get("removed_tables", []))
|
|
172
|
+
print("changedTables:")
|
|
173
|
+
for table in res.get("changed_tables", []):
|
|
174
|
+
print(f" - name: \"{table.get('name')}\"")
|
|
175
|
+
attrs = table.get("attributes") or {}
|
|
176
|
+
print(" attributes:")
|
|
177
|
+
print(f" added: {attrs.get('added', [])}")
|
|
178
|
+
print(f" removed: {attrs.get('removed', [])}")
|
|
179
|
+
changed_attrs = attrs.get("changed", [])
|
|
180
|
+
if changed_attrs:
|
|
181
|
+
print(" changed:")
|
|
182
|
+
for ch in changed_attrs:
|
|
183
|
+
print(f" - name: \"{ch.get('name')}\"")
|
|
184
|
+
from_attr = ch.get("from") or {}
|
|
185
|
+
to_attr = ch.get("to") or {}
|
|
186
|
+
diffs = []
|
|
187
|
+
for key in sorted(set(from_attr.keys()) | set(to_attr.keys())):
|
|
188
|
+
if from_attr.get(key) != to_attr.get(key):
|
|
189
|
+
diffs.append(f"{key} ({from_attr.get(key)} -> {to_attr.get(key)})")
|
|
190
|
+
if diffs:
|
|
191
|
+
print(f" change: {', '.join(diffs)}")
|
|
192
|
+
else:
|
|
193
|
+
print(" change: none")
|
|
194
|
+
else:
|
|
195
|
+
print(" changed: []")
|
|
196
|
+
return 0
|
|
197
|
+
|
|
198
|
+
raise ValueError(f"Unknown schema action: {action}")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def handle_gen(args: argparse.Namespace) -> int:
|
|
202
|
+
source = args.source
|
|
203
|
+
schema_path = Path(args.schema) if args.schema else _default_schema_path()
|
|
204
|
+
out_paths = args.out or ["./onyx"]
|
|
205
|
+
timestamp_mode = args.timestamps
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
db = onyx.init(
|
|
209
|
+
request_timeout_seconds=args.timeout,
|
|
210
|
+
max_retries=args.max_retries,
|
|
211
|
+
retry_backoff_seconds=args.retry_backoff,
|
|
212
|
+
)
|
|
213
|
+
except Exception as exc:
|
|
214
|
+
print(f"Config error: {exc}")
|
|
215
|
+
return 1
|
|
216
|
+
if source == "api":
|
|
217
|
+
schema = db.get_schema()
|
|
218
|
+
else:
|
|
219
|
+
if not schema_path.exists():
|
|
220
|
+
raise FileNotFoundError(f"Schema file not found: {schema_path}")
|
|
221
|
+
schema = _load_json(schema_path)
|
|
222
|
+
|
|
223
|
+
if args.tables:
|
|
224
|
+
# subset to stdout only
|
|
225
|
+
subset = {**schema, "entities": [e for e in schema.get("entities", []) if e.get("name") in args.tables]}
|
|
226
|
+
print(json.dumps(subset, indent=2))
|
|
227
|
+
return 0
|
|
228
|
+
|
|
229
|
+
for out in out_paths:
|
|
230
|
+
out_path = Path(out)
|
|
231
|
+
pkg = args.package
|
|
232
|
+
if not pkg:
|
|
233
|
+
pkg = out_path.stem if out_path.suffix else out_path.name
|
|
234
|
+
generate_models(schema, out_path, package=pkg, timestamp_mode=timestamp_mode, models_mode=args.models)
|
|
235
|
+
print(f"generated models at {out}")
|
|
236
|
+
return 0
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
240
|
+
parser = argparse.ArgumentParser(prog="onyx-py", description="Onyx Database Python SDK CLI")
|
|
241
|
+
sub = parser.add_subparsers(dest="command")
|
|
242
|
+
|
|
243
|
+
gen = sub.add_parser("gen", help="Generate Python helpers/models from schema")
|
|
244
|
+
gen.add_argument("--source", choices=["api", "file"], default="file")
|
|
245
|
+
gen.add_argument("--schema", help="Path to schema JSON (when --source=file)")
|
|
246
|
+
gen.add_argument("--out", nargs="+", help="Output path(s) (dir or .py file). Default: ./generated")
|
|
247
|
+
gen.add_argument("--package", help="Package name when writing a package (optional)")
|
|
248
|
+
gen.add_argument("--timestamps", choices=["datetime", "string", "number"], default="datetime", help="Timestamp annotation mode")
|
|
249
|
+
gen.add_argument("--models", choices=["plain", "pydantic"], default="plain", help="Model generation style (plain classes or Pydantic BaseModel)")
|
|
250
|
+
gen.add_argument("--timeout", type=float, help="Request timeout (seconds)")
|
|
251
|
+
gen.add_argument("--max-retries", type=int, help="Max retries for GET/query requests")
|
|
252
|
+
gen.add_argument("--retry-backoff", type=float, help="Initial retry backoff in seconds")
|
|
253
|
+
gen.add_argument("--tables", nargs="+", help="When provided, print a subset of entities to stdout instead of writing files")
|
|
254
|
+
|
|
255
|
+
schema = sub.add_parser("schema", help="Schema management")
|
|
256
|
+
schema.add_argument("action", choices=["get", "publish", "validate", "diff"])
|
|
257
|
+
schema.add_argument("--schema", help="Local schema path (default: ./schema/onyx.schema.json or ./onyx.schema.json)")
|
|
258
|
+
schema.add_argument("--out", help="Output path for `schema get` (default: ./schema/onyx.schema.json)")
|
|
259
|
+
schema.add_argument("--tables", help="Comma-separated table names (for get)")
|
|
260
|
+
schema.add_argument("--timeout", type=float, help="Request timeout (seconds)")
|
|
261
|
+
schema.add_argument("--max-retries", type=int, help="Max retries for GET/query requests")
|
|
262
|
+
schema.add_argument("--retry-backoff", type=float, help="Initial retry backoff in seconds")
|
|
263
|
+
schema.add_argument("--print", dest="print_only", action="store_true", help="Print only for get")
|
|
264
|
+
|
|
265
|
+
info = sub.add_parser("info", help="Show resolved configuration and paths")
|
|
266
|
+
info.add_argument("--json", action="store_true", help="Print JSON output")
|
|
267
|
+
|
|
268
|
+
return parser
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def main(argv: list[str] | None = None) -> int:
|
|
272
|
+
parser = build_parser()
|
|
273
|
+
args = parser.parse_args(argv)
|
|
274
|
+
if args.command == "schema":
|
|
275
|
+
return handle_schema(args)
|
|
276
|
+
if args.command == "gen":
|
|
277
|
+
return handle_gen(args)
|
|
278
|
+
if args.command == "info":
|
|
279
|
+
return handle_info(args)
|
|
280
|
+
parser.print_help()
|
|
281
|
+
return 0
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
if __name__ == "__main__": # pragma: no cover
|
|
285
|
+
sys.exit(main())
|
onyx_database/codegen.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Minimal schema-driven model generator (stdlib only)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _safe_name(name: str) -> str:
|
|
11
|
+
return "".join(ch for ch in name if ch.isalnum() or ch == "_") or "Model"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _timestamp_annotation(mode: str) -> str:
|
|
15
|
+
if mode == "string":
|
|
16
|
+
return "str"
|
|
17
|
+
if mode == "number":
|
|
18
|
+
return "float"
|
|
19
|
+
return "datetime.datetime"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _attribute_annotation(attr: Dict[str, Any], ts_mode: str) -> str:
|
|
23
|
+
type_name = attr.get("type", "String")
|
|
24
|
+
nullable = bool(attr.get("isNullable"))
|
|
25
|
+
py = "str"
|
|
26
|
+
if type_name.lower() in {"int", "integer"}:
|
|
27
|
+
py = "int"
|
|
28
|
+
elif type_name.lower() == "boolean":
|
|
29
|
+
py = "bool"
|
|
30
|
+
elif type_name.lower() in {"float", "double", "number"}:
|
|
31
|
+
py = "float"
|
|
32
|
+
elif type_name.lower() == "timestamp":
|
|
33
|
+
py = _timestamp_annotation(ts_mode)
|
|
34
|
+
elif type_name.lower() == "embeddedobject":
|
|
35
|
+
py = "dict"
|
|
36
|
+
ann = py
|
|
37
|
+
if nullable:
|
|
38
|
+
ann = f"Optional[{py}]"
|
|
39
|
+
return ann
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _emit_tables(entities: List[Dict[str, Any]]) -> str:
|
|
43
|
+
lines = ["class tables:", " \"\"\"Table name constants.\"\"\""]
|
|
44
|
+
if not entities:
|
|
45
|
+
lines.append(" pass")
|
|
46
|
+
return "\n".join(lines) + "\n"
|
|
47
|
+
for e in entities:
|
|
48
|
+
name = e.get("name", "")
|
|
49
|
+
safe = _safe_name(name)
|
|
50
|
+
lines.append(f" {safe} = \"{name}\"")
|
|
51
|
+
return "\n".join(lines) + "\n"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _emit_init(entities: List[Dict[str, Any]]) -> str:
|
|
55
|
+
class_names = [_safe_name(e.get("name", "Model")) for e in entities]
|
|
56
|
+
imports = ", ".join(class_names)
|
|
57
|
+
model_map_items = ", ".join([f'"{e.get("name", "")}": {cls}' for e, cls in zip(entities, class_names)])
|
|
58
|
+
return "\n".join(
|
|
59
|
+
[
|
|
60
|
+
f"from .models import {imports}" if imports else "",
|
|
61
|
+
"from .tables import tables",
|
|
62
|
+
"from .schema import SCHEMA_JSON",
|
|
63
|
+
f"SCHEMA = {{{model_map_items}}}",
|
|
64
|
+
"__all__ = ['tables', 'SCHEMA_JSON', 'SCHEMA'" + (", " + ", ".join([f"'{c}'" for c in class_names]) if class_names else "") + "]",
|
|
65
|
+
]
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _emit_model(entity: Dict[str, Any], ts_mode: str) -> str:
|
|
70
|
+
name = _safe_name(entity.get("name", "Model"))
|
|
71
|
+
attrs: List[Dict[str, Any]] = entity.get("attributes", []) or []
|
|
72
|
+
lines = [
|
|
73
|
+
f"class {name}:",
|
|
74
|
+
' """Generated model (plain Python class). Resolver/extra fields are allowed via **extra."""',
|
|
75
|
+
]
|
|
76
|
+
init_params = ["self"]
|
|
77
|
+
body: List[str] = []
|
|
78
|
+
for attr in attrs:
|
|
79
|
+
field = attr.get("name", "")
|
|
80
|
+
ann = _attribute_annotation(attr, ts_mode)
|
|
81
|
+
init_params.append(f"{field}: {ann} = None")
|
|
82
|
+
body.append(f" self.{field} = {field}")
|
|
83
|
+
init_params.append("**extra: Any")
|
|
84
|
+
body.append(" # allow resolver-attached fields or extra properties")
|
|
85
|
+
body.append(" for k, v in extra.items():")
|
|
86
|
+
body.append(" setattr(self, k, v)")
|
|
87
|
+
if not body:
|
|
88
|
+
body.append(" pass")
|
|
89
|
+
lines.append(f" def __init__({', '.join(init_params)}):")
|
|
90
|
+
lines.extend(body)
|
|
91
|
+
lines.append("")
|
|
92
|
+
return "\n".join(lines)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _emit_all_models(entities: List[Dict[str, Any]], ts_mode: str) -> str:
|
|
96
|
+
out: List[str] = ["import datetime", "from typing import Any, Optional", ""]
|
|
97
|
+
for e in entities:
|
|
98
|
+
out.append(_emit_model(e, ts_mode))
|
|
99
|
+
out.append("")
|
|
100
|
+
return "\n".join(out)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _emit_schema_map(schema: Dict[str, Any]) -> str:
|
|
104
|
+
cleaned = {"databaseId": schema.get("databaseId"), "revisionDescription": schema.get("revisionDescription"), "entities": schema.get("entities", [])}
|
|
105
|
+
py_literal = json.dumps(cleaned, indent=2)
|
|
106
|
+
py_literal = (
|
|
107
|
+
py_literal.replace("true", "True")
|
|
108
|
+
.replace("false", "False")
|
|
109
|
+
.replace("null", "None")
|
|
110
|
+
)
|
|
111
|
+
return "SCHEMA_JSON = " + py_literal + "\n"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _emit_pydantic_models(entities: List[Dict[str, Any]], ts_mode: str) -> str:
|
|
115
|
+
lines: List[str] = [
|
|
116
|
+
"from pydantic import BaseModel, ConfigDict",
|
|
117
|
+
"import datetime",
|
|
118
|
+
"from typing import Optional, Any",
|
|
119
|
+
"",
|
|
120
|
+
]
|
|
121
|
+
for entity in entities:
|
|
122
|
+
name = _safe_name(entity.get("name", "Model"))
|
|
123
|
+
attrs: List[Dict[str, Any]] = entity.get("attributes", []) or []
|
|
124
|
+
lines.append(f"class {name}(BaseModel):")
|
|
125
|
+
if not attrs:
|
|
126
|
+
lines.append(" model_config = ConfigDict(extra='allow')")
|
|
127
|
+
lines.append(" pass")
|
|
128
|
+
lines.append("")
|
|
129
|
+
continue
|
|
130
|
+
for attr in attrs:
|
|
131
|
+
field = attr.get("name", "")
|
|
132
|
+
ann = _attribute_annotation(attr, ts_mode)
|
|
133
|
+
lines.append(f" {field}: {ann} = None")
|
|
134
|
+
lines.append(" model_config = ConfigDict(extra='allow')")
|
|
135
|
+
lines.append("")
|
|
136
|
+
return "\n".join(lines)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def generate_models(schema: Dict[str, Any], out: Path, *, package: Optional[str] = None, timestamp_mode: str = "datetime", models_mode: str = "plain") -> None:
|
|
140
|
+
entities: List[Dict[str, Any]] = schema.get("entities", []) or []
|
|
141
|
+
ts_mode = timestamp_mode
|
|
142
|
+
if out.suffix == ".py":
|
|
143
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
144
|
+
model_block = _emit_all_models(entities, ts_mode) if models_mode == "plain" else _emit_pydantic_models(entities, ts_mode)
|
|
145
|
+
content = "\n".join(
|
|
146
|
+
[
|
|
147
|
+
"import datetime",
|
|
148
|
+
"from typing import Optional",
|
|
149
|
+
"",
|
|
150
|
+
_emit_tables(entities),
|
|
151
|
+
_emit_schema_map(schema),
|
|
152
|
+
model_block,
|
|
153
|
+
]
|
|
154
|
+
)
|
|
155
|
+
out.write_text(content, encoding="utf-8")
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
# treat as directory
|
|
159
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
init_text = _emit_init(entities)
|
|
161
|
+
(out / "__init__.py").write_text(init_text + "\n", encoding="utf-8")
|
|
162
|
+
(out / "tables.py").write_text(_emit_tables(entities), encoding="utf-8")
|
|
163
|
+
(out / "schema.py").write_text(_emit_schema_map(schema), encoding="utf-8")
|
|
164
|
+
models_path = out / "models.py"
|
|
165
|
+
model_block = _emit_all_models(entities, ts_mode) if models_mode == "plain" else _emit_pydantic_models(entities, ts_mode)
|
|
166
|
+
models_path.write_text(model_block, encoding="utf-8")
|