exdrf-ts 0.1.17__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.
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: exdrf-ts
3
+ Version: 0.1.17
4
+ Summary: Map exdrf field and Python types to TypeScript for codegen.
5
+ Author-email: Nicu Tofan <nicu.tofan@gmail.com>
6
+ License-Expression: MIT
7
+ Classifier: Operating System :: OS Independent
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Typing :: Typed
11
+ Requires-Python: >=3.12.2
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: exdrf>=0.1.17
14
+ Requires-Dist: exdrf-pd>=0.1.18
15
+ Provides-Extra: dev
16
+ Requires-Dist: autoflake; extra == "dev"
17
+ Requires-Dist: black; extra == "dev"
18
+ Requires-Dist: build; extra == "dev"
19
+ Requires-Dist: flake8; extra == "dev"
20
+ Requires-Dist: isort; extra == "dev"
21
+ Requires-Dist: mypy; extra == "dev"
22
+ Requires-Dist: pre-commit; extra == "dev"
23
+ Requires-Dist: pyproject-flake8; extra == "dev"
24
+ Requires-Dist: pytest-cov; extra == "dev"
25
+ Requires-Dist: pytest-mock; extra == "dev"
26
+ Requires-Dist: pytest; extra == "dev"
27
+ Requires-Dist: twine; extra == "dev"
28
+ Requires-Dist: wheel; extra == "dev"
29
+
30
+ # exdrf-ts
31
+
32
+ **exdrf-ts** maps **exdrf** field-type constants and Python typing objects to
33
+ **TypeScript** strings and DARE field class names. Code generators
34
+ (**exdrf-gen-pd2dare**, **exdrf-gen-openapi2rtk**, app-specific pipelines)
35
+ import it when emitting TS from Pydantic-backed or OpenAPI-backed resources.
36
+
37
+ The PyPI distribution name is **`exdrf-ts`**. The import package is
38
+ **`exdrf_ts`**.
39
+
40
+ Python **3.12.2+** is required. Runtime dependencies: **exdrf** and
41
+ **exdrf-pd**.
42
+
43
+ ## Installation
44
+
45
+ ```text
46
+ pip install exdrf-ts
47
+ ```
48
+
49
+ Editable install from the exdrf monorepo root (with other packages present as
50
+ needed):
51
+
52
+ ```text
53
+ pip install -e ./exdrf-ts
54
+ ```
55
+
56
+ ## API
57
+
58
+ - **`py_type_to_ts`**: Python annotation or runtime type to TS type string.
59
+ - **`type_to_field_class`**: Maps **exdrf** `FIELD_TYPE_*` values to DARE
60
+ field class names (for example `StringField`).
61
+ - **`model_rel_import`**: Relative import path between two models (via
62
+ **`exdrf_pd.visitor.ExModelVisitor`**).
63
+
64
+ ## Role in the stack
65
+
66
+ - **`exdrf`**: core dataset / field model.
67
+ - **`exdrf-pd`**: Pydantic **`ExModel`** and visitors.
68
+ - **`exdrf-ts`**: TypeScript-oriented views of those types.
@@ -0,0 +1,39 @@
1
+ # exdrf-ts
2
+
3
+ **exdrf-ts** maps **exdrf** field-type constants and Python typing objects to
4
+ **TypeScript** strings and DARE field class names. Code generators
5
+ (**exdrf-gen-pd2dare**, **exdrf-gen-openapi2rtk**, app-specific pipelines)
6
+ import it when emitting TS from Pydantic-backed or OpenAPI-backed resources.
7
+
8
+ The PyPI distribution name is **`exdrf-ts`**. The import package is
9
+ **`exdrf_ts`**.
10
+
11
+ Python **3.12.2+** is required. Runtime dependencies: **exdrf** and
12
+ **exdrf-pd**.
13
+
14
+ ## Installation
15
+
16
+ ```text
17
+ pip install exdrf-ts
18
+ ```
19
+
20
+ Editable install from the exdrf monorepo root (with other packages present as
21
+ needed):
22
+
23
+ ```text
24
+ pip install -e ./exdrf-ts
25
+ ```
26
+
27
+ ## API
28
+
29
+ - **`py_type_to_ts`**: Python annotation or runtime type to TS type string.
30
+ - **`type_to_field_class`**: Maps **exdrf** `FIELD_TYPE_*` values to DARE
31
+ field class names (for example `StringField`).
32
+ - **`model_rel_import`**: Relative import path between two models (via
33
+ **`exdrf_pd.visitor.ExModelVisitor`**).
34
+
35
+ ## Role in the stack
36
+
37
+ - **`exdrf`**: core dataset / field model.
38
+ - **`exdrf-pd`**: Pydantic **`ExModel`** and visitors.
39
+ - **`exdrf-ts`**: TypeScript-oriented views of those types.
@@ -0,0 +1,17 @@
1
+ """TypeScript-oriented type mapping for exdrf-backed codegen."""
2
+
3
+ from exdrf_ts.json_schema_ts import json_schema_to_ts
4
+ from exdrf_ts.mapping import (
5
+ model_rel_import,
6
+ py_type_to_ts,
7
+ py_type_to_ts_map,
8
+ type_to_field_class,
9
+ )
10
+
11
+ __all__ = [
12
+ "json_schema_to_ts",
13
+ "model_rel_import",
14
+ "py_type_to_ts",
15
+ "py_type_to_ts_map",
16
+ "type_to_field_class",
17
+ ]
@@ -0,0 +1,24 @@
1
+ """Package version from PEP 621 or installed metadata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from exdrf.pep621_version import distribution_version, version_tuple_from_string
8
+
9
+ _PYPROJECT = Path(__file__).resolve().parents[1] / "pyproject.toml"
10
+ _DIST_NAME = "exdrf-ts"
11
+
12
+ __version__ = version = distribution_version(_DIST_NAME, _PYPROJECT)
13
+ __version_tuple__ = version_tuple = version_tuple_from_string(__version__)
14
+
15
+ __commit_id__ = commit_id = None
16
+
17
+ __all__ = [
18
+ "__version__",
19
+ "__version_tuple__",
20
+ "version",
21
+ "version_tuple",
22
+ "__commit_id__",
23
+ "commit_id",
24
+ ]
@@ -0,0 +1,238 @@
1
+ """Map OpenAPI / JSON Schema fragments to TypeScript type strings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from typing import Any, Mapping
8
+
9
+ _REF_RE = re.compile(r"^#/components/schemas/([A-Za-z0-9_.-]+)$")
10
+
11
+
12
+ def _json_pointer_get(doc: Mapping[str, Any], pointer: str) -> Any:
13
+ """Resolve a JSON Pointer (RFC 6901) against ``doc``.
14
+
15
+ Only supports absolute pointers starting with ``/`` (no URI fragment).
16
+
17
+ Args:
18
+ doc: JSON object to traverse.
19
+ pointer: Pointer such as ``/components/schemas/Foo``.
20
+
21
+ Returns:
22
+ The value at ``pointer``, or ``None`` if a segment is missing.
23
+ """
24
+
25
+ if not pointer.startswith("/"):
26
+ return None
27
+ cur: Any = doc
28
+ for raw in pointer.strip("/").split("/"):
29
+ key = raw.replace("~1", "/").replace("~0", "~")
30
+ if not isinstance(cur, Mapping) or key not in cur:
31
+ return None
32
+ cur = cur[key]
33
+ return cur
34
+
35
+
36
+ def _resolve_ref(
37
+ ref: str,
38
+ resolve_root: Mapping[str, Any],
39
+ seen_refs: frozenset[str],
40
+ ) -> tuple[Any, frozenset[str]] | None:
41
+ """Follow an OpenAPI ``#/components/schemas/...`` reference.
42
+
43
+ Args:
44
+ ref: Reference string from ``"$ref"``.
45
+ resolve_root: Full OpenAPI document (or object containing
46
+ ``components``).
47
+ seen_refs: References already followed (cycle guard).
48
+
49
+ Returns:
50
+ ``(target_schema, seen_refs_with_ref)`` or ``None`` if unsupported.
51
+ """
52
+
53
+ m = _REF_RE.match(ref)
54
+ if not m:
55
+ return None
56
+ name = m.group(1)
57
+ if ref in seen_refs:
58
+ return None
59
+ pointer = f"/components/schemas/{name}"
60
+ target = _json_pointer_get(resolve_root, pointer)
61
+ if target is None:
62
+ return None
63
+ return target, frozenset((*seen_refs, ref))
64
+
65
+
66
+ def json_schema_to_ts(
67
+ schema: Any,
68
+ resolve_root: Mapping[str, Any] | None = None,
69
+ *,
70
+ seen_refs: frozenset[str] | None = None,
71
+ ) -> str:
72
+ """Convert a JSON Schema fragment to a TypeScript type string.
73
+
74
+ Intended for OpenAPI 3.x ``components.schemas`` and inline parameter /
75
+ response bodies. Unsupported or cyclic constructs fall back to
76
+ ``unknown``.
77
+
78
+ Args:
79
+ schema: Schema object (``dict``), ``True`` (accept-all schema in JSON
80
+ Schema drafts), or ``False`` (reject-all; emitted as ``never``).
81
+ resolve_root: Document used to resolve ``#/components/schemas/...``
82
+ ``"$ref"`` values. When ``None``, ``"$ref"`` resolves to
83
+ ``unknown``.
84
+ seen_refs: Internal recursion guard; do not pass from callers.
85
+
86
+ Returns:
87
+ TypeScript type text.
88
+ """
89
+
90
+ stack = frozenset() if seen_refs is None else seen_refs
91
+
92
+ if schema is True:
93
+ return "unknown"
94
+ if schema is False:
95
+ return "never"
96
+ if not isinstance(schema, Mapping):
97
+ return "unknown"
98
+
99
+ ref = schema.get("$ref")
100
+ if isinstance(ref, str) and resolve_root is not None:
101
+ resolved = _resolve_ref(ref, resolve_root, stack)
102
+ if resolved is not None:
103
+ sub, new_stack = resolved
104
+ return json_schema_to_ts(sub, resolve_root, seen_refs=new_stack)
105
+ m = _REF_RE.match(ref)
106
+ if m:
107
+ return m.group(1)
108
+ return "unknown"
109
+
110
+ if "enum" in schema and isinstance(schema["enum"], list):
111
+ literals: list[str] = []
112
+ for v in schema["enum"]:
113
+ if isinstance(v, str):
114
+ literals.append(json.dumps(v))
115
+ elif isinstance(v, bool):
116
+ literals.append("true" if v else "false")
117
+ elif v is None:
118
+ literals.append("null")
119
+ elif isinstance(v, (int, float)):
120
+ literals.append(json.dumps(v))
121
+ if not literals:
122
+ return "unknown"
123
+ return " | ".join(literals)
124
+
125
+ if "oneOf" in schema and isinstance(schema["oneOf"], list):
126
+ parts = [
127
+ json_schema_to_ts(s, resolve_root, seen_refs=stack) for s in schema["oneOf"]
128
+ ]
129
+ parts_u = [p for p in parts if p != "unknown"]
130
+ if not parts_u:
131
+ return "unknown"
132
+ return " | ".join(dict.fromkeys(parts_u))
133
+
134
+ if "anyOf" in schema and isinstance(schema["anyOf"], list):
135
+ parts = [
136
+ json_schema_to_ts(s, resolve_root, seen_refs=stack) for s in schema["anyOf"]
137
+ ]
138
+ parts_u = [p for p in parts if p != "unknown"]
139
+ if not parts_u:
140
+ return "unknown"
141
+ return " | ".join(dict.fromkeys(parts_u))
142
+
143
+ if "allOf" in schema and isinstance(schema["allOf"], list):
144
+ parts = [
145
+ json_schema_to_ts(s, resolve_root, seen_refs=stack) for s in schema["allOf"]
146
+ ]
147
+ if all(p == "unknown" for p in parts):
148
+ return "unknown"
149
+
150
+ def _part(p: str) -> str:
151
+ if " | " in p or " & " in p:
152
+ return f"({p})"
153
+ return p
154
+
155
+ return " & ".join(_part(p) for p in parts)
156
+
157
+ type_val = schema.get("type")
158
+ types: list[str] = []
159
+ if isinstance(type_val, list):
160
+ types = [t for t in type_val if isinstance(t, str)]
161
+ elif isinstance(type_val, str):
162
+ types = [type_val]
163
+
164
+ nullable = schema.get("nullable") is True
165
+
166
+ def _wrap_null(t: str) -> str:
167
+ if nullable:
168
+ return f"{t} | null"
169
+ return t
170
+
171
+ if not types:
172
+ if "properties" in schema or "additionalProperties" in schema:
173
+ return _wrap_null(_object_to_ts(schema, resolve_root, stack))
174
+ return "unknown"
175
+
176
+ out_parts: list[str] = []
177
+ for t in types:
178
+ if t == "string":
179
+ out_parts.append("string")
180
+ elif t == "integer":
181
+ out_parts.append("number")
182
+ elif t == "number":
183
+ out_parts.append("number")
184
+ elif t == "boolean":
185
+ out_parts.append("boolean")
186
+ elif t == "null":
187
+ out_parts.append("null")
188
+ elif t == "array":
189
+ items = schema.get("items", {})
190
+ inner = json_schema_to_ts(items, resolve_root, seen_refs=stack)
191
+ out_parts.append(f"{inner}[]")
192
+ elif t == "object":
193
+ out_parts.append(_object_to_ts(schema, resolve_root, stack))
194
+ else:
195
+ out_parts.append("unknown")
196
+
197
+ merged = " | ".join(dict.fromkeys(out_parts)) if out_parts else "unknown"
198
+ return _wrap_null(merged)
199
+
200
+
201
+ def _object_to_ts(
202
+ schema: Mapping[str, Any],
203
+ resolve_root: Mapping[str, Any] | None,
204
+ stack: frozenset[str],
205
+ ) -> str:
206
+ """Build an inline object type from schema ``properties`` / ``additional``.
207
+
208
+ Args:
209
+ schema: JSON Schema with ``type`` ``object`` (or implied).
210
+ resolve_root: Document for ``$ref`` inside nested schemas.
211
+ stack: Active ``$ref`` stack.
212
+
213
+ Returns:
214
+ Inline TypeScript object type or ``Record<...>``.
215
+ """
216
+
217
+ props = schema.get("properties")
218
+ required = set(schema.get("required", []) or [])
219
+ if isinstance(props, Mapping) and props:
220
+ lines: list[str] = []
221
+ for key, sub in props.items():
222
+ opt = "" if key in required else "?"
223
+ ts = json_schema_to_ts(sub, resolve_root, seen_refs=stack)
224
+ safe_key = (
225
+ key
226
+ if re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", str(key))
227
+ else json.dumps(str(key))
228
+ )
229
+ lines.append(f" {safe_key}{opt}: {ts};")
230
+ return "{\n" + "\n".join(lines) + "\n}"
231
+
232
+ addl = schema.get("additionalProperties")
233
+ if addl is True:
234
+ return "{ [key: string]: unknown }"
235
+ if isinstance(addl, Mapping):
236
+ inner = json_schema_to_ts(addl, resolve_root, seen_refs=stack)
237
+ return f"{{ [key: string]: {inner} }}"
238
+ return "{ [key: string]: unknown }"
@@ -0,0 +1,159 @@
1
+ """Map exdrf types and Python annotations to TypeScript."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import types
6
+ from typing import Any, Union, get_args, get_origin
7
+
8
+ from exdrf.constants import (
9
+ FIELD_TYPE_BLOB,
10
+ FIELD_TYPE_BOOL,
11
+ FIELD_TYPE_DATE,
12
+ FIELD_TYPE_DT,
13
+ FIELD_TYPE_DURATION,
14
+ FIELD_TYPE_ENUM,
15
+ FIELD_TYPE_FILTER,
16
+ FIELD_TYPE_FLOAT,
17
+ FIELD_TYPE_FLOAT_LIST,
18
+ FIELD_TYPE_FORMATTED,
19
+ FIELD_TYPE_INT_LIST,
20
+ FIELD_TYPE_INTEGER,
21
+ FIELD_TYPE_REF_MANY_TO_MANY,
22
+ FIELD_TYPE_REF_MANY_TO_ONE,
23
+ FIELD_TYPE_REF_ONE_TO_MANY,
24
+ FIELD_TYPE_REF_ONE_TO_ONE,
25
+ FIELD_TYPE_SORT,
26
+ FIELD_TYPE_STRING,
27
+ FIELD_TYPE_STRING_LIST,
28
+ )
29
+ from exdrf_pd.visitor import ExModelVisitor
30
+
31
+ py_type_to_ts_map = {
32
+ "str": "string",
33
+ "int": "number",
34
+ "float": "number",
35
+ "bool": "boolean",
36
+ }
37
+
38
+ type_to_field_class = {
39
+ FIELD_TYPE_BLOB: "BlobField",
40
+ FIELD_TYPE_BOOL: "BooleanField",
41
+ FIELD_TYPE_DATE: "DateField",
42
+ FIELD_TYPE_DT: "DateTimeField",
43
+ FIELD_TYPE_DURATION: "DurationField",
44
+ FIELD_TYPE_ENUM: "EnumField",
45
+ FIELD_TYPE_FLOAT: "FloatField",
46
+ FIELD_TYPE_INTEGER: "IntegerField",
47
+ FIELD_TYPE_STRING: "StringField",
48
+ FIELD_TYPE_FORMATTED: "FormattedField",
49
+ FIELD_TYPE_REF_ONE_TO_MANY: "RefOneToManyField",
50
+ FIELD_TYPE_REF_MANY_TO_ONE: "RefManyToOneField",
51
+ FIELD_TYPE_REF_ONE_TO_ONE: "RefOneToOneField",
52
+ FIELD_TYPE_REF_MANY_TO_MANY: "RefManyToManyField",
53
+ FIELD_TYPE_STRING_LIST: "StringListField",
54
+ FIELD_TYPE_INT_LIST: "IntListField",
55
+ FIELD_TYPE_FLOAT_LIST: "FloatListField",
56
+ FIELD_TYPE_FILTER: "FilterField",
57
+ FIELD_TYPE_SORT: "SortField",
58
+ }
59
+
60
+
61
+ def _py_type_to_ts_string(name: str) -> str:
62
+ """Convert a stringified annotation to a TypeScript type.
63
+
64
+ Args:
65
+ name: Annotation text (for example ``List[str]``).
66
+
67
+ Returns:
68
+ Equivalent TypeScript type string.
69
+ """
70
+
71
+ if name == "Any":
72
+ return "unknown"
73
+
74
+ if name.startswith("List[") and name.endswith("]"):
75
+ return f"{py_type_to_ts(name[5:-1])}[]"
76
+
77
+ if name.startswith("Dict[") and name.endswith("]"):
78
+ key, value = name[5:-1].split(", ")
79
+ if py_type_to_ts(key) == "str":
80
+ return f"{{ {key}: {py_type_to_ts(value)} }}"
81
+ return f"{{ [key: {py_type_to_ts(key)}]: {py_type_to_ts(value)} }}"
82
+
83
+ if name.startswith("Optional[") and name.endswith("]"):
84
+ return f"{py_type_to_ts(name[9:-1])} | undefined"
85
+
86
+ return py_type_to_ts_map.get(name, name)
87
+
88
+
89
+ def py_type_to_ts(name: Union[str, type, types.UnionType, Any]) -> str:
90
+ """Convert a Python type or annotation string to a TypeScript type.
91
+
92
+ Args:
93
+ name: A type object, string annotation, or typing construct.
94
+
95
+ Returns:
96
+ TypeScript type text suitable for emitted source.
97
+ """
98
+
99
+ if isinstance(name, str):
100
+ return _py_type_to_ts_string(name)
101
+
102
+ args = get_args(name)
103
+ if args and type(None) in args:
104
+ non_none = [a for a in args if a is not type(None)]
105
+ if len(non_none) == 1:
106
+ return f"{py_type_to_ts(non_none[0])} | undefined"
107
+
108
+ origin = get_origin(name)
109
+ if origin is list:
110
+ if len(args) == 1:
111
+ return f"{py_type_to_ts(args[0])}[]"
112
+
113
+ if origin is dict:
114
+ if len(args) == 2:
115
+ key_t, val_t = args
116
+ if py_type_to_ts(key_t) == "string":
117
+ return f"{{ [key: string]: {py_type_to_ts(val_t)} }}"
118
+ return f"{{ [key: {py_type_to_ts(key_t)}]: {py_type_to_ts(val_t)} }}"
119
+
120
+ if hasattr(name, "__name__") and origin is None:
121
+ py_name = name.__name__
122
+ if py_name == "Any":
123
+ return "unknown"
124
+ mapped = py_type_to_ts_map.get(py_name)
125
+ if mapped is not None:
126
+ return mapped
127
+ return py_name
128
+
129
+ return "unknown"
130
+
131
+
132
+ def model_rel_import(model: Any, ref_model: Any) -> str:
133
+ """Compute a model import path relative to another model.
134
+
135
+ Args:
136
+ model: The resource or model to import.
137
+ ref_model: The reference model (import origin).
138
+
139
+ Returns:
140
+ Relative path using ``/`` segments (for TS-style paths).
141
+ """
142
+
143
+ categories = ExModelVisitor.category(model)
144
+ ref_categories = ExModelVisitor.category(ref_model)
145
+
146
+ # Find the common prefix between category chains.
147
+ i = 0
148
+ while (
149
+ i < len(categories)
150
+ and i < len(ref_categories)
151
+ and categories[i] == ref_categories[i]
152
+ ):
153
+ i += 1
154
+
155
+ # Walk up from the reference, then down through the remainder.
156
+ path = [".."] * (len(ref_categories) - i)
157
+ path.extend(categories[i:])
158
+
159
+ return "/".join(path)
File without changes
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: exdrf-ts
3
+ Version: 0.1.17
4
+ Summary: Map exdrf field and Python types to TypeScript for codegen.
5
+ Author-email: Nicu Tofan <nicu.tofan@gmail.com>
6
+ License-Expression: MIT
7
+ Classifier: Operating System :: OS Independent
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Typing :: Typed
11
+ Requires-Python: >=3.12.2
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: exdrf>=0.1.17
14
+ Requires-Dist: exdrf-pd>=0.1.18
15
+ Provides-Extra: dev
16
+ Requires-Dist: autoflake; extra == "dev"
17
+ Requires-Dist: black; extra == "dev"
18
+ Requires-Dist: build; extra == "dev"
19
+ Requires-Dist: flake8; extra == "dev"
20
+ Requires-Dist: isort; extra == "dev"
21
+ Requires-Dist: mypy; extra == "dev"
22
+ Requires-Dist: pre-commit; extra == "dev"
23
+ Requires-Dist: pyproject-flake8; extra == "dev"
24
+ Requires-Dist: pytest-cov; extra == "dev"
25
+ Requires-Dist: pytest-mock; extra == "dev"
26
+ Requires-Dist: pytest; extra == "dev"
27
+ Requires-Dist: twine; extra == "dev"
28
+ Requires-Dist: wheel; extra == "dev"
29
+
30
+ # exdrf-ts
31
+
32
+ **exdrf-ts** maps **exdrf** field-type constants and Python typing objects to
33
+ **TypeScript** strings and DARE field class names. Code generators
34
+ (**exdrf-gen-pd2dare**, **exdrf-gen-openapi2rtk**, app-specific pipelines)
35
+ import it when emitting TS from Pydantic-backed or OpenAPI-backed resources.
36
+
37
+ The PyPI distribution name is **`exdrf-ts`**. The import package is
38
+ **`exdrf_ts`**.
39
+
40
+ Python **3.12.2+** is required. Runtime dependencies: **exdrf** and
41
+ **exdrf-pd**.
42
+
43
+ ## Installation
44
+
45
+ ```text
46
+ pip install exdrf-ts
47
+ ```
48
+
49
+ Editable install from the exdrf monorepo root (with other packages present as
50
+ needed):
51
+
52
+ ```text
53
+ pip install -e ./exdrf-ts
54
+ ```
55
+
56
+ ## API
57
+
58
+ - **`py_type_to_ts`**: Python annotation or runtime type to TS type string.
59
+ - **`type_to_field_class`**: Maps **exdrf** `FIELD_TYPE_*` values to DARE
60
+ field class names (for example `StringField`).
61
+ - **`model_rel_import`**: Relative import path between two models (via
62
+ **`exdrf_pd.visitor.ExModelVisitor`**).
63
+
64
+ ## Role in the stack
65
+
66
+ - **`exdrf`**: core dataset / field model.
67
+ - **`exdrf-pd`**: Pydantic **`ExModel`** and visitors.
68
+ - **`exdrf-ts`**: TypeScript-oriented views of those types.
@@ -0,0 +1,14 @@
1
+ README.md
2
+ pyproject.toml
3
+ exdrf_ts/__init__.py
4
+ exdrf_ts/__version__.py
5
+ exdrf_ts/json_schema_ts.py
6
+ exdrf_ts/mapping.py
7
+ exdrf_ts/py.typed
8
+ exdrf_ts.egg-info/PKG-INFO
9
+ exdrf_ts.egg-info/SOURCES.txt
10
+ exdrf_ts.egg-info/dependency_links.txt
11
+ exdrf_ts.egg-info/requires.txt
12
+ exdrf_ts.egg-info/top_level.txt
13
+ tests/json_schema_ts_test.py
14
+ tests/mapping_test.py
@@ -0,0 +1,17 @@
1
+ exdrf>=0.1.17
2
+ exdrf-pd>=0.1.18
3
+
4
+ [dev]
5
+ autoflake
6
+ black
7
+ build
8
+ flake8
9
+ isort
10
+ mypy
11
+ pre-commit
12
+ pyproject-flake8
13
+ pytest-cov
14
+ pytest-mock
15
+ pytest
16
+ twine
17
+ wheel
@@ -0,0 +1,3 @@
1
+ dist
2
+ exdrf_ts
3
+ tests
@@ -0,0 +1,63 @@
1
+ [project]
2
+ authors = [
3
+ { name = "Nicu Tofan", email = "nicu.tofan@gmail.com" },
4
+ ]
5
+ license = "MIT"
6
+ classifiers = [
7
+ "Operating System :: OS Independent",
8
+ "Programming Language :: Python :: 3 :: Only",
9
+ "Development Status :: 3 - Alpha",
10
+ "Typing :: Typed",
11
+ ]
12
+ dependencies = [
13
+ "exdrf>=0.1.17",
14
+ "exdrf-pd>=0.1.18",
15
+ ]
16
+ description = "Map exdrf field and Python types to TypeScript for codegen."
17
+ version = "0.1.17"
18
+ name = "exdrf-ts"
19
+ readme = "README.md"
20
+ requires-python = ">=3.12.2"
21
+
22
+ [project.optional-dependencies]
23
+ dev = [
24
+ "autoflake",
25
+ "black",
26
+ "build",
27
+ "flake8",
28
+ "isort",
29
+ "mypy",
30
+ "pre-commit",
31
+ "pyproject-flake8",
32
+ "pytest-cov",
33
+ "pytest-mock",
34
+ "pytest",
35
+ "twine",
36
+ "wheel",
37
+ ]
38
+
39
+ [build-system]
40
+ build-backend = "setuptools.build_meta"
41
+ requires = ["setuptools>=67.0"]
42
+
43
+ [tool.setuptools]
44
+ include-package-data = true
45
+
46
+ [tool.setuptools.package-data]
47
+ exdrf_ts = ["py.typed"]
48
+
49
+ [tool.setuptools.packages.find]
50
+ exclude = ["venv*", "playground*"]
51
+
52
+
53
+ [tool.isort]
54
+ profile = "black"
55
+
56
+ [tool.black]
57
+ line-length = 80
58
+ target-version = ['py312']
59
+
60
+ [tool.flake8]
61
+ docstring-convention = "google"
62
+ max-line-length = 80
63
+ extend-ignore = ["E203", "E501", "W503"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,100 @@
1
+ """Tests for :mod:`exdrf_ts.json_schema_ts`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from exdrf_ts.json_schema_ts import json_schema_to_ts
6
+
7
+
8
+ class TestJsonSchemaToTsPrimitives:
9
+ """Primitive and array JSON Schema types."""
10
+
11
+ def test_string(self) -> None:
12
+ """``type: string`` maps to TypeScript ``string``."""
13
+
14
+ assert json_schema_to_ts({"type": "string"}) == "string"
15
+
16
+ def test_integer_and_number(self) -> None:
17
+ """Numeric JSON Schema types map to ``number``."""
18
+
19
+ assert json_schema_to_ts({"type": "integer"}) == "number"
20
+ assert json_schema_to_ts({"type": "number"}) == "number"
21
+
22
+ def test_boolean(self) -> None:
23
+ """``type: boolean`` maps to ``boolean``."""
24
+
25
+ assert json_schema_to_ts({"type": "boolean"}) == "boolean"
26
+
27
+ def test_array_of_string(self) -> None:
28
+ """``type: array`` with ``items`` maps to element type plus ``[]``."""
29
+
30
+ out = json_schema_to_ts(
31
+ {"type": "array", "items": {"type": "string"}},
32
+ )
33
+ assert out == "string[]"
34
+
35
+ def test_nullable_string(self) -> None:
36
+ """OpenAPI 3 ``nullable: true`` adds ``| null``."""
37
+
38
+ assert (
39
+ json_schema_to_ts({"type": "string", "nullable": True}) == "string | null"
40
+ )
41
+
42
+
43
+ class TestJsonSchemaToTsRef:
44
+ """``$ref`` resolution against a synthetic OpenAPI document."""
45
+
46
+ def test_components_schema_ref(self) -> None:
47
+ """``#/components/schemas/Name`` emits the schema name."""
48
+
49
+ root = {
50
+ "components": {
51
+ "schemas": {
52
+ "Foo": {
53
+ "type": "object",
54
+ "properties": {"a": {"type": "integer"}},
55
+ }
56
+ }
57
+ }
58
+ }
59
+ out = json_schema_to_ts({"$ref": "#/components/schemas/Foo"}, root)
60
+ assert "a" in out
61
+ assert "number" in out
62
+
63
+
64
+ class TestJsonSchemaToTsEnum:
65
+ """Enum keyword."""
66
+
67
+ def test_string_enum(self) -> None:
68
+ """String ``enum`` becomes a union of string literals."""
69
+
70
+ out = json_schema_to_ts({"type": "string", "enum": ["a", "b"]})
71
+ assert '"a"' in out
72
+ assert '"b"' in out
73
+ assert " | " in out
74
+
75
+
76
+ class TestJsonSchemaToTsLogical:
77
+ """``oneOf`` / ``anyOf`` / ``allOf``."""
78
+
79
+ def test_one_of_primitives(self) -> None:
80
+ """``oneOf`` joins alternatives with ``|``."""
81
+
82
+ out = json_schema_to_ts(
83
+ {"oneOf": [{"type": "string"}, {"type": "integer"}]},
84
+ )
85
+ assert "string" in out
86
+ assert "number" in out
87
+
88
+
89
+ class TestJsonSchemaToTsSpecial:
90
+ """Boolean schema and empty shapes."""
91
+
92
+ def test_true_schema(self) -> None:
93
+ """JSON Schema ``true`` accepts anything → ``unknown``."""
94
+
95
+ assert json_schema_to_ts(True) == "unknown"
96
+
97
+ def test_false_schema(self) -> None:
98
+ """JSON Schema ``false`` is unsatisfiable → ``never``."""
99
+
100
+ assert json_schema_to_ts(False) == "never"
@@ -0,0 +1,58 @@
1
+ """Tests for ``exdrf_ts.mapping``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from exdrf_ts.mapping import model_rel_import, py_type_to_ts
6
+
7
+
8
+ def test_py_type_to_ts_primitives() -> None:
9
+ """Primitive names map to TS keywords."""
10
+
11
+ assert py_type_to_ts("str") == "string"
12
+ assert py_type_to_ts("int") == "number"
13
+ assert py_type_to_ts("float") == "number"
14
+ assert py_type_to_ts("bool") == "boolean"
15
+
16
+
17
+ def test_py_type_to_ts_containers_and_optional() -> None:
18
+ """List, optional, and dict string forms map as expected."""
19
+
20
+ assert py_type_to_ts("List[str]") == "string[]"
21
+ assert py_type_to_ts("List[int]") == "number[]"
22
+ assert py_type_to_ts("Optional[str]") == "string | undefined"
23
+ assert py_type_to_ts("Optional[List[int]]") == "number[] | undefined"
24
+ assert py_type_to_ts("Dict[str, int]") == "{ [key: string]: number }"
25
+ assert py_type_to_ts("Dict[int, float]") == "{ [key: number]: number }"
26
+
27
+
28
+ def test_py_type_to_ts_runtime_pep604_and_generics() -> None:
29
+ """Runtime ``type`` / union objects map like annotations."""
30
+
31
+ assert py_type_to_ts(str | None) == "string | undefined"
32
+ assert py_type_to_ts(list[int]) == "number[]"
33
+ assert py_type_to_ts(dict[str, int]) == "{ [key: string]: number }"
34
+
35
+
36
+ def test_model_rel_import_relative_paths() -> None:
37
+ """Category segments from ``__module__`` yield stable relative paths."""
38
+
39
+ def _model_with_category_segments(segments: list[str]) -> type:
40
+ """Build a type whose ``ExModelVisitor.category`` is ``segments``."""
41
+
42
+ class M:
43
+ pass
44
+
45
+ M.__module__ = "pkg." + ".".join(segments) + ".resource"
46
+ return M
47
+
48
+ model = _model_with_category_segments(["Api", "Users", "Profiles"])
49
+ ref = _model_with_category_segments(["Api"])
50
+ assert model_rel_import(model, ref) == "Users/Profiles"
51
+
52
+ model = _model_with_category_segments(["Api", "Users"])
53
+ ref = _model_with_category_segments(["Api", "Posts"])
54
+ assert model_rel_import(model, ref) == "../Users"
55
+
56
+ model = _model_with_category_segments(["Api", "Users"])
57
+ ref = _model_with_category_segments(["Api", "Users"])
58
+ assert model_rel_import(model, ref) == ""