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.
@@ -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())
@@ -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")