typesync 0.0.1a1__tar.gz → 0.0.1a2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. {typesync-0.0.1a1 → typesync-0.0.1a2}/PKG-INFO +5 -6
  2. {typesync-0.0.1a1 → typesync-0.0.1a2}/README.md +3 -5
  3. {typesync-0.0.1a1 → typesync-0.0.1a2}/pyproject.toml +8 -1
  4. {typesync-0.0.1a1 → typesync-0.0.1a2}/typesync/__init__.py +3 -1
  5. typesync-0.0.1a2/typesync/annotations.py +27 -0
  6. typesync-0.0.1a2/typesync/argument_types.py +116 -0
  7. {typesync-0.0.1a1 → typesync-0.0.1a2}/typesync/cli.py +75 -23
  8. typesync-0.0.1a2/typesync/codegen/__init__.py +7 -0
  9. {typesync-0.0.1a1 → typesync-0.0.1a2}/typesync/codegen/extractor.py +122 -51
  10. {typesync-0.0.1a1 → typesync-0.0.1a2}/typesync/codegen/inference.py +15 -5
  11. {typesync-0.0.1a1 → typesync-0.0.1a2}/typesync/codegen/writer.py +101 -65
  12. typesync-0.0.1a2/typesync/misc.py +7 -0
  13. {typesync-0.0.1a1 → typesync-0.0.1a2}/typesync/type_translators/__init__.py +4 -0
  14. {typesync-0.0.1a1 → typesync-0.0.1a2}/typesync/type_translators/abstract.py +10 -3
  15. typesync-0.0.1a2/typesync/type_translators/annotations_translator.py +47 -0
  16. {typesync-0.0.1a1 → typesync-0.0.1a2}/typesync/type_translators/base_translator.py +14 -7
  17. typesync-0.0.1a2/typesync/type_translators/context.py +16 -0
  18. {typesync-0.0.1a1 → typesync-0.0.1a2}/typesync/type_translators/flask_translator.py +2 -7
  19. {typesync-0.0.1a1 → typesync-0.0.1a2}/typesync/type_translators/type_node.py +28 -4
  20. {typesync-0.0.1a1 → typesync-0.0.1a2}/typesync.egg-info/PKG-INFO +5 -6
  21. {typesync-0.0.1a1 → typesync-0.0.1a2}/typesync.egg-info/SOURCES.txt +5 -0
  22. {typesync-0.0.1a1 → typesync-0.0.1a2}/typesync.egg-info/requires.txt +1 -0
  23. typesync-0.0.1a1/typesync/codegen/__init__.py +0 -7
  24. {typesync-0.0.1a1 → typesync-0.0.1a2}/LICENSE +0 -0
  25. {typesync-0.0.1a1 → typesync-0.0.1a2}/setup.cfg +0 -0
  26. {typesync-0.0.1a1 → typesync-0.0.1a2}/tests/test_arg_types.py +0 -0
  27. {typesync-0.0.1a1 → typesync-0.0.1a2}/tests/test_inference.py +0 -0
  28. {typesync-0.0.1a1 → typesync-0.0.1a2}/tests/test_nested_types.py +0 -0
  29. {typesync-0.0.1a1 → typesync-0.0.1a2}/tests/test_return_types.py +0 -0
  30. {typesync-0.0.1a1 → typesync-0.0.1a2}/tests/test_stubs.py +0 -0
  31. {typesync-0.0.1a1 → typesync-0.0.1a2}/tests/test_typed_dict.py +0 -0
  32. {typesync-0.0.1a1 → typesync-0.0.1a2}/typesync/ts_types.py +0 -0
  33. {typesync-0.0.1a1 → typesync-0.0.1a2}/typesync/utils.py +0 -0
  34. {typesync-0.0.1a1 → typesync-0.0.1a2}/typesync.egg-info/dependency_links.txt +0 -0
  35. {typesync-0.0.1a1 → typesync-0.0.1a2}/typesync.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: typesync
3
- Version: 0.0.1a1
3
+ Version: 0.0.1a2
4
4
  Summary: Auto-generate TypeScript client code from Flask routes and Python type annotations.
5
5
  Author-email: Francisco Rodrigues <francisco.rodrigues0908@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/ArmindoFlores/typesync
@@ -11,6 +11,7 @@ License-File: LICENSE
11
11
  Requires-Dist: click
12
12
  Requires-Dist: flask
13
13
  Requires-Dist: inflection>=0.5.1
14
+ Requires-Dist: prettytable>=3.17.0
14
15
  Dynamic: license-file
