Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: python API #10

Draft
wants to merge 73 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
46f8722
Addition of reference folder and schemapi reference files
Hamzu24 Sep 10, 2024
eb2682e
Created example-api-usage file
Hamzu24 Sep 14, 2024
de5fb5b
Moved example-api-usgae
Hamzu24 Sep 15, 2024
21acdda
Initial comments
Hamzu24 Sep 15, 2024
e5fb1c7
reference/generate_schema_wrapper_commented.py
Hamzu24 Sep 15, 2024
03ef6cf
Added first round of comments
Hamzu24 Sep 15, 2024
589140a
add comments
Xinyue-Yang Sep 16, 2024
aacac75
add comments (#1)
Xinyue-Yang Sep 16, 2024
4d8a839
Adding generate_mosaic_schema_wrapper.py
mhli1260 Sep 20, 2024
89b5c18
Creating generate_mosaic_schema_wrapper.py
mhli1260 Sep 20, 2024
8567d66
creating general outline for file
mhli1260 Sep 20, 2024
ebaa400
Merge branch 'main' of github.com:Hamzu24/mosaic
Hamzu24 Sep 23, 2024
248abdf
More comments
Hamzu24 Sep 24, 2024
3c2503d
Initial draft of the schema generator
Hamzu24 Sep 25, 2024
64704b4
adding recursive_dict_update and get_field_datum_value_defs
mhli1260 Sep 25, 2024
77b5954
Merge pull request #4 from mhli1260/patch-2
mhli1260 Sep 25, 2024
e481186
simple schema
Xinyue-Yang Sep 26, 2024
8f2e673
Merge branch 'main' of https://github.com/Hamzu24/mosaic
Xinyue-Yang Sep 26, 2024
80e06ea
Merge pull request #2 from mhli1260/main
mhli1260 Sep 26, 2024
d544c28
delete extra file
mhli1260 Sep 26, 2024
fb033ef
add part of utils
Xinyue-Yang Sep 26, 2024
7272f86
Merge branch 'main' of https://github.com/Hamzu24/mosaic
Xinyue-Yang Sep 26, 2024
3ede84d
anyOf and ref handles
mhli1260 Oct 2, 2024
bcb8861
deleting extra comments
mhli1260 Oct 2, 2024
ed36a29
Merge pull request #6 from Hamzu24/mia-branch
mhli1260 Oct 2, 2024
4b6586d
build package for generator
Xinyue-Yang Oct 8, 2024
aea01d4
Made the schema wrapper into a python package plus a few other features
Hamzu24 Oct 8, 2024
cb3d58e
add get_valid_identifier
Xinyue-Yang Oct 8, 2024
9f2fa00
Cleaned up the package
Hamzu24 Oct 8, 2024
ae585e2
Merge branch 'main' of github.com:Hamzu24/mosaic
mhli1260 Oct 9, 2024
86d148f
Moved functions to utils from main file
Hamzu24 Oct 9, 2024
5092821
implemented get_valid_identifiers
Hamzu24 Oct 9, 2024
066616f
Fixed error in ensuring valid identifiers and cleaned up
Hamzu24 Oct 9, 2024
7d98293
Final fix
Hamzu24 Oct 9, 2024
03172e5
Merge branch 'main' of github.com:Hamzu24/mosaic
mhli1260 Oct 9, 2024
d8451d8
changing path of output file generated_classes.py
mhli1260 Oct 9, 2024
29af85f
Moving the package to packages folder
Hamzu24 Oct 18, 2024
9d7acf9
Amended path to generated classes
Hamzu24 Oct 18, 2024
a94a495
Amended path to parent
Hamzu24 Oct 18, 2024
efdc54d
Merge branch 'main' of github.com:Hamzu24/mosaic
mhli1260 Oct 18, 2024
c516246
fix importing dict bug,delete legacy code
Xinyue-Yang Oct 18, 2024
9c87fa7
fix quoting around references
mhli1260 Oct 18, 2024
3f89e2c
Merge branch 'main' of github.com:Hamzu24/mosaic
mhli1260 Oct 18, 2024
c1dde0f
Added a list as a type which can be generated
Hamzu24 Oct 18, 2024
e2cff16
Removed a lot of redundancy
Hamzu24 Oct 18, 2024
8b8ea00
add test cases
Xinyue-Yang Oct 20, 2024
c877a78
Removed unnecessary comments
Hamzu24 Oct 22, 2024
d40d937
Merged others changes to my local ones
Hamzu24 Oct 22, 2024
231f6f6
Refined the typing system further
Hamzu24 Oct 22, 2024
4a2aae0
adding test cases
mhli1260 Oct 22, 2024
c5c33fc
Merge branch 'main' of github.com:Hamzu24/mosaic
mhli1260 Oct 22, 2024
9c0b1c0
Fixed negligence
Hamzu24 Oct 22, 2024
48eadc6
Merge branch 'main' of github.com:Hamzu24/mosaic
mhli1260 Oct 22, 2024
5a69500
Finalised the typing
Hamzu24 Oct 22, 2024
c04f379
fixing parameter type errors
mhli1260 Oct 23, 2024
af3ec06
fixing parameter type errors
mhli1260 Oct 23, 2024
c39832e
fixing parameter type errors
mhli1260 Oct 23, 2024
fe1125a
additional test cases
mhli1260 Oct 23, 2024
550e204
add pytest
Xinyue-Yang Oct 28, 2024
785a0f5
add automated testing in github workflow
Xinyue-Yang Oct 28, 2024
34272fe
create virtual environment
Xinyue-Yang Nov 5, 2024
58b7efc
to_dict function
Hamzu24 Nov 5, 2024
1e3b019
Merge branch 'main' of github.com:Hamzu24/mosaic
Hamzu24 Nov 5, 2024
eeb9d1a
can now create a spec usable by mosaic_widget
Hamzu24 Nov 6, 2024
441c55d
switched to hatch test framework
Xinyue-Yang Nov 6, 2024
cf221e3
Merge branch 'main' of https://github.com/Hamzu24/mosaic
Xinyue-Yang Nov 6, 2024
e0b2b94
switched to hatch testing framework
Xinyue-Yang Nov 6, 2024
4f6a09a
add testing widget plot
Xinyue-Yang Nov 12, 2024
31f7b9e
updating spec tests
mhli1260 Nov 12, 2024
4f0be93
Took changes from origin/main for tools/schemapi/schemapi.py
mhli1260 Nov 20, 2024
8613764
Resolved conflicts and kept .gitignore from upstream/main
mhli1260 Nov 20, 2024
91ea687
lint fixes
mhli1260 Nov 20, 2024
c6565db
delete tools
mhli1260 Nov 20, 2024
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
8 changes: 8 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ jobs:

- name: Format and lint
run: |
python -m pip install --upgrade pip
pip install hatch pytest pytest-cov
uv run ruff check
uv run ruff format --check

Expand All @@ -63,6 +65,12 @@ jobs:
uv run mypy
uv run --with pytest-cov pytest --cov-report=term-missing --color=yes --cov=pkg

- name: Build and test Schema Wrapper
run: |
cd packages/schema_wrapper
pip install -e .
pytest test/ --cov=schema_wrapper --cov-report=term-missing

rust:
name: Test in Rust

Expand Down
1 change: 1 addition & 0 deletions packages/schema_wrapper/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TODO
4 changes: 4 additions & 0 deletions packages/schema_wrapper/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .src.generate_schema_wrapper import generate_schema_wrapper
from .src.utils import jsonschema_to_python_types

__all__ = ['generate_schema_wrapper', 'jsonschema_to_python_types']
Binary file not shown.
Binary file not shown.
Binary file not shown.
5,441 changes: 5,441 additions & 0 deletions packages/schema_wrapper/generated_classes.py

Large diffs are not rendered by default.

59 changes: 59 additions & 0 deletions packages/schema_wrapper/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "schema-wrapper"
version = "0.1.0"
description = "Schema wrapper classes for Mosaic"
readme = "README.md"
requires-python = ">=3.9"
dependencies = [
"pyyaml",
]

# Add development dependencies
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"pytest-cov>=4.0",
]

