pax_global_header00006660000000000000000000000064135461720220014514gustar00rootroot0000000000000052 comment=85b16a0f9d51de8373cbd1db40bd9ea9f0fd2fab jschema-to-python-1.2.3/000077500000000000000000000000001354617202200150705ustar00rootroot00000000000000jschema-to-python-1.2.3/.gitignore000066400000000000000000000023161354617202200170620ustar00rootroot00000000000000.vscode/ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ AUTHORS ChangeLog downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ jschema-to-python-1.2.3/CODE_OF_CONDUCT.md000066400000000000000000000007051354617202200176710ustar00rootroot00000000000000# Microsoft Open Source Code of Conduct This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). Resources: - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns jschema-to-python-1.2.3/LICENSE000066400000000000000000000022121354617202200160720ustar00rootroot00000000000000 MIT License Copyright (c) Microsoft Corporation. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE jschema-to-python-1.2.3/README.rst000066400000000000000000000044771354617202200165730ustar00rootroot00000000000000jschema-to-python ================= Generate Python classes from a JSON schema. Usage ===== :: python -m jschema_to_python [-h] -s SCHEMA_PATH -o OUTPUT_DIRECTORY [-m MODULE_NAME] -r ROOT_CLASS_NAME [-g HINTS_FILE_PATH] [-f] [-v] Generate source code for a set of Python classes from a JSON schema. optional arguments: -h, --help show this help message and exit -s SCHEMA_PATH, --schema-path SCHEMA_PATH path to the JSON schema file -o OUTPUT_DIRECTORY, --output-directory OUTPUT_DIRECTORY directory in which the generated classes will be created -m MODULE_NAME, --module-name MODULE_NAME name of the module containing the object model classes -r ROOT_CLASS_NAME, --root-class-name ROOT_CLASS_NAME the name of the class at the root of the object model represented by the schema -g HINTS_FILE_PATH, --hints-file-path HINTS_FILE_PATH path to a file containing hints that control code generation -f, --force overwrite the output directory if it exists -v, --verbose increase output verbosity (may be specified up to two times) Contributing ============ This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. This project has adopted the `Microsoft Open Source Code of Conduct `_. For more information see the `Code of Conduct FAQ `_ or contact `opencode@microsoft.com `_ with any additional questions or comments. jschema-to-python-1.2.3/SECURITY.md000066400000000000000000000053721354617202200166700ustar00rootroot00000000000000 ## Security Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [many more](https://opensource.microsoft.com/). If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [definition](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below. ## Reporting Security Issues **Please do not report security vulnerabilities through public GitHub issues.** Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) * Full paths of source file(s) related to the manifestation of the issue * The location of the affected source code (tag/branch/commit or direct URL) * Any special configuration required to reproduce the issue * Step-by-step instructions to reproduce the issue * Proof-of-concept or exploit code (if possible) * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. ## Preferred Languages We prefer all communications to be in English. ## Policy Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). jschema-to-python-1.2.3/jschema_to_python/000077500000000000000000000000001354617202200206055ustar00rootroot00000000000000jschema-to-python-1.2.3/jschema_to_python/__init__.py000066400000000000000000000001731354617202200227170ustar00rootroot00000000000000import pbr from pbr.version import VersionInfo __version__ = VersionInfo(__package__).semantic_version().release_string() jschema-to-python-1.2.3/jschema_to_python/__main__.py000066400000000000000000000004231354617202200226760ustar00rootroot00000000000000from __future__ import absolute_import import os import sys if not __package__: path = os.path.dirname(os.path.dirname(__file__)) sys.path.insert(0, path) from jschema_to_python.driver import main as _main # noqa if __name__ == "__main__": sys.exit(_main()) jschema-to-python-1.2.3/jschema_to_python/class_generator.py000066400000000000000000000117771354617202200243470ustar00rootroot00000000000000import sys from jschema_to_python.python_file_generator import PythonFileGenerator import jschema_to_python.utilities as util class ClassGenerator(PythonFileGenerator): def __init__(self, class_schema, class_name, code_gen_hints, output_directory): super(ClassGenerator, self).__init__(output_directory) self.class_schema = class_schema self.required_property_names = class_schema.get("required") if self.required_property_names: self.required_property_names.sort() self.class_name = class_name self.code_gen_hints = code_gen_hints self.file_path = self.make_class_file_path() def __del__(self): sys.stdout = sys.__stdout__ def generate(self): with open(self.file_path, "w") as sys.stdout: self.write_generation_comment() self.write_class_declaration() self.write_class_description() self.write_class_body() def make_class_file_path(self): class_module_name = util.class_name_to_private_module_name(self.class_name) return self.make_output_file_path(class_module_name + ".py") def write_class_declaration(self): print("import attr") print("") print("") # The black formatter wants two blank lines here. print("@attr.s") print("class " + self.class_name + "(object):") def write_class_description(self): description = self.class_schema.get("description") if description: print(' """' + description + '"""') print("") # The black formatter wants a blank line here. def write_class_body(self): property_schemas = self.class_schema["properties"] if not property_schemas: print(" pass") return schema_property_names = sorted(property_schemas.keys()) # attrs requires that mandatory attributes be declared before optional # attributes. if self.required_property_names: for schema_property_name in self.required_property_names: attrib = self.make_attrib(schema_property_name) print(attrib) for schema_property_name in schema_property_names: if self.is_optional(schema_property_name): attrib = self.make_attrib(schema_property_name) print(attrib) def make_attrib(self, schema_property_name): python_property_name = self.make_python_property_name_from_schema_property_name( schema_property_name ) attrib = "".join([" ", python_property_name, " = attr.ib("]) if self.is_optional(schema_property_name): property_schema = self.class_schema["properties"][schema_property_name] default_setter = self.make_default_setter(property_schema) attrib = "".join([attrib, default_setter, ", "]) attrib = "".join([attrib, "metadata={\"schema_property_name\": \"", schema_property_name, "\"})"]) return attrib def is_optional(self, schema_property_name): return ( not self.required_property_names or schema_property_name not in self.required_property_names ) def make_default_setter(self, property_schema): initializer = self.make_initializer(property_schema) return "default=" + str(initializer) def make_initializer(self, property_schema): default = property_schema.get("default") if default: type = property_schema.get("type") if type: if type == "string": default = ( '"' + default + '"' ) # The black formatter wants double-quotes. elif type == "array": # It isn't safe to specify a mutable object as a default value, # because all new instances share the same mutable object, and # one of them might mutate it, affecting all future instances! # attr.Factory creates a new value for each instance. default="attr.Factory(lambda: " + str(default) + ")" elif property_schema.get("enum"): default = '"' + default + '"' return default return "None" def make_python_property_name_from_schema_property_name(self, schema_property_name): hint_key = self.class_name + "." + schema_property_name property_name_hint = self.get_hint(hint_key, "PropertyNameHint") if not property_name_hint: property_name = schema_property_name else: property_name = property_name_hint["arguments"]["pythonPropertyName"] return util.to_underscore_separated_name(property_name) def get_hint(self, hint_key, hint_kind): if not self.code_gen_hints or hint_key not in self.code_gen_hints: return None hint_array = self.code_gen_hints[hint_key] for hint in hint_array: if hint["kind"] == hint_kind: return hint return None jschema-to-python-1.2.3/jschema_to_python/driver.py000066400000000000000000000043651354617202200224620ustar00rootroot00000000000000import argparse from jschema_to_python.object_model_module_generator import ObjectModelModuleGenerator from jschema_to_python import __version__ def main(): parser = init_parser() args = parser.parse_args() display_args(args) generator = ObjectModelModuleGenerator(args) generator.generate() if args.verbose: print("Done.") def init_parser(): parser = argparse.ArgumentParser( description="Generate source code for a set of Python classes from a JSON schema." ) parser.add_argument( "-s", "--schema-path", help="path to the JSON schema file", required=True ) parser.add_argument( "-o", "--output-directory", help="directory in which the generated classes will be created", required=True, ) parser.add_argument( "-m", "--module-name", help="name of the module containing the object model classes", ) parser.add_argument( "-r", "--root-class-name", help="the name of the class at the root of the object model represented by the schema", required=True, ) parser.add_argument( "-g", "--hints-file-path", help="path to a file containing hints that control code generation", ) parser.add_argument( "-f", "--force", action="store_true", help="overwrite the output directory if it exists", ) parser.add_argument( "-v", "--verbose", action="count", default=0, help="increase output verbosity (may be specified up to two times)", ) return parser def display_args(args): if args.verbose: print( __package__ + ": JSON schema to Python object model generator, version " + __version__ ) print("Generating Python classes...") if args.verbose > 1: print(" from JSON schema " + args.schema_path) print(" to module " + args.module_name) print(" in directory " + args.output_directory) print(" with root class " + args.root_class_name) if args.hints_file_path: print(" with code generation hints from " + args.hints_file_path) if __name__ == "__main__": main() jschema-to-python-1.2.3/jschema_to_python/init_file_generator.py000066400000000000000000000027071354617202200251750ustar00rootroot00000000000000import sys from jschema_to_python.python_file_generator import PythonFileGenerator import jschema_to_python.utilities as util class InitFileGenerator(PythonFileGenerator): def __init__(self, module_name, root_schema, root_class_name, output_directory): super(InitFileGenerator, self).__init__(output_directory) self.module_name = module_name self.root_schema = root_schema self.root_class_name = root_class_name def __del__(self): sys.stdout = sys.__stdout__ def generate(self): file_path = self.make_output_file_path("__init__.py") with open(file_path, "w") as sys.stdout: self.write_generation_comment() self.write_import_statements() def write_import_statements(self): self.write_import_statement(self.root_class_name) definition_schemas = self.root_schema.get("definitions") if definition_schemas: definition_keys = sorted(definition_schemas.keys()) for definition_key in definition_keys: class_name = util.capitalize_first_letter(definition_key) self.write_import_statement(class_name) def write_import_statement(self, class_name): class_module_name = util.class_name_to_private_module_name(class_name) print( "from " + self.module_name + "." + class_module_name + " import " + class_name ) jschema-to-python-1.2.3/jschema_to_python/object_model_module_generator.py000066400000000000000000000045571354617202200272330ustar00rootroot00000000000000import os import jschema_to_python.utilities as util from jschema_to_python.init_file_generator import InitFileGenerator from jschema_to_python.class_generator import ClassGenerator class ObjectModelModuleGenerator: def __init__(self, args): self.output_directory = args.output_directory self.force = args.force self.module_name = args.module_name self.root_schema = self.read_schema(args.schema_path) self.code_gen_hints = self.read_code_gen_hints(args.hints_file_path) self.root_class_name = args.root_class_name def generate(self): util.create_directory(self.output_directory, self.force) self.generate_root_class() self.generate_definition_classes() self.generate_init_file() def generate_init_file(self): init_file_generator = InitFileGenerator( self.module_name, self.root_schema, self.root_class_name, self.output_directory, ) init_file_generator.generate() def generate_root_class(self): class_generator = ClassGenerator( self.root_schema, self.root_class_name, self.code_gen_hints, self.output_directory, ) class_generator.generate() def generate_definition_classes(self): definition_schemas = self.root_schema.get("definitions") if definition_schemas: for key in definition_schemas: self.generate_definition_class(key, definition_schemas[key]) def generate_definition_class(self, definition_key, definition_schema): class_name = util.capitalize_first_letter(definition_key) class_generator = ClassGenerator( definition_schema, class_name, self.code_gen_hints, self.output_directory ) class_generator.generate() def read_schema(self, schema_path): if not os.path.exists(schema_path): util.exit_with_error("schema file {} does not exist", schema_path) return util.unpickle_file(schema_path) def read_code_gen_hints(self, hints_file_path): if not hints_file_path: return None if not os.path.exists(hints_file_path): util.exit_with_error( "code generation hints file {} does not exist", hints_file_path ) return util.unpickle_file(hints_file_path) jschema-to-python-1.2.3/jschema_to_python/python_file_generator.py000066400000000000000000000007641354617202200255540ustar00rootroot00000000000000import os from jschema_to_python import __version__ class PythonFileGenerator(object): def __init__(self, output_directory): self.output_directory = output_directory def write_generation_comment(self): print( "# This file was generated by " + __package__ + " version " + __version__ + ".\n" ) def make_output_file_path(self, file_name): return os.path.join(self.output_directory, file_name) jschema-to-python-1.2.3/jschema_to_python/to_json.py000066400000000000000000000035171354617202200226400ustar00rootroot00000000000000# Copyright (c) Microsoft. All Rights Reserved. # Licensed under the MIT license. See LICENSE file in the project root for full license information. import attr import copy import json def to_json(obj): """''Serializes an instance of a generated class to JSON. :param obj: an instance of any class generated by jschema-to-python. Before serializing the instance to JSON, this function maps the Python property names to the corresponding JSON schema property names. It also removes any property that has the default value specified by attr.ib, making the resulting JSON smaller. """ return json.dumps(obj, indent=2, default=_generated_class_serializer) def _generated_class_serializer(obj): if hasattr(obj, "__dict__"): dict = getattr(obj, "__dict__") dict = copy.deepcopy(dict) _remove_properties_with_default_values(obj, dict) _change_python_property_names_to_schema_property_names(obj, dict) return dict else: return str(obj) def _remove_properties_with_default_values(obj, dict): for field in attr.fields(obj.__class__): dict_entry = dict.get(field.name) if value_is_default(dict_entry, field.default) and field.name in dict: del dict[field.name] def value_is_default(dict_entry, default_value): if type(default_value) == attr._make.Factory: value = default_value.factory() else: value = default_value return dict_entry == value def _change_python_property_names_to_schema_property_names(obj, dict): for field in attr.fields(obj.__class__): schema_property_name = field.metadata.get("schema_property_name") if schema_property_name and schema_property_name != field.name and field.name in dict: dict[schema_property_name] = dict[field.name] del dict[field.name] jschema-to-python-1.2.3/jschema_to_python/utilities.py000066400000000000000000000023611354617202200231740ustar00rootroot00000000000000import os import shutil import sys import jsonpickle def capitalize_first_letter(identifier): return identifier[0].capitalize() + identifier[1:] def create_directory(directory, force): if os.path.exists(directory): if force: shutil.rmtree(directory, ignore_errors=True) else: exit_with_error("output directory {} already exists", directory) os.makedirs(directory) def to_underscore_separated_name(name): result = "" first_char = True for ch in name: if ch.islower(): next_char = ch else: next_char = ch.lower() if not first_char: next_char = "_" + next_char first_char = False result += next_char return result def class_name_to_private_module_name(class_name): # The leading underscore indicates that users are not intended to import # the class module individually. return "_" + to_underscore_separated_name(class_name) def unpickle_file(path): with open(path, mode="rt") as file_obj: contents = file_obj.read() return jsonpickle.decode(contents) def exit_with_error(message, *args): sys.stderr.write("error : " + message.format(*args) + "\n") sys.exit(1) jschema-to-python-1.2.3/pytest.ini000066400000000000000000000000321354617202200171140ustar00rootroot00000000000000[pytest] testpaths = testsjschema-to-python-1.2.3/setup.cfg000066400000000000000000000013701354617202200167120ustar00rootroot00000000000000[metadata] name = jschema_to_python author = Microsoft Corporation author-email = v-lgold@microsoft.com summary = Generate source code for Python classes from a JSON schema. home-page = https://github.com/microsoft/jschema-to-python description-file = README.rst license = MIT classifier = Development Status :: 5 - Production/Stable Environment :: Console Intended Audience :: Developers Intended Audience :: Information Technology License :: OSI Approved :: MIT License Operating System :: OS Independent Programming Language :: Python Topic :: Software Development :: Libraries :: Python Modules [options] python_requires = >= 2.7 install_requires = attrs jsonpickle pbr [files] packages = jschema_to_pythonjschema-to-python-1.2.3/setup.py000066400000000000000000000001351354617202200166010ustar00rootroot00000000000000#!/usr/bin/env python from setuptools import setup setup(setup_requires=["pbr"], pbr=True) jschema-to-python-1.2.3/tests/000077500000000000000000000000001354617202200162325ustar00rootroot00000000000000jschema-to-python-1.2.3/tests/__init__.py000066400000000000000000000000001354617202200203310ustar00rootroot00000000000000jschema-to-python-1.2.3/tests/test_class_generator.py000066400000000000000000000023161354617202200230200ustar00rootroot00000000000000import pytest import jschema_to_python as js2p import jschema_to_python.class_generator as cg def test_required_property(tmp_path): verify( class_schema={"properties": {"requiredProperty": {"type": "int"}}, "required": ["requiredProperty"]}, class_name="RequiredProperty", expected_file_name="_required_property.py", tmp_path=tmp_path, ) def test_optional_property(tmp_path): verify( class_schema={"properties": {"optionalProperty": {"type": "int", "default": 42}}}, class_name="OptionalProperty", expected_file_name="_optional_property.py", tmp_path=tmp_path, ) def verify(class_schema, class_name, expected_file_name, tmp_path): class_generator = cg.ClassGenerator( class_schema=class_schema, class_name=class_name, code_gen_hints=None, output_directory=str(tmp_path), ) class_generator.generate() with open(class_generator.file_path, "r") as fileObj: actual = fileObj.read() with open("tests/test_files/" + expected_file_name, "r") as fileObj: expected = fileObj.read() expected = expected.replace("$js2p_version$", js2p.__version__) assert actual == expected jschema-to-python-1.2.3/tests/test_files/000077500000000000000000000000001354617202200203735ustar00rootroot00000000000000jschema-to-python-1.2.3/tests/test_files/_optional_property.py000066400000000000000000000003411354617202200246730ustar00rootroot00000000000000# This file was generated by jschema_to_python version $js2p_version$. import attr @attr.s class OptionalProperty(object): optional_property = attr.ib(default=42, metadata={"schema_property_name": "optionalProperty"}) jschema-to-python-1.2.3/tests/test_files/_required_property.py000066400000000000000000000003251354617202200246700ustar00rootroot00000000000000# This file was generated by jschema_to_python version $js2p_version$. import attr @attr.s class RequiredProperty(object): required_property = attr.ib(metadata={"schema_property_name": "requiredProperty"})