15
16
 
16
17
  # Typesync
@@ -36,9 +37,9 @@ Some features are already implemented, others are planned.
36
37
  - [x] Support for multiple HTTP methods (GET, POST, PUT, DELETE, etc.)
37
38
  - [x] Support for JSON request bodies with typed parameters
38
39
  - [ ] Support validators such as pydantic
39
- - [ ] Support for `typing.Annotated` to:
40
+ - [x] Support for `typing.Annotated` to:
40
41
  - [ ] Ignore specific routes
41
- - [ ] Customize generation behavior (naming, visibility, etc.)
42
+ - [x] Customize generation behavior (naming, visibility, etc.)
42
43
  - [ ] Improved error reporting for unsupported or ambiguous annotations
43
44
  - [ ] Optional generation modes (types only, requests only)
44
45
  - [ ] Configuration file support
@@ -52,9 +53,7 @@ Some features are already implemented, others are planned.
52
53
 
53
54
  ## Installation
54
55
 
55
- The project is not yet published on PyPI. Installation is currently done directly from source, preferably using `uv`.
56
-
57
- Clone the repository and install dependencies:
56
+ You can install typesync using `pip install typesync`. Alternatively, you can do it directly from source, preferably using `uv`, by cloning the repository and installing dependencies:
58
57
 
59
58
  ```bash
60
59
  git clone https://github.com/ArmindoFlores/typesync
@@ -21,9 +21,9 @@ Some features are already implemented, others are planned.
21
21
  - [x] Support for multiple HTTP methods (GET, POST, PUT, DELETE, etc.)
22
22
  - [x] Support for JSON request bodies with typed parameters
23
23
  - [ ] Support validators such as pydantic
24
- - [ ] Support for `typing.Annotated` to:
24
+ - [x] Support for `typing.Annotated` to:
25
25
  - [ ] Ignore specific routes
26
- - [ ] Customize generation behavior (naming, visibility, etc.)
26
+ - [x] Customize generation behavior (naming, visibility, etc.)
27
27
  - [ ] Improved error reporting for unsupported or ambiguous annotations
28
28
  - [ ] Optional generation modes (types only, requests only)
29
29
  - [ ] Configuration file support
@@ -37,9 +37,7 @@ Some features are already implemented, others are planned.
37
37
 
38
38
  ## Installation
39
39
 
40
- The project is not yet published on PyPI. Installation is currently done directly from source, preferably using `uv`.
41
-
42
- Clone the repository and install dependencies:
40
+ You can install typesync using `pip install typesync`. Alternatively, you can do it directly from source, preferably using `uv`, by cloning the repository and installing dependencies:
43
41
 
44
42
  ```bash
45
43
  git clone https://github.com/ArmindoFlores/typesync
@@ -1,5 +1,5 @@
1
1
  [project]
2
- version = "0.0.1a1"
2
+ version = "0.0.1a2"
3
3
  name = "typesync"
4
4
  description = "Auto-generate TypeScript client code from Flask routes and Python type annotations."
5
5
  readme = "README.md"
@@ -8,6 +8,7 @@ dependencies = [
8
8
  "click",
9
9
  "flask",
10
10
  "inflection>=0.5.1",
11
+ "prettytable>=3.17.0",
11
12
  ]
12
13
  requires-python = ">= 3.12"
