Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 45 additions & 3 deletions gts/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,48 @@
# GTS Python Library

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.
A minimal, idiomatic Python library for working with **GTS** ([Global Type System](https://github.com/gts-spec/gts-spec)) identifiers and type definitions.

## File Format Support

GTS Python supports multiple file formats for schemas and instances:

### JSON (Native)
Standard JSON format with `.json`, `.jsonc`, and `.gts` extensions.
Comment thread
Artifizer marked this conversation as resolved.

### YAML
Full YAML support with `.yaml` and `.yml` extensions. YAML files are automatically parsed and treated identically to JSON.

```python
from gts import GtsFileReader

# Reads both JSON and YAML files
reader = GtsFileReader("path/to/schemas/")
for entity in reader:
print(f"{entity.gts_id.id}: {entity.file.name}")
```

### TypeSpec
TypeSpec (`.tsp`) schemas must be pre-compiled to JSON Schema before use with gts-python.

**Setup:**
```bash
# Install TypeSpec compiler
npm install -g @typespec/compiler @typespec/json-schema

# Compile TypeSpec to JSON Schema
tsp compile --emit @typespec/json-schema your-schemas/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd like to have *.tsp files support right in the _load_file() and avoid this pre-processing. Could you please add it as separate PR?

```

**Usage:**
```python
from gts import GtsFileReader

# Point to the generated JSON Schema output directory
reader = GtsFileReader("tsp-output/@typespec/json-schema/")
entities = list(reader)
```

See [gts-spec TypeSpec examples](https://github.com/globaltypesystem/gts-spec/tree/main/examples/typespec) for sample TypeSpec definitions.

## Featureset

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

```python
import json
from gts import JsonEntity, DEFAULT_GTS_CONFIG
from gts import GtsEntity, DEFAULT_GTS_CONFIG

content = json.load(open("path/to/file.json"))
entity = JsonEntity(content=content, cfg=DEFAULT_GTS_CONFIG)
entity = GtsEntity(content=content, cfg=DEFAULT_GTS_CONFIG)
if entity.gts_id:
print(entity.gts_id.id)
```
Expand Down
3 changes: 2 additions & 1 deletion gts/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ requires-python = ">=3.9"
dependencies = [
"jsonschema>=4.18,<5",
"fastapi>=0.110,<1",
"uvicorn>=0.23,<1"
"uvicorn>=0.23,<1",
"pyyaml>=6.0,<7"
]

[project.urls]
Expand Down
21 changes: 15 additions & 6 deletions gts/src/gts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
from .entities import (
ValidationError,
ValidationResult,
JsonFile,
JsonEntity,
GtsFile,
GtsEntity,
GtsConfig,
DEFAULT_GTS_CONFIG,
)
from .path_resolver import JsonPathResolver
from .path_resolver import GtsPathResolver
from .store import (
GtsReader,
GtsStore,
Expand All @@ -26,12 +26,21 @@
"GtsWildcard",
"ValidationError",
"ValidationResult",
"JsonFile",
"JsonEntity",
"JsonPathResolver",
"GtsFile",
"GtsEntity",
"GtsPathResolver",
"GtsConfig",
"DEFAULT_GTS_CONFIG",
"GtsReader",
"GtsStore",
"GtsFileReader",
# Backward compatibility aliases
"JsonFile",
"JsonEntity",
"JsonPathResolver",
]

# Backward compatibility aliases
JsonFile = GtsFile
JsonEntity = GtsEntity
JsonPathResolver = GtsPathResolver
20 changes: 10 additions & 10 deletions gts/src/gts/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
from typing import Any, Dict, List, Optional, Tuple

from .gts import GtsID
from .path_resolver import JsonPathResolver
from .schema_cast import JsonEntityCastResult, SchemaCastError
from .path_resolver import GtsPathResolver
from .schema_cast import GtsEntityCastResult, SchemaCastError


@dataclass
Expand All @@ -24,7 +24,7 @@ class ValidationResult:


@dataclass
class JsonFile:
class GtsFile:
path: str
name: str
content: Any
Expand Down Expand Up @@ -74,10 +74,10 @@ class GtsConfig:


@dataclass
class JsonEntity:
class GtsEntity:
gts_id: Optional[GtsID] = None
is_schema: bool = False
file: Optional[JsonFile] = None
file: Optional[GtsFile] = None
list_sequence: Optional[int] = None
label: str = ""
content: Any = None
Expand All @@ -92,7 +92,7 @@ class JsonEntity:
def __init__(
self,
*,
file: Optional[JsonFile] = None,
file: Optional[GtsFile] = None,
list_sequence: Optional[int] = None,
content: Any = None,
cfg: Optional[GtsConfig] = None,
Expand Down Expand Up @@ -168,11 +168,11 @@ def _is_json_schema_entity(self) -> bool:
return True
return False

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

def cast(self, to_schema: JsonEntity, from_schema: JsonEntity, resolver: Optional[Any] = None) -> JsonEntityCastResult:
def cast(self, to_schema: GtsEntity, from_schema: GtsEntity, resolver: Optional[Any] = None) -> GtsEntityCastResult:
if self.is_schema:
# When casting a schema, from_schema might be a standard JSON Schema (no gts_id)
# In that case, skip the sanity check
Expand All @@ -182,7 +182,7 @@ def cast(self, to_schema: JsonEntity, from_schema: JsonEntity, resolver: Optiona
raise SchemaCastError("Target must be a schema")
if not from_schema.is_schema:
raise SchemaCastError("Source schema must be a schema")
return JsonEntityCastResult.cast(
return GtsEntityCastResult.cast(
self.gts_id.id,
to_schema.gts_id.id,
self.content,
Expand Down
42 changes: 23 additions & 19 deletions gts/src/gts/files_reader.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from __future__ import annotations

import json
import yaml
from pathlib import Path
import os
from typing import Iterator, List, Optional, Any

from .store import GtsReader
from .entities import JsonEntity, JsonFile, DEFAULT_GTS_CONFIG, GtsConfig
from .entities import GtsEntity, GtsFile, DEFAULT_GTS_CONFIG, GtsConfig

import logging

Expand All @@ -15,7 +16,7 @@


class GtsFileReader(GtsReader):
"""Reads JSON entities from files and directories specified by path."""
"""Reads GTS entities from JSON and YAML files in directories specified by path."""

def __init__(self, path: str | List[str], cfg: Optional[GtsConfig] = None) -> None:
"""
Expand All @@ -34,13 +35,13 @@ def __init__(self, path: str | List[str], cfg: Optional[GtsConfig] = None) -> No
self.cfg = cfg or DEFAULT_GTS_CONFIG
self._files: List[Path] = []
self._current_index = 0
self._current_file_entities: List[JsonEntity] = []
self._current_file_entities: List[GtsEntity] = []
self._current_entity_index = 0
self._initialized = False

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

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

self._files = collected

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

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

try:
content = self._load_json_file(file_path)
json_file = JsonFile(
content = self._load_file(file_path)
json_file = GtsFile(
path=str(file_path),
name=file_path.name,
content=content
Expand All @@ -93,7 +97,7 @@ def _process_file(self, file_path: Path) -> List[JsonEntity]:
# Handle both single objects and arrays
if isinstance(content, list):
for idx, item in enumerate(content):
entity = JsonEntity(
entity = GtsEntity(
file=json_file,
list_sequence=idx,
content=item,
Expand All @@ -103,7 +107,7 @@ def _process_file(self, file_path: Path) -> List[JsonEntity]:
logging.debug(f"- discovered entity: {entity.gts_id.id}")
entities.append(entity)
else:
entity = JsonEntity(
entity = GtsEntity(
file=json_file,
list_sequence=None,
content=content,
Expand All @@ -118,8 +122,8 @@ def _process_file(self, file_path: Path) -> List[JsonEntity]:

return entities

def __iter__(self) -> Iterator[JsonEntity]:
"""Iterate over all JsonEntity objects from all files."""
def __iter__(self) -> Iterator[GtsEntity]:
"""Iterate over all GtsEntity objects from all files."""
if not self._initialized:
self._collect_files()
self._initialized = True
Expand All @@ -130,9 +134,9 @@ def __iter__(self) -> Iterator[JsonEntity]:
for entity in entities:
yield entity

def read_by_id(self, entity_id: str) -> Optional[JsonEntity]:
def read_by_id(self, entity_id: str) -> Optional[GtsEntity]:
"""
Read a JsonEntity by its ID.
Read a GtsEntity by its ID.
For FileReader, this returns None as we don't support random access by ID.
"""
return None
Expand Down
6 changes: 3 additions & 3 deletions gts/src/gts/ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
from pathlib import Path as SysPath

from .gts import GtsID, GtsWildcard
from .entities import DEFAULT_GTS_CONFIG, GtsConfig, JsonEntity
from .entities import DEFAULT_GTS_CONFIG, GtsConfig, GtsEntity
from .files_reader import GtsFileReader
from .path_resolver import JsonPathResolver
from .path_resolver import GtsPathResolver
from .store import GtsStore, GtsStoreQueryResult
from .schema_cast import JsonEntityCastResult
from .schema_cast import GtsEntityCastResult

# Interface helpers

Expand Down
6 changes: 3 additions & 3 deletions gts/src/gts/path_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@


@dataclass
class JsonPathResolver:
class GtsPathResolver:
gts_id: str
content: Any
path: str = ""
Expand Down Expand Up @@ -70,7 +70,7 @@ def _collect_from(self, node: Any) -> List[str]:
self._list_available(node, '', acc)
return acc

def resolve(self, path: str) -> JsonPathResolver:
def resolve(self, path: str) -> GtsPathResolver:
self.path = path
self.value = None
self.resolved = False
Expand Down Expand Up @@ -119,7 +119,7 @@ def resolve(self, path: str) -> JsonPathResolver:
self.resolved = True
return self

def failure(self, path: str, error: str) -> JsonPathResolver:
def failure(self, path: str, error: str) -> GtsPathResolver:
self.path = path
self.value = None
self.resolved = False
Expand Down
Loading