Skip to content

Commit 5c62a5c

Browse files
authored
Merge pull request #8 from KvizadSaderah/feat/yaml-typespec-support
Feat/yaml typespec support
2 parents b1ff09d + bddf7e0 commit 5c62a5c

9 files changed

Lines changed: 136 additions & 80 deletions

File tree

gts/README.md

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,48 @@
11
# GTS Python Library
22

3-
A minimal, idiomatic Python library for working with **GTS** ([Global Type System](https://github.com/gts-spec/gts-spec)) identifiers and JSON/JSON Schema artifacts.
3+
A minimal, idiomatic Python library for working with **GTS** ([Global Type System](https://github.com/gts-spec/gts-spec)) identifiers and type definitions.
4+
5+
## File Format Support
6+
7+
GTS Python supports multiple file formats for schemas and instances:
8+
9+
### JSON (Native)
10+
Standard JSON format with `.json`, `.jsonc`, and `.gts` extensions.
11+
12+
### YAML
13+
Full YAML support with `.yaml` and `.yml` extensions. YAML files are automatically parsed and treated identically to JSON.
14+
15+
```python
16+
from gts import GtsFileReader
17+
18+
# Reads both JSON and YAML files
19+
reader = GtsFileReader("path/to/schemas/")
20+
for entity in reader:
21+
print(f"{entity.gts_id.id}: {entity.file.name}")
22+
```
23+
24+
### TypeSpec
25+
TypeSpec (`.tsp`) schemas must be pre-compiled to JSON Schema before use with gts-python.
26+
27+
**Setup:**
28+
```bash
29+
# Install TypeSpec compiler
30+
npm install -g @typespec/compiler @typespec/json-schema
31+
32+
# Compile TypeSpec to JSON Schema
33+
tsp compile --emit @typespec/json-schema your-schemas/
34+
```
35+
36+
**Usage:**
37+
```python
38+
from gts import GtsFileReader
39+
40+
# Point to the generated JSON Schema output directory
41+
reader = GtsFileReader("tsp-output/@typespec/json-schema/")
42+
entities = list(reader)
43+
```
44+
45+
See [gts-spec TypeSpec examples](https://github.com/globaltypesystem/gts-spec/tree/main/examples/typespec) for sample TypeSpec definitions.
446

547
## Featureset
648

@@ -23,10 +65,10 @@ print(is_valid) # True or False
2365

2466
```python
2567
import json
26-
from gts import JsonEntity, DEFAULT_GTS_CONFIG
68+
from gts import GtsEntity, DEFAULT_GTS_CONFIG
2769

2870
content = json.load(open("path/to/file.json"))
29-
entity = JsonEntity(content=content, cfg=DEFAULT_GTS_CONFIG)
71+
entity = GtsEntity(content=content, cfg=DEFAULT_GTS_CONFIG)
3072
if entity.gts_id:
3173
print(entity.gts_id.id)
3274
```

gts/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ requires-python = ">=3.9"
1313
dependencies = [
1414
"jsonschema>=4.18,<5",
1515
"fastapi>=0.110,<1",
16-
"uvicorn>=0.23,<1"
16+
"uvicorn>=0.23,<1",
17+
"pyyaml>=6.0,<7"
1718
]
1819

1920
[project.urls]

gts/src/gts/__init__.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
from .entities import (
77
ValidationError,
88
ValidationResult,
9-
JsonFile,
10-
JsonEntity,
9+
GtsFile,
10+
GtsEntity,
1111
GtsConfig,
1212
DEFAULT_GTS_CONFIG,
1313
)
14-
from .path_resolver import JsonPathResolver
14+
from .path_resolver import GtsPathResolver
1515
from .store import (
1616
GtsReader,
1717
GtsStore,
@@ -26,12 +26,21 @@
2626
"GtsWildcard",
2727
"ValidationError",
2828
"ValidationResult",
29-
"JsonFile",
30-
"JsonEntity",
31-
"JsonPathResolver",
29+
"GtsFile",
30+
"GtsEntity",
31+
"GtsPathResolver",
3232
"GtsConfig",
3333
"DEFAULT_GTS_CONFIG",
3434
"GtsReader",
3535
"GtsStore",
3636
"GtsFileReader",
37+
# Backward compatibility aliases
38+
"JsonFile",
39+
"JsonEntity",
40+
"JsonPathResolver",
3741
]
42+
43+
# Backward compatibility aliases
44+
JsonFile = GtsFile
45+
JsonEntity = GtsEntity
46+
JsonPathResolver = GtsPathResolver

gts/src/gts/entities.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
from typing import Any, Dict, List, Optional, Tuple
55

66
from .gts import GtsID
7-
from .path_resolver import JsonPathResolver
8-
from .schema_cast import JsonEntityCastResult, SchemaCastError
7+
from .path_resolver import GtsPathResolver
8+
from .schema_cast import GtsEntityCastResult, SchemaCastError
99

1010

1111
@dataclass
@@ -24,7 +24,7 @@ class ValidationResult:
2424

2525

2626
@dataclass
27-
class JsonFile:
27+
class GtsFile:
2828
path: str
2929
name: str
3030
content: Any
@@ -74,10 +74,10 @@ class GtsConfig:
7474

7575

7676
@dataclass
77-
class JsonEntity:
77+
class GtsEntity:
7878
gts_id: Optional[GtsID] = None
7979
is_schema: bool = False
80-
file: Optional[JsonFile] = None
80+
file: Optional[GtsFile] = None
8181
list_sequence: Optional[int] = None
8282
label: str = ""
8383
content: Any = None
@@ -92,7 +92,7 @@ class JsonEntity:
9292
def __init__(
9393
self,
9494
*,
95-
file: Optional[JsonFile] = None,
95+
file: Optional[GtsFile] = None,
9696
list_sequence: Optional[int] = None,
9797
content: Any = None,
9898
cfg: Optional[GtsConfig] = None,
@@ -168,11 +168,11 @@ def _is_json_schema_entity(self) -> bool:
168168
return True
169169
return False
170170

171-
def resolve_path(self, path: str) -> JsonPathResolver:
172-
resolver = JsonPathResolver(self.gts_id.id if self.gts_id else '', self.content)
171+
def resolve_path(self, path: str) -> GtsPathResolver:
172+
resolver = GtsPathResolver(self.gts_id.id if self.gts_id else '', self.content)
173173
return resolver.resolve(path)
174174

175-
def cast(self, to_schema: JsonEntity, from_schema: JsonEntity, resolver: Optional[Any] = None) -> JsonEntityCastResult:
175+
def cast(self, to_schema: GtsEntity, from_schema: GtsEntity, resolver: Optional[Any] = None) -> GtsEntityCastResult:
176176
if self.is_schema:
177177
# When casting a schema, from_schema might be a standard JSON Schema (no gts_id)
178178
# In that case, skip the sanity check
@@ -182,7 +182,7 @@ def cast(self, to_schema: JsonEntity, from_schema: JsonEntity, resolver: Optiona
182182
raise SchemaCastError("Target must be a schema")
183183
if not from_schema.is_schema:
184184
raise SchemaCastError("Source schema must be a schema")
185-
return JsonEntityCastResult.cast(
185+
return GtsEntityCastResult.cast(
186186
self.gts_id.id,
187187
to_schema.gts_id.id,
188188
self.content,

gts/src/gts/files_reader.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from __future__ import annotations
22

33
import json
4+
import yaml
45
from pathlib import Path
56
import os
67
from typing import Iterator, List, Optional, Any
78

89
from .store import GtsReader
9-
from .entities import JsonEntity, JsonFile, DEFAULT_GTS_CONFIG, GtsConfig
10+
from .entities import GtsEntity, GtsFile, DEFAULT_GTS_CONFIG, GtsConfig
1011

1112
import logging
1213

@@ -15,7 +16,7 @@
1516

1617

1718
class GtsFileReader(GtsReader):
18-
"""Reads JSON entities from files and directories specified by path."""
19+
"""Reads GTS entities from JSON and YAML files in directories specified by path."""
1920

2021
def __init__(self, path: str | List[str], cfg: Optional[GtsConfig] = None) -> None:
2122
"""
@@ -34,13 +35,13 @@ def __init__(self, path: str | List[str], cfg: Optional[GtsConfig] = None) -> No
3435
self.cfg = cfg or DEFAULT_GTS_CONFIG
3536
self._files: List[Path] = []
3637
self._current_index = 0
37-
self._current_file_entities: List[JsonEntity] = []
38+
self._current_file_entities: List[GtsEntity] = []
3839
self._current_entity_index = 0
3940
self._initialized = False
4041

4142
def _collect_files(self) -> None:
42-
"""Collect all JSON files from the specified paths, following symlinks."""
43-
valid_extensions = {'.json', '.jsonc', '.gts'}
43+
"""Collect all JSON and YAML files from the specified paths, following symlinks."""
44+
valid_extensions = {'.json', '.jsonc', '.gts', '.yaml', '.yml'}
4445
seen: set[str] = set()
4546
collected: List[Path] = []
4647

@@ -73,18 +74,21 @@ def _collect_files(self) -> None:
7374

7475
self._files = collected
7576

76-
def _load_json_file(self, file_path: Path) -> Any:
77-
"""Load JSON content from a file."""
77+
def _load_file(self, file_path: Path) -> Any:
78+
"""Load content from JSON or YAML file."""
7879
with file_path.open("r", encoding="utf-8") as f:
79-
return json.load(f)
80+
if file_path.suffix.lower() in {'.yaml', '.yml'}:
81+
return yaml.safe_load(f)
82+
else:
83+
return json.load(f)
8084

81-
def _process_file(self, file_path: Path) -> List[JsonEntity]:
82-
"""Process a single JSON file and return list of JsonEntity objects."""
83-
entities: List[JsonEntity] = []
85+
def _process_file(self, file_path: Path) -> List[GtsEntity]:
86+
"""Process a single JSON or YAML file and return list of GtsEntity objects."""
87+
entities: List[GtsEntity] = []
8488

8589
try:
86-
content = self._load_json_file(file_path)
87-
json_file = JsonFile(
90+
content = self._load_file(file_path)
91+
json_file = GtsFile(
8892
path=str(file_path),
8993
name=file_path.name,
9094
content=content
@@ -93,7 +97,7 @@ def _process_file(self, file_path: Path) -> List[JsonEntity]:
9397
# Handle both single objects and arrays
9498
if isinstance(content, list):
9599
for idx, item in enumerate(content):
96-
entity = JsonEntity(
100+
entity = GtsEntity(
97101
file=json_file,
98102
list_sequence=idx,
99103
content=item,
@@ -103,7 +107,7 @@ def _process_file(self, file_path: Path) -> List[JsonEntity]:
103107
logging.debug(f"- discovered entity: {entity.gts_id.id}")
104108
entities.append(entity)
105109
else:
106-
entity = JsonEntity(
110+
entity = GtsEntity(
107111
file=json_file,
108112
list_sequence=None,
109113
content=content,
@@ -118,8 +122,8 @@ def _process_file(self, file_path: Path) -> List[JsonEntity]:
118122

119123
return entities
120124

121-
def __iter__(self) -> Iterator[JsonEntity]:
122-
"""Iterate over all JsonEntity objects from all files."""
125+
def __iter__(self) -> Iterator[GtsEntity]:
126+
"""Iterate over all GtsEntity objects from all files."""
123127
if not self._initialized:
124128
self._collect_files()
125129
self._initialized = True
@@ -130,9 +134,9 @@ def __iter__(self) -> Iterator[JsonEntity]:
130134
for entity in entities:
131135
yield entity
132136

133-
def read_by_id(self, entity_id: str) -> Optional[JsonEntity]:
137+
def read_by_id(self, entity_id: str) -> Optional[GtsEntity]:
134138
"""
135-
Read a JsonEntity by its ID.
139+
Read a GtsEntity by its ID.
136140
For FileReader, this returns None as we don't support random access by ID.
137141
"""
138142
return None

gts/src/gts/ops.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
from pathlib import Path as SysPath
88

99
from .gts import GtsID, GtsWildcard
10-
from .entities import DEFAULT_GTS_CONFIG, GtsConfig, JsonEntity
10+
from .entities import DEFAULT_GTS_CONFIG, GtsConfig, GtsEntity
1111
from .files_reader import GtsFileReader
12-
from .path_resolver import JsonPathResolver
12+
from .path_resolver import GtsPathResolver
1313
from .store import GtsStore, GtsStoreQueryResult
14-
from .schema_cast import JsonEntityCastResult
14+
from .schema_cast import GtsEntityCastResult
1515

1616
# Interface helpers
1717

gts/src/gts/path_resolver.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66

77
@dataclass
8-
class JsonPathResolver:
8+
class GtsPathResolver:
99
gts_id: str
1010
content: Any
1111
path: str = ""
@@ -70,7 +70,7 @@ def _collect_from(self, node: Any) -> List[str]:
7070
self._list_available(node, '', acc)
7171
return acc
7272

73-
def resolve(self, path: str) -> JsonPathResolver:
73+
def resolve(self, path: str) -> GtsPathResolver:
7474
self.path = path
7575
self.value = None
7676
self.resolved = False
@@ -119,7 +119,7 @@ def resolve(self, path: str) -> JsonPathResolver:
119119
self.resolved = True
120120
return self
121121

122-
def failure(self, path: str, error: str) -> JsonPathResolver:
122+
def failure(self, path: str, error: str) -> GtsPathResolver:
123123
self.path = path
124124
self.value = None
125125
self.resolved = False

0 commit comments

Comments
 (0)