13
14
  authors = [
@@ -36,6 +37,12 @@ dev = [
36
37
  "ruff>=0.14.13",
37
38
  ]
38
39
 
40
+ [tool.ty.rules]
41
+ invalid-type-form = "ignore"
42
+
43
+ [tool.ty.src]
44
+ exclude = ["tests"]
45
+
39
46
  [tool.ruff.lint]
40
47
  extend-select = [
41
48
  "E", # pycodestyle (errors)
@@ -1,4 +1,5 @@
1
1
  __all__ = [
2
+ "annotations",
2
3
  "cli",
3
4
  "extractor",
4
5
  "ts_types",
@@ -6,8 +7,9 @@ __all__ = [
6
7
  "utils",
7
8
  ]
8
9
 
9
- from .codegen import extractor
10
+ from . import annotations
10
11
  from . import type_translators
11
12
  from . import utils
12
13
  from . import ts_types
13
14
  from .cli import cli
15
+ from .codegen import extractor
@@ -0,0 +1,27 @@
1
+ import typing
2
+
3
+ from typesync.misc import HTTPMethod
4
+
5
+
6
+ class TypesyncAnnotation: ...
7
+
8
+
9
+ class TypesyncSkipGenerationAnnotation(TypesyncAnnotation): ...
10
+
11
+
12
+ class TypesyncHTTPMethodAnnotation(TypesyncAnnotation):
13
+ def __init__(self, methods: set[HTTPMethod]) -> None:
14
+ self.methods = methods
15
+
16
+
17
+ type SkipGeneration[T] = typing.Annotated[T, TypesyncSkipGenerationAnnotation()]
18
+
19
+ type ForHTTPGet[T] = typing.Annotated[T, TypesyncHTTPMethodAnnotation({"GET"})]
20
+ type ForHTTPHead[T] = typing.Annotated[T, TypesyncHTTPMethodAnnotation({"HEAD"})]
21
+ type ForHTTPPost[T] = typing.Annotated[T, TypesyncHTTPMethodAnnotation({"POST"})]
22
+ type ForHTTPPut[T] = typing.Annotated[T, TypesyncHTTPMethodAnnotation({"PUT"})]
23
+ type ForHTTPDelete[T] = typing.Annotated[T, TypesyncHTTPMethodAnnotation({"DELETE"})]
24
+ type ForHTTPConnect[T] = typing.Annotated[T, TypesyncHTTPMethodAnnotation({"CONNECT"})]
25
+ type ForHTTPOptions[T] = typing.Annotated[T, TypesyncHTTPMethodAnnotation({"OPTIONS"})]
26
+ type ForHTTPTrace[T] = typing.Annotated[T, TypesyncHTTPMethodAnnotation({"TRACE"})]
27
+ type ForHTTPPatch[T] = typing.Annotated[T, TypesyncHTTPMethodAnnotation({"PATCH"})]
@@ -0,0 +1,116 @@
1
+ import hashlib
2
+ import importlib.util
3
+ import pathlib
4
+ import sys
5
+ import types
6
+
7
+ from click import ParamType
8
+
9
+ from .type_translators import Translator
10
+
11
+
12
+ class PythonModuleParamType(ParamType):
13
+ name = "python_module"
14
+
15
+ def convert(self, value, param, ctx) -> types.ModuleType:
16
+ if isinstance(value, types.ModuleType):
17
+ return value
18
+
19
+ if not isinstance(value, str):
20
+ return self.fail(f"{value!r} is not a valid value", param, ctx)
21
+
22
+ path = pathlib.Path(value).resolve()
23
+
24
+ if not path.exists:
25
+ return self.fail(f"file {value!r} does not exist", param, ctx)
26
+
27
+ if not path.is_file():
28
+ return self.fail(f"{value!r} is not a file", param, ctx)
29
+
30
+ mod_name = f"module_{hashlib.sha256(str(path).encode()).hexdigest()[:16]}"
31
+ spec = importlib.util.spec_from_file_location(mod_name, str(path))
32
+ if spec is None or spec.loader is None:
33
+ return self.fail(f"{value!r} is not a valid python module", param, ctx)
34
+
35
+ try:
36
+ module = importlib.util.module_from_spec(spec)
37
+ except ModuleNotFoundError:
38
+ return self.fail(f"{value!r} is not a valid python module", param, ctx)
39
+
40
+ sys.modules[mod_name] = module
41
+ try:
42
+ spec.loader.exec_module(module)
43
+ except Exception as e:
44
+ del sys.modules[mod_name]
45
+ return self.fail(f"could not load module at {value!r} ({e})", param, ctx)
46
+
47
+ return module
48
+
49
+
50
+ class TranslatorPluginParamType(ParamType):
51
+ name = "translator_plugin"
52
+
53
+ def __init__(self, *args, **kwargs) -> None:
54
+ super().__init__(*args, **kwargs)
55
+ self.python_module_plugin_param_type = PythonModuleParamType()
56
+
57
+ def convert(self, value, param, ctx) -> type[Translator]:
58
+ module = self.python_module_plugin_param_type.convert(value, param, ctx)
59
+
60
+ translator_func = getattr(module, "translator", None)
61
+ if translator_func is None:
62
+ return self.fail(
63
+ f"{value!r} must define a function 'translator() -> type[Translator]'",
64
+ param,
65
+ ctx,
66
+ )
67
+ try:
68
+ translator = translator_func()
69
+ except Exception as e:
70
+ return self.fail(
71
+ f"failed to run the 'translator()' function defined by {value!r} ({e})",
72
+ param,
73
+ ctx,
74
+ )
75
+
76
+ if not issubclass(translator, Translator):
77
+ return self.fail(
78
+ f"'translator()' function defined by {value!r} must return a class "
79
+ "that inherits from typesync.type_translators.Translator",
80
+ param,
81
+ ctx,
82
+ )
83
+
84
+ return translator
85
+
86
+
87
+ class TranslatorPriorityParamType(ParamType):
88
+ name = "translator_priority"
89
+
90
+ def convert(self, value, param, ctx) -> tuple[str, int]:
91
+ if isinstance(value, tuple):
92
+ return value
93
+
94
+ if not isinstance(value, str):
95
+ return self.fail(f"{value!r} is not a valid value", param, ctx)
96
+
97
+ id_and_priority = value.rsplit(":", 1)
98
+ if len(id_and_priority) != 2:
99
+ return self.fail(
100
+ f"must be of the form ID:PRIORITY (was {value!r})", param, ctx
101
+ )
102
+
103
+ id_, priority_str = id_and_priority
104
+ try:
105
+ priority = int(priority_str)
106
+ except ValueError:
107
+ return self.fail(
108
+ f"priority {priority_str!r} cannot be converted to an int", param, ctx
109
+ )
110
+
111
+ return id_, priority
112
+
113
+
114
+ PYTHON_MODULE = PythonModuleParamType()
115
+ TRANSLATOR_PLUGIN = TranslatorPluginParamType()
116
+ TRANSLATOR_PRIORITY = TranslatorPriorityParamType()
@@ -1,11 +1,17 @@
1
1
  import os
2
+ import typing
2
3
 
3
4
  import click
4
5
  from flask import current_app
5
6
  from flask.cli import AppGroup
7
+ from prettytable import PrettyTable
6
8
  from werkzeug.routing.rules import Rule
7
9
 
8
- from .codegen import CodeWriter, FlaskRouteTypeExtractor
10
+ from . import argument_types
11
+ from .codegen import CodeWriter, RouteTypeExtractor
12
+
13
+ if typing.TYPE_CHECKING:
14
+ from .type_translators import Translator
9
15
 
10
16
 
11
17
  cli = AppGroup("typesync")
@@ -14,12 +20,37 @@ cli = AppGroup("typesync")
14
20
  @cli.command(help="Generate Typescript types based on Flask routes.")
15
21
  @click.argument("out_dir", type=click.Path(file_okay=False, resolve_path=True))
16
22
  @click.option("--endpoint", "-E", help="The base endpoint.", default="")
17
- @click.option("--samefile", "-S", help="Write types and apis to the same file")
23
+ @click.option("--samefile", "-S", help="Write types and apis to the same file.")
24
+ @click.option(
25
+ "--translator",
26
+ "-t",
27
+ help=(
28
+ "Path to a python script containing a additional type translators. "
29
+ "May be used multiple times."
30
+ ),
31
+ type=argument_types.TRANSLATOR_PLUGIN,
32
+ multiple=True,
33
+ )
34
+ @click.option(
35
+ "--translator-priority",
36
+ help=("Set the priority of a translator.May be used multiple times."),
37
+ type=argument_types.TRANSLATOR_PRIORITY,
38
+ multiple=True,
39
+ )
40
+ @click.option(
41
+ "--skip-unannotated",
42
+ type=bool,
43
+ default=True,
44
+ help=(
45
+ "Whether to skip code generation for routes whose annotations are not specified"
46
+ " and could not be inferred. Defaults to True."
47
+ ),
48
+ )
18
49
  @click.option(
19
50
  "--inference",
20
51
  "-i",
21
52
  is_flag=True,
22
- help="Whether to use inference when type annotations cannot be resolved",
53
+ help="Whether to use inference when type annotations cannot be resolved.",
23
54
  )
24
55
  @click.option(
25
56
  "--inference-can-eval",
@@ -31,42 +62,42 @@ cli = AppGroup("typesync")
31
62
  )
32
63
  @click.option(
33
64
  "--types-file",
34
- help="Name of output file containing type definitions (defaults to 'types.ts')",
65
+ help="Name of output file containing type definitions (defaults to 'types.ts').",
35
66
  default="types.ts",
36
67
  )
37
68
  @click.option(
38
69
  "--apis-file",
39
- help="Name of output file containing API functions (defaults to 'apis.ts')",
70
+ help="Name of output file containing API functions (defaults to 'apis.ts').",
40
71
  default="apis.ts",
41
72
  )
42
73
  @click.option(
43
74
  "--return-type-format",
44
- default="{pc}ReturnType",
75
+ default="{r_pc}{m_uc}ReturnType",
45
76
  help=(
46
77
  "Format string used to generate return type names from the route name. "
47
78
  "Available placeholders are: "
48
- "{d} (default route name), "
49
- "{cc} (camelCase), "
50
- "{pc} (PascalCase), "
51
- "{uc} (UPPERCASE), "
52
- "{lc} (lowercase), "
53
- "{sc} (snake_case). "
54
- "Defaults to: '{pc}ReturnType'"
79
+ "{r_d} or {m_d} (default route name or HTTP method), "
80
+ "{r_cc} or {m_cc} (camelCase), "
81
+ "{r_pc} or {m_pc} (PascalCase), "
82
+ "{r_uc} or {m_uc} (UPPERCASE), "
83
+ "{r_lc} or {m_lc} (lowercase), "
84
+ "{r_sc} or {m_sc} (snake_case). "
85
+ "Defaults to: '{r_pc}{m_uc}ReturnType'."
55
86
  ),
56
87
  )
57
88
  @click.option(
58
89
  "--args-type-format",
59
- default="{pc}ArgsType",
90
+ default="{r_pc}{m_uc}ArgsType",
60
91
  help=(
61
92
  "Format string used to generate argument type names from the route name. "
62
93
  "Available placeholders are: "
63
- "{d} (default route name), "
64
- "{cc} (camelCase), "
65
- "{pc} (PascalCase), "
66
- "{uc} (UPPERCASE), "
67
- "{lc} (lowercase), "
68
- "{sc} (snake_case). "
69
- "Defaults to: '{pc}ArgsType'"
94
+ "{r_d} or {m_d} (default route name or HTTP method), "
95
+ "{r_cc} or {m_cc} (camelCase), "
96
+ "{r_pc} or {m_pc} (PascalCase), "
97
+ "{r_uc} or {m_uc} (UPPERCASE), "
98
+ "{r_lc} or {m_lc} (lowercase), "
99
+ "{r_sc} or {m_sc} (snake_case). "
100
+ "Defaults to: '{r_pc}{m_uc}ArgsType'."
70
101
  ),
71
102
  )
72
103
  @click.option(
@@ -81,14 +112,17 @@ cli = AppGroup("typesync")
81
112
  "{r_uc} or {m_uc} (UPPERCASE), "
82
113
  "{r_lc} or {m_lc} (lowercase), "
83
114
  "{r_sc} or {m_sc} (snake_case). "
84
- "Defaults to: '{m_lc}{r_pc}'"
115
+ "Defaults to: '{m_lc}{r_pc}'."
85
116
  ),
86
117
  )
87
118
  def generate(
88
119
  out_dir: str,
89
120
  endpoint: str,
121
+ translator: tuple[type["Translator"], ...],
122
+ translator_priority: tuple[tuple[str, int], ...],
90
123
  inference: bool,
91
124
  inference_can_eval: bool,
125
+ skip_unannotated: bool,
92
126
  types_file: str,
93
127
  apis_file: str,
94
128
  return_type_format: str,
@@ -114,13 +148,31 @@ def generate(
114
148
  endpoint,
115
149
  )
116
150
  result = code_writer.write(
117
- FlaskRouteTypeExtractor(
151
+ RouteTypeExtractor(
118
152
  current_app,
119
153
  rule,
154
+ translators=translator,
155
+ translator_priorities=dict(translator_priority),
120
156
  inference_enabled=inference,
121
157
  inference_can_eval=inference_can_eval,
158
+ skip_unannotated=skip_unannotated,
122
159
  )
123
160
  for rule in rules
124
161
  )
125
162
  if not result:
126
163
  click.secho("Errors occurred during file generation", fg="red")
164
+
165
+
166
+ @cli.command(help="Show available translators and their default priorities.")
167
+ def list_translators():
168
+ translators = RouteTypeExtractor.sort_translators(
169
+ RouteTypeExtractor.default_translators(), {}
170
+ )
171
+ table = PrettyTable()
172
+ table.field_names = ["ID", "Priority"]
173
+ table.add_rows(
174
+ [[translator.ID, translator.DEFAULT_PRIORITY] for translator in translators]
175
+ )
176
+ table.align["ID"] = "l"
177
+ table.align["Priority"] = "r"
178
+ print(table)
@@ -0,0 +1,7 @@
1
+ __all__ = [
2
+ "CodeWriter",
3
+ "RouteTypeExtractor",
4
+ ]
5
+
6
+ from .extractor import RouteTypeExtractor
7
+ from .writer import CodeWriter