Skip to content

Commit 5ce0534

Browse files
authored
βœ¨πŸš‡ Add CI/CD pipeline (#19)
* 🧹 Added .vscode to gitignore * βœ¨πŸš‡ Added CI CD pipeline * πŸ§ͺ Added save guard for tests that only work for python >=3.8 * πŸ§ͺπŸ‘Œ Use subprocess.run instead of os.system to run CLI test * πŸ§ͺ Added save guard for tests that only work for python >=3.7 * πŸ§ͺπŸ‘Œ Changed relative path to be relative to repo root * 🩹 Fixed banner_comment on windows The problem was that the multiline string wasn't escaped properly in the windows system call to the json2ts_cmd * πŸ”§ Added pytest-cov to CI test * πŸ“š Added CI status badge
1 parent 4641944 commit 5ce0534

File tree

5 files changed

+123
-26
lines changed

5 files changed

+123
-26
lines changed

β€Ž.github/workflows/tests.yml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: "Tests"
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
test:
9+
name: Test pyglotaran stable
10+
runs-on: ${{ matrix.os }}
11+
strategy:
12+
matrix:
13+
os: [ubuntu-latest, windows-latest, macOS-latest]
14+
python-version: ["3.8"]
15+
include:
16+
- os: ubuntu-latest
17+
python-version: "3.6"
18+
- os: ubuntu-latest
19+
python-version: "3.7"
20+
- os: ubuntu-latest
21+
python-version: "3.9"
22+
- os: ubuntu-latest
23+
python-version: "3.10"
24+
25+
steps:
26+
- name: Check out repo
27+
uses: actions/checkout@v3
28+
- name: Set up Node.js 16
29+
uses: actions/setup-node@v3
30+
with:
31+
node-version: 16
32+
- name: Set up Python ${{ matrix.python-version }}
33+
uses: actions/setup-python@v4
34+
with:
35+
python-version: ${{ matrix.python-version }}
36+
- name: Install json-schema-to-typescript
37+
run: |
38+
npm i -g json-schema-to-typescript
39+
- name: Install python dependencies
40+
run: |
41+
python -m pip install -U pip wheel pytest pytest-cov
42+
python -m pip install -U .
43+
- name: Run tests
44+
run: |
45+
python -m pytest --cov=pydantic2ts
46+
47+
deploy:
48+
name: Deploy to PyPi
49+
runs-on: ubuntu-latest
50+
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags')
51+
needs: test
52+
steps:
53+
- uses: actions/checkout@v3
54+
- name: Set up Python 3.8
55+
uses: actions/setup-python@v4
56+
with:
57+
python-version: 3.8
58+
- name: Install dependencies
59+
run: |
60+
python -m pip install -U pip wheel
61+
- name: Build dist
62+
run: |
63+
python setup.py sdist bdist_wheel
64+
65+
- name: Publish package
66+
uses: pypa/gh-action-pypi-publish@v1.5.0
67+
with:
68+
user: __token__
69+
password: ${{ secrets.pypi_password }}

β€Ž.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,7 @@ cython_debug/
139139

140140
# my stuff
141141
.idea
142-
.private.txt
142+
.private.txt
143+
144+
# VS Code config
145+
.vscode

β€ŽREADME.md

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,35 @@
11
# pydantic-to-typescript
22

33
[![PyPI version](https://badge.fury.io/py/pydantic-to-typescript.svg)](https://badge.fury.io/py/pydantic-to-typescript)
4+
[![Tests](https://github.com/phillipdupuis/pydantic-to-typescript/actions/workflows/tests.yml/badge.svg)](https://github.com/phillipdupuis/pydantic-to-typescript/actions/workflows/tests.yml)
45

56
A simple CLI tool for converting pydantic models into typescript interfaces. Useful for any scenario in which python and javascript applications are interacting, since it allows you to have a single source of truth for type definitions.
67

78
This tool requires that you have the lovely json2ts CLI utility installed. Instructions can be found here: https://www.npmjs.com/package/json-schema-to-typescript
89

910
### Installation
11+
1012
```bash
1113
$ pip install pydantic-to-typescript
1214
```
15+
1316
---
17+
1418
### CLI
1519

16-
|Prop|Description|
17-
|:----------|:-----------|
18-
|‑‑module|name or filepath of the python module you would like to convert. All the pydantic models within it will be converted to typescript interfaces. Discoverable submodules will also be checked.|
19-
|‑‑output|name of the file the typescript definitions should be written to. Ex: './frontend/apiTypes.ts'|
20-
|‑‑exclude|name of a pydantic model which should be omitted from the resulting typescript definitions. This option can be defined multiple times, ex: `--exclude Foo --exclude Bar` to exclude both the Foo and Bar models from the output.|
21-
|‑‑json2ts‑cmd|optional, the command used to invoke json2ts. The default is 'json2ts'. Specify this if you have it installed in a strange location and need to provide the exact path (ex: /myproject/node_modules/bin/json2ts)|
20+
| Prop | Description |
21+
| :------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
22+
| ‑‑module | name or filepath of the python module you would like to convert. All the pydantic models within it will be converted to typescript interfaces. Discoverable submodules will also be checked. |
23+
| ‑‑output | name of the file the typescript definitions should be written to. Ex: './frontend/apiTypes.ts' |
24+
| ‑‑exclude | name of a pydantic model which should be omitted from the resulting typescript definitions. This option can be defined multiple times, ex: `--exclude Foo --exclude Bar` to exclude both the Foo and Bar models from the output. |
25+
| ‑‑json2ts‑cmd | optional, the command used to invoke json2ts. The default is 'json2ts'. Specify this if you have it installed in a strange location and need to provide the exact path (ex: /myproject/node_modules/bin/json2ts) |
26+
2227
---
28+
2329
### Usage
30+
2431
Define your pydantic models (ex: /backend/api.py):
32+
2533
```python
2634
from fastapi import FastAPI
2735
from pydantic import BaseModel
@@ -47,22 +55,29 @@ def login(body: LoginCredentials):
4755
profile = Profile(**body.dict(), age=72, hobbies=['cats'])
4856
return LoginResponseData(token='very-secure', profile=profile)
4957
```
58+
5059
Execute the command for converting these models into typescript definitions, via:
60+
5161
```bash
5262
$ pydantic2ts --module backend.api --output ./frontend/apiTypes.ts
5363
```
64+
5465
or:
66+
5567
```bash
5668
$ pydantic2ts --module ./backend/api.py --output ./frontend/apiTypes.ts
5769
```
70+
5871
or:
72+
5973
```python
6074
from pydantic2ts import generate_typescript_defs
6175

6276
generate_typescript_defs("backend.api", "./frontend/apiTypes.ts")
6377
```
6478

6579
The models are now defined in typescript...
80+
6681
```ts
6782
/* tslint:disable */
6883
/**
@@ -84,19 +99,21 @@ export interface Profile {
8499
hobbies: string[];
85100
}
86101
```
102+
87103
...and can be used in your typescript code with complete confidence.
104+
88105
```ts
89-
import { LoginCredentials, LoginResponseData } from './apiTypes.ts';
106+
import { LoginCredentials, LoginResponseData } from "./apiTypes.ts";
90107

91108
async function login(
92109
credentials: LoginCredentials,
93110
resolve: (data: LoginResponseData) => void,
94-
reject: (error: string) => void,
111+
reject: (error: string) => void
95112
) {
96113
try {
97-
const response: Response = await fetch('/login/', {
98-
method: 'POST',
99-
headers: {'Content-Type': 'application/json'},
114+
const response: Response = await fetch("/login/", {
115+
method: "POST",
116+
headers: { "Content-Type": "application/json" },
100117
body: JSON.stringify(credentials),
101118
});
102119
const data: LoginResponseData = await response.json();

β€Žpydantic2ts/cli/script.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,16 @@ def remove_master_model_from_output(output: str) -> None:
106106
end = i
107107
break
108108

109-
new_lines = lines[:start] + lines[(end + 1) :]
109+
banner_comment_lines = [
110+
"/* tslint:disable */\n",
111+
"/* eslint-disable */\n",
112+
"/**\n",
113+
"/* This file was automatically generated from pydantic models by running pydantic2ts.\n",
114+
"/* Do not modify it by hand - just update the pydantic models and then re-run the script\n",
115+
"*/\n\n",
116+
]
117+
118+
new_lines = banner_comment_lines + lines[:start] + lines[(end + 1) :]
110119
with open(output, "w") as f:
111120
f.writelines(new_lines)
112121

@@ -200,18 +209,8 @@ def generate_typescript_defs(
200209

201210
logger.info("Converting JSON schema to typescript definitions...")
202211

203-
banner_comment = "\n".join(
204-
[
205-
"/* tslint:disable */",
206-
"/* eslint-disable */",
207-
"/**",
208-
"/* This file was automatically generated from pydantic models by running pydantic2ts.",
209-
"/* Do not modify it by hand - just update the pydantic models and then re-run the script",
210-
"*/",
211-
]
212-
)
213212
os.system(
214-
f'{json2ts_cmd} -i {schema_file_path} -o {output} --bannerComment "{banner_comment}"'
213+
f'{json2ts_cmd} -i {schema_file_path} -o {output} --bannerComment ""'
215214
)
216215
shutil.rmtree(schema_dir)
217216
remove_master_model_from_output(output)

β€Žtests/test_script.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import os
2+
import sys
3+
import subprocess
24
from pydantic2ts import generate_typescript_defs
35

46

@@ -23,6 +25,13 @@ def run_test(
2325
Execute pydantic2ts logic for converting pydantic models into tyepscript definitions.
2426
Compare the output with the expected output, verifying it is identical.
2527
"""
28+
# Literal was only introduced in python 3.8 (Ref.: PEP 586) so tests break
29+
if sys.version_info < (3, 8) and test_name == "submodules":
30+
return
31+
# GenericModel is only supported for python>=3.7
32+
# (Ref.: https://pydantic-docs.helpmanual.io/usage/models/#generic-models)
33+
if sys.version_info < (3, 7) and test_name == "generics":
34+
return
2635
module_path = module_path or get_input_module(test_name)
2736
output_path = tmpdir.join(f"cli_{test_name}.ts").strpath
2837

@@ -32,7 +41,7 @@ def run_test(
3241
cmd = f"pydantic2ts --module {module_path} --output {output_path}"
3342
for model_to_exclude in exclude:
3443
cmd += f" --exclude {model_to_exclude}"
35-
os.system(cmd)
44+
subprocess.run(cmd, shell=True)
3645

3746
with open(output_path, "r") as f:
3847
output = f.read()
@@ -59,7 +68,7 @@ def test_excluding_models(tmpdir):
5968

6069
def test_relative_filepath(tmpdir):
6170
test_name = "single_module"
62-
relative_path = os.path.join(".", "expected_results", test_name, "input.py")
71+
relative_path = os.path.join(".", "tests", "expected_results", test_name, "input.py")
6372
run_test(
6473
tmpdir, "single_module", module_path=relative_path,
6574
)

0 commit comments

Comments
Β (0)