[tool.hatch.envs.default]
python = "3.11"
features = ["dev"]
installer = "uv"

[tool.hatch.envs.test]
installer = "uv"
dependencies = [
"coverage[toml]",
"pytest",
"pytest-cov",
]

[tool.hatch.envs.test.scripts]
cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=."

[tool.hatch.build.targets.wheel]
packages = ["."]

[tool.pytest.ini_options]
testpaths = ["test"]
python_files = ["test_*.py"]
addopts = "-v"

[tool.coverage.run]
source = ["."]

[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"if __name__ == .__main__.:",
"raise NotImplementedError",
]

[tool.ruff]
line-length = 88
target-version = "py39"
Empty file.
Binary file not shown.
Binary file not shown.
200 changes: 200 additions & 0 deletions packages/schema_wrapper/src/generate_schema_wrapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import json
from typing import Any, Dict, List, Final
import sys
import argparse
from pathlib import Path
from urllib import request
import graphlib
from .utils import get_valid_identifier, get_dependencies

sys.path.insert(0, str(Path.cwd()))

SCHEMA_VERSION: Final = "v0.10.0"
SCHEMA_URL_TEMPLATE: Final = "https://raw.githubusercontent.com/uwdata/mosaic/refs/heads/main/docs/public/schema/{version}.json"
KNOWN_PRIMITIVES = {"string": "str", "boolean": "bool", "number": "float", "object": "Dict[str, Any]"}

def schema_url(version: str = SCHEMA_VERSION) -> str:
return SCHEMA_URL_TEMPLATE.format(version=version)

def download_schemafile(
version: str, schemapath: Path, download: bool = False
) -> Path:
url = schema_url(version=version)
if download:
request.urlretrieve(url, schemapath)
elif not schemapath.exists():
msg = f"Cannot skip download: {schemapath!s} does not exist"
raise ValueError(msg)
return schemapath

def generate_class(class_name: str, class_schema: Dict[str, Any]) -> str:
class_name = get_valid_identifier(class_name)

