Skip to content

Commit 1a70568

Browse files
committed
Adding a "preamble" section to allow arbitrary python to be inserted at the top of the file
1 parent ebf9d0a commit 1a70568

File tree

4 files changed

+153
-83
lines changed

4 files changed

+153
-83
lines changed

README.md

Lines changed: 91 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
[![json2python-models](/etc/logo.png)](https://github.com/bogdandm/json2python-models)
1+
dr[![json2python-models](/etc/logo.png)](https://github.com/bogdandm/json2python-models)
22

33
[![PyPI version](https://img.shields.io/pypi/v/json2python-models.svg?color=green)](https://badge.fury.io/py/json2python-models)
44
[![Build](https://github.com/bogdandm/json2python-models/actions/workflows/test_and_release.yml/badge.svg)](https://github.com/bogdandm/json2python-models/actions/workflows/test_and_release.yml)
@@ -7,9 +7,9 @@
77

88
![Example](/etc/convert.png)
99

10-
json2python-models is a [Python](https://www.python.org/) tool that can generate Python models classes
11-
([pydantic](https://github.com/samuelcolvin/pydantic), dataclasses, [attrs](https://github.com/python-attrs/attrs))
12-
from JSON dataset.
10+
json2python-models is a [Python](https://www.python.org/) tool that can generate Python models classes
11+
([pydantic](https://github.com/samuelcolvin/pydantic), dataclasses, [attrs](https://github.com/python-attrs/attrs))
12+
from JSON dataset.
1313

1414
## Features
1515

@@ -26,18 +26,23 @@ from JSON dataset.
2626

2727
## Table of Contents
2828

29-
* [Features](#features)
30-
* [Table of Contents](#table-of-contents)
31-
* [Example](#example)
32-
* [Installation](#installation)
33-
* [Usage](#usage)
34-
* [CLI](#cli)
35-
* [Low level API]()
36-
* [Tests](#tests)
37-
* [Test examples](#test-examples)
38-
* [Built With](#built-with)
39-
* [Contributing](#contributing)
40-
* [License](#license)
29+
- [Features](#features)
30+
- [Table of Contents](#table-of-contents)
31+
- [Examples](#examples)
32+
- [Part of Path of Exile public items API](#part-of-path-of-exile-public-items-api)
33+
- [F1 Season Results](#f1-season-results)
34+
- [Swagger](#swagger)
35+
- [Github-actions config files](#github-actions-config-files)
36+
- [Example with preamble](#example-with-preamble)
37+
- [Installation](#installation)
38+
- [Usage](#usage)
39+
- [CLI](#cli)
40+
- [Low level API](#low-level-api)
41+
- [Tests](#tests)
42+
- [Test examples](#test-examples)
43+
- [Built With](#built-with)
44+
- [Contributing](#contributing)
45+
- [License](#license)
4146

4247
## Examples
4348

@@ -155,9 +160,9 @@ class Constructor(BaseModel):
155160

156161
`swagger.json` from any online API (I tested file generated by drf-yasg and another one for Spotify API)
157162

158-
It requires a lit bit of tweaking:
163+
It requires a bit of tweaking:
159164
* Some fields store routes/models specs as dicts
160-
* There is a lot of optinal fields so we reduce merging threshold
165+
* There are a lot of optinal fields so we reduce merging threshold
161166
* Disable string literals
162167

163168
```
@@ -405,9 +410,45 @@ class Run(BaseModel):
405410

406411
</p></details>
407412

413+
### Example with preamble
414+
415+
<details><summary>----- Show -----</summary>
416+
<p>
417+
A simple example to demonstrate adding extra code before the class list.
418+
419+
```sh
420+
json2models -f pydantic --preamble "# set up defaults
421+
USERNAME = 'user'
422+
SERVER_IP = '127.0.0.1'
423+
" -m Swagger testing_tools/swagger.json
424+
```
425+
426+
```py
427+
r"""
428+
generated by json2python-models v0.2.5 at Tue Aug 23 08:55:09 2022
429+
command: json2models -f pydantic --preamble # set up defaults
430+
USERNAME = 'user'
431+
SERVER_IP = '127.0.0.1'
432+
-m Swagger testing_tools/swagger.json -o output.py
433+
"""
434+
from pydantic import BaseModel, Field
435+
from typing import Any, List, Literal, Optional, Union
436+
437+
438+
# set up defaults
439+
USERNAME = 'user'
440+
SERVER_IP = '127.0.0.1'
441+
442+
443+
444+
class Swagger(BaseModel):
445+
# etc.
446+
```
447+
</p></details>
448+
408449
## Installation
409450

410-
| **Be ware**: this project supports only `python3.7` and higher. |
451+
| **Beware**: this project supports only `python3.7` and higher. |
411452
| --- |
412453

413454
To install it, use `pip`:
@@ -426,7 +467,7 @@ python setup.py install
426467

427468
### CLI
428469

429-
For regular usage CLI tool is the best option. After you install this package you could use it as `json2models <arguments>`
470+
For regular usage CLI tool is the best option. After you install this package you can use it as `json2models <arguments>`
430471
or `python -m json_to_models <arguments>`. I.e.:
431472
```
432473
json2models -m Car car_*.json -f attrs > car.py
@@ -464,61 +505,71 @@ Arguments:
464505
* **Format**: `-f {base, pydantic, attrs, dataclasses, custom}`
465506
* **Example**: `-f pydantic`
466507
* **Default**: `-f base`
467-
508+
468509
* `-s`, `--structure` - Models composition style.
469-
* **Format**: `-s {flat, nested}`
510+
* **Format**: `-s {flat, nested}`
470511
* **Example**: `-s nested`
471512
* **Default**: `-s flat`
472-
513+
514+
* `--preamble` - Additional material to be
515+
* **Format**: `--preamble "<formatted python code string to be added after module imports>"`
516+
* **Example**:
517+
```sh
518+
--preamble "# set up defaults
519+
USERNAME = 'user'
520+
SERVER = '127.0.0.1'"
521+
```
522+
* **Optional**
523+
473524
* `--datetime` - Enable datetime/date/time strings parsing.
474525
* **Default**: disabled
475526
* **Warning**: This can lead to 6-7 times slowdown on large datasets. Be sure that you really need this option.
476-
527+
477528
* `--disable-unicode-conversion`, `--no-unidecode` - Disable unicode conversion in field labels and class names
478529
* **Default**: enabled
479-
530+
480531
* `--strings-converters` - Enable generation of string types converters (i.e. `IsoDatetimeString` or `BooleanString`).
481532
* **Default**: disabled
482-
533+
483534
* `--max-strings-literals` - Generate `Literal['foo', 'bar']` when field have less than NUMBER string constants as values.
484-
* **Format**: `--max-strings-literals <NUMBER>`
535+
* **Format**: `--max-strings-literals <NUMBER>`
485536
* **Default**: 10 (generator classes could override it)
486537
* **Example**: `--max-strings-literals 5` - only 5 literals will be saved and used to code generation
487538
* **Note**: There could not be more than **15** literals per field (for performance reasons)
488539
* **Note**: `attrs` code generator do not use Literals and just generate `str` fields instead
489540

490-
* `--merge` - Merge policy settings. Possible values are:
541+
* `--merge` - Merge policy settings. Possible values are:
491542
* **Format**: `--merge MERGE_POLICY [MERGE_POLICY ...]`
492543
* **Possible values** (MERGE_POLICY):
493-
* `percent[_<percent>]` - two models had a certain percentage of matched field names.
494-
Custom value could be i.e. `percent_95`.
495-
* `number[_<number>]` - two models had a certain number of matched field names.
544+
* `percent[_<percent>]` - two models had a certain percentage of matched field names.
545+
Custom value could be i.e. `percent_95`.
546+
* `number[_<number>]` - two models had a certain number of matched field names.
496547
* `exact` - two models should have exact same field names to merge.
497548
* **Example**: `--merge percent_95 number_20` - merge if 95% of fields are matched or 20 of fields are matched
498549
* **Default**: `--merge percent_70 number_10`
499-
550+
500551
* `--dict-keys-regex`, `--dkr` - List of regular expressions (Python syntax).
501-
If all keys of some dict are match one of the pattern then
552+
If all keys of some dict are match one of the pattern then
502553
this dict will be marked as dict field but not nested model.
503554
* **Format**: `--dkr RegEx [RegEx ...]`
504555
* **Example**: `--dkr node_\d+ \d+_\d+_\d+`
505-
* **Note**: `^` and `$` (string borders) tokens will be added automatically but you
556+
* **Note**: `^` and `$` (string borders) tokens will be added automatically but you
506557
have to escape other special characters manually.
507558
* **Optional**
508-
559+
509560
* `--dict-keys-fields`, `--dkf` - List of model fields names that will be marked as dict fields
510561
* **Format**: `--dkf FIELD_NAME [FIELD_NAME ...]`
511562
* **Example**: `--dkf "dict_data" "mapping"`
512563
* **Optional**
513-
564+
514565
* `--code-generator` - Absolute import path to `GenericModelCodeGenerator` subclass.
515566
* **Format**: `--code-generator CODE_GENERATOR`
516567
* **Example**: `-f mypackage.mymodule.DjangoModelsGenerator`
517568
* **Note**: Is ignored without `-f custom` but is required with it.
518-
519-
* `--code-generator-kwargs` - List of GenericModelCodeGenerator subclass arguments (for `__init__` method,
520-
see docs of specific subclass).
521-
Each argument should be in following format: `argument_name=value` or `"argument_name=value with space"`.
569+
570+
* `--code-generator-kwargs` - List of GenericModelCodeGenerator subclass arguments (for `__init__` method,
571+
see docs of specific subclass).
572+
Each argument should be in following format: `argument_name=value` or `"argument_name=value with space"`.
522573
Boolean values should be passed in JS style: `true` or `false`
523574
* **Format**: `--code-generator-kwargs [NAME=VALUE [NAME=VALUE ...]]`
524575
* **Example**: `--code-generator-kwargs kwarg1=true kwarg2=10 "kwarg3=It is string with spaces"`

json_to_models/cli.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,12 @@ def parse_args(self, args: List[str] = None):
102102
code_generator_kwargs_raw: List[str] = namespace.code_generator_kwargs
103103
dict_keys_regex: List[str] = namespace.dict_keys_regex
104104
dict_keys_fields: List[str] = namespace.dict_keys_fields
105+
preamble: str = namespace.preamble
105106

106-
self.validate(models, models_lists, merge_policy, framework, code_generator)
107+
self.validate(models_lists, merge_policy, framework, code_generator)
107108
self.setup_models_data(models, models_lists, parser)
108109
self.set_args(merge_policy, structure, framework, code_generator, code_generator_kwargs_raw,
109-
dict_keys_regex, dict_keys_fields, disable_unicode_conversion)
110+
dict_keys_regex, dict_keys_fields, disable_unicode_conversion, preamble)
110111

111112
def run(self):
112113
if self.enable_datetime:
@@ -122,8 +123,11 @@ def run(self):
122123
registry.merge_models(generator)
123124
registry.generate_names()
124125
structure = self.structure_fn(registry.models_map)
125-
output = self.version_string + \
126-
generate_code(structure, self.model_generator, class_generator_kwargs=self.model_generator_kwargs)
126+
output = self.version_string + generate_code(
127+
structure,
128+
self.model_generator,
129+
class_generator_kwargs=self.model_generator_kwargs,
130+
preamble=self.preamble)
127131
if self.output_file:
128132
with open(self.output_file, "w", encoding="utf-8") as f:
129133
f.write(output)
@@ -140,11 +144,10 @@ def version_string(self):
140144
'"""\n'
141145
)
142146

143-
def validate(self, models, models_list, merge_policy, framework, code_generator):
147+
def validate(self, models_list, merge_policy, framework, code_generator):
144148
"""
145149
Validate parsed args
146150
147-
:param models: List of pairs (model name, list of filesystem path)
148151
:param models_list: List of pairs (model name, list of lookup expr and filesystem path)
149152
:param merge_policy: List of merge policies. Each merge policy is either string or string and policy arguments
150153
:param framework: Framework name (predefined code generator)
@@ -196,7 +199,8 @@ def set_args(
196199
code_generator_kwargs_raw: List[str],
197200
dict_keys_regex: List[str],
198201
dict_keys_fields: List[str],
199-
disable_unicode_conversion: bool
202+
disable_unicode_conversion: bool,
203+
preamble: str,
200204
):
201205
"""
202206
Convert CLI args to python representation and set them to appropriate object attributes
@@ -236,7 +240,7 @@ def set_args(
236240

237241
self.dict_keys_regex = [re.compile(rf"^{r}$") for r in dict_keys_regex] if dict_keys_regex else ()
238242
self.dict_keys_fields = dict_keys_fields or ()
239-
243+
self.preamble = preamble or None
240244
self.initialized = True
241245

242246
@classmethod
@@ -366,6 +370,11 @@ def _create_argparser(cls) -> argparse.ArgumentParser:
366370
"Boolean values should be passed in JS style: true | false"
367371
"\n\n"
368372
)
373+
parser.add_argument(
374+
"--preamble",
375+
type=str,
376+
help="Code to insert into the generated file after the imports and before the list of classes\n\n"
377+
)
369378

370379
return parser
371380

json_to_models/models/base.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,19 +54,19 @@ class GenericModelCodeGenerator:
5454
@{{ decorator }}
5555
{% endfor -%}
5656
class {{ name }}{% if bases %}({{ bases }}){% endif %}:
57-
57+
5858
{%- for code in nested %}
5959
{{ code }}
6060
{% endfor -%}
61-
61+
6262
{%- if fields -%}
6363
{%- for field in fields %}
6464
{{ field }}
6565
{%- endfor %}
6666
{%- else %}
6767
pass
6868
{%- endif -%}
69-
{%- if extra %}
69+
{%- if extra %}
7070
{{ extra }}
7171
{%- endif -%}
7272
""")
@@ -210,7 +210,7 @@ def _generate_code(
210210
lvl=0
211211
) -> Tuple[ImportPathList, List[str]]:
212212
"""
213-
Walk thought models structure and covert them into code
213+
Walk through the model structures and convert them into code
214214
215215
:param structure: Result of compose_models or similar function
216216
:param class_generator: GenericModelCodeGenerator subclass
@@ -241,23 +241,28 @@ def _generate_code(
241241

242242

243243
def generate_code(structure: ModelsStructureType, class_generator: Type[GenericModelCodeGenerator],
244-
class_generator_kwargs: dict = None, objects_delimiter: str = OBJECTS_DELIMITER) -> str:
244+
class_generator_kwargs: dict = None,
245+
objects_delimiter: str = OBJECTS_DELIMITER,
246+
preamble: str = None) -> str:
245247
"""
246248
Generate ready-to-use code
247249
248250
:param structure: Result of compose_models or similar function
249251
:param class_generator: GenericModelCodeGenerator subclass
250252
:param class_generator_kwargs: kwags for GenericModelCodeGenerator init
251253
:param objects_delimiter: Delimiter between root level classes
254+
:param preamble: code to insert after the imports and before the classes
252255
:return: Generated code
253256
"""
254257
root, mapping = structure
255258
with AbsoluteModelRef.inject(mapping):
256259
imports, classes = _generate_code(root, class_generator, class_generator_kwargs or {})
260+
imports_str = ""
257261
if imports:
258262
imports_str = compile_imports(imports) + objects_delimiter
259-
else:
260-
imports_str = ""
263+
if preamble:
264+
imports_str += preamble + objects_delimiter
265+
261266
return imports_str + objects_delimiter.join(classes) + "\n"
262267

263268

0 commit comments

Comments
 (0)