# Check if the schema defines a simple type (like string, number) without properties
if 'type' in class_schema and 'properties' not in class_schema:
return f"class {class_name}:\n def __init__(self):\n pass\n"

# Check for '$ref' and handle it
if '$ref' in class_schema:
ref_class_name = class_schema['$ref'].split('/')[-1]
return f"\nclass {class_name}:\n pass # This is a reference to '{ref_class_name}'\n"
if 'anyOf' in class_schema:
return generate_any_of_class(class_name, class_schema['anyOf'])

# Extract properties and required fields
properties = class_schema.get('properties', {})
required = class_schema.get('required', [])

class_def = f"class {class_name}:\n"
class_def += " def __init__(self"

# Generate __init__ method parameters
optional_params = []

# Ensuring all the property names are valid Python identifiers
valid_properties = {}
for prop, prop_schema in properties.items():
valid_prop = get_valid_identifier(prop)
valid_properties[valid_prop] = prop_schema

for prop, prop_schema in valid_properties.items():
if 'anyOf' in prop_schema:
# Handle anyOf case
type_hint = f"Union[{', '.join(get_type_hint(item) for item in prop_schema['anyOf'])}]"
else:
type_hint = get_type_hint(prop_schema)

if prop in required:
# Required parameters should not have default values
class_def += f", {prop}: {type_hint}"
else:
# Ensure we add optional parameters last
optional_params.append((prop, type_hint))

for prop, type_hint in optional_params:
class_def += f", {prop}: {type_hint} = None"

class_def += "):\n"

# Generate attribute assignments in __init__
for prop in valid_properties:
class_def += f" self.{prop} = {prop}\n"

return class_def

def get_type_union(types: List[str]):
unique_types = list(set(types))
if len(unique_types) == 1:
return unique_types[0]

# Moving the potential "Any" to the end of the list
if "Any" in unique_types:
unique_types.remove("Any")
unique_types.append("Any")
return f'Union[{", ".join(unique_types)}]'

def generate_any_of_class(class_name: str, any_of_schemas: List[Dict[str, Any]]) -> str:
types = [get_type_hint(schema) for schema in any_of_schemas]
type_union = get_type_union(types)

class_def = f"class {class_name}:\n"
class_def += f" def __init__(self, value: {type_union}):\n"
class_def += " self.value = value\n"

return class_def

def get_type_hint(type_schema: Dict[str, Any]) -> str:
"""Get type hint for a property schema."""
if 'items' in type_schema:
assert type_schema['type'] == 'array'

items_schema = type_schema['items']
# items_schema contains the types which are stored in the list

datatype = get_type_hint(items_schema)
return f"List[{datatype}]"

if 'type' in type_schema:
if isinstance(type_schema['type'], list):
types = []
for t in type_schema['type']:
datatype = KNOWN_PRIMITIVES.get(t)
if datatype == None:
types.append('Any')
else:
types.append(datatype)

return get_type_union(types)
else:
datatype = KNOWN_PRIMITIVES.get(type_schema['type'])
if datatype == None:
return 'Any'
return datatype
elif 'anyOf' in type_schema:
types = [get_type_hint(option) for option in type_schema['anyOf']]
return get_type_union(types)
elif '$ref' in type_schema:
ref_class_name = type_schema['$ref'].split('/')[-1]
return f'"{ref_class_name}"'
return 'Any'

def load_schema(schema_path: Path) -> dict:
"""Load a JSON schema from the specified path."""
with schema_path.open(encoding="utf8") as f:
return json.load(f)

def generate_schema_wrapper(schema_file: Path, output_file: Path) -> str:
"""Generate a schema wrapper for the given schema file."""
rootschema = load_schema(schema_file)

rootschema_definitions = rootschema.get("definitions", {})
ts = graphlib.TopologicalSorter()

for name, schema in rootschema_definitions.items():
dependencies = get_dependencies(schema)
if dependencies:
ts.add(name, *dependencies)
else:
ts.add(name)

class_order = list(ts.static_order())

definitions: Dict[str, str] = {}

for name in class_order:
schema = rootschema_definitions.get(name)
class_code = generate_class(name, schema)
definitions[name] = class_code

generated_classes = "\n\n".join(definitions.values())
generated_classes = "from typing import List, Dict, Any, Union\n\n" + generated_classes


with open(output_file, 'w') as f:
f.write(generated_classes)

def main():
parser = argparse.ArgumentParser(
prog="our_schema_generator", description="Generate the JSON schema for mosaic apps"
)
parser.add_argument("schema_file", help="Path to the JSON schema file")

parser.add_argument(
"--download", action="store_true", help="download the schema"
)
args = parser.parse_args()

#vn = '.'.join(version.split(".")[:1]) #Not using this currently
schemapath = Path(args.schema_file).resolve()
schemapath = download_schemafile(
version=SCHEMA_VERSION,
schemapath=schemapath,
download = args.download
)

output_file = Path("packages/schema_wrapper/generated_classes.py")
generate_schema_wrapper(schemapath, output_file)

# Main execution
if __name__ == "__main__":
main()
Loading
Loading