typesync 0.0.1a1__py3-none-any.whl → 0.0.1a2__py3-none-any.whl

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.
typesync/__init__.py CHANGED
@@ -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()
typesync/cli.py CHANGED
@@ -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)
@@ -1,7 +1,7 @@
1
1
  __all__ = [
2
2
  "CodeWriter",
3
- "FlaskRouteTypeExtractor",
3
+ "RouteTypeExtractor",
4
4
  ]
5
5
 
6
- from .extractor import FlaskRouteTypeExtractor
6
+ from .extractor import RouteTypeExtractor
7
7
  from .writer import CodeWriter
@@ -12,9 +12,11 @@ from werkzeug.routing import (
12
12
  )
13
13
  from werkzeug.routing.rules import Rule
14
14
 
15
+
15
16
  from .inference import infer_return_type
17
+ from typesync.misc import HTTPMethod
16
18
  from typesync.ts_types import TSType, TSSimpleType, TSObject
17
- from typesync.type_translators import TypeNode, to_type_node
19
+ from typesync.type_translators import TranslationContext, TypeNode, to_type_node
18
20
 
19
21
  if typing.TYPE_CHECKING:
20
22
  from typesync.type_translators import Translator
@@ -51,30 +53,50 @@ def get_type_hints(tp: typing.Any) -> dict[str, typing.Any]:
51
53
  return getattr(tp, "__annotations__", {})
52
54
 
53
55
 
54
- class FlaskRouteTypeExtractor:
56
+ class RouteTypeExtractor:
55
57
  def __init__(
56
58
  self,
57
59
  app: Flask,
58
60
  rule: Rule,
59
- translators: tuple[type["Translator"]] | None = None,
61
+ translators: tuple[type["Translator"], ...] | None = None,
62
+ translator_priorities: dict[str, int] | None = None,
60
63
  inference_enabled: bool = False,
61
64
  inference_can_eval: bool = False,
65
+ skip_unannotated: bool = True,
62
66
  logger: Logger | None = None,
63
67
  ) -> None:
64
68
  self.app = app
65
69
  self.rule = rule
66
70
  self.inference_enabled = inference_enabled
67
71
  self.inference_can_eval = inference_can_eval
72
+ self.skip_unannotated = skip_unannotated
68
73
  self.logger = ClickLogger() if logger is None else logger
69
- self.translators = (
70
- self._load_default_translators() if translators is None else translators
74
+ self.translator_priorities = (
75
+ {} if translator_priorities is None else translator_priorities
76
+ )
77
+ self.translators = self.sort_translators(
78
+ (*self.default_translators(), *(translators or ())),
79
+ self.translator_priorities,
71
80
  )
72
81
 
73
82
  @staticmethod
74
- def _load_default_translators() -> tuple[type["Translator"], ...]:
75
- from typesync.type_translators import BaseTranslator, FlaskTranslator # noqa: PLC0415
83
+ def sort_translators[T: type["Translator"] | "Translator"](
84
+ translators: typing.Iterable[T],
85
+ priorities: dict[str, int],
86
+ ) -> tuple[T, ...]:
87
+ return tuple(
88
+ sorted(translators, key=lambda t: -priorities.get(t.ID, t.DEFAULT_PRIORITY))
89
+ )
76
90
 
77
- return (BaseTranslator, FlaskTranslator)
91
+ @staticmethod
92
+ def default_translators() -> tuple[type["Translator"], ...]:
93
+ from typesync.type_translators import ( # noqa: PLC0415
94
+ AnnotationsTranslator,
95
+ BaseTranslator,
96
+ FlaskTranslator,
97
+ )
98
+
99
+ return (AnnotationsTranslator, BaseTranslator, FlaskTranslator)
78
100
 
79
101
  @property
80
102
  def rule_name(self) -> str:
@@ -88,7 +110,7 @@ class FlaskRouteTypeExtractor:
88
110
  ]
89
111
  )
90
112
 
91
- def parse_args_type(self) -> TSType | None:
113
+ def parse_args_types(self) -> dict[HTTPMethod, TSType]:
92
114
  try:
93
115
  used_converters: dict[str, BaseConverter] = {
94
116
  arg: converter
@@ -98,22 +120,30 @@ class FlaskRouteTypeExtractor:
98
120
  )
99
121
  if converter is not None
100
122
  }
101
- types: list[tuple[str, TSType]] = [
102
- (arg, self._get_converter_type(arg, converter))
103
- for arg, converter in used_converters.items()
104
- ]
105
123
 
106
- return (
107
- TSObject([t[0] for t in types], [t[1] for t in types])
108
- if len(types) > 0
109
- else TSSimpleType("undefined")
110
- )
124
+ results: dict[HTTPMethod, TSType] = {}
125
+ for method in self.rule.methods or set():
126
+ types: list[tuple[str, TSType]] = [
127
+ (arg, self._get_converter_type(arg, converter, method))
128
+ for arg, converter in used_converters.items()
129
+ ]
130
+
131
+ results[method] = (
132
+ TSObject([t[0] for t in types], [t[1] for t in types])
133
+ if len(types) > 0
134
+ else TSSimpleType("undefined")
135
+ )
111
136
 
112
137
  except Exception as e:
113
138
  self.logger.error(f"couldn't parse argument types ({e})")
114
- return None
139
+ return {}
140
+
141
+ else:
142
+ return results
115
143
 
116
- def translate_type(self, type_: Type) -> tuple[TSType, str | None]:
144
+ def translate_type(
145
+ self, type_: Type, ctx: TranslationContext
146
+ ) -> tuple[TSType, str | None]:
117
147
  warning = None
118
148
 
119
149
  def translate(
@@ -131,52 +161,68 @@ class FlaskRouteTypeExtractor:
131
161
  )
132
162
  return TSSimpleType("any")
133
163
 
134
- translators = [Translator(translate) for Translator in self.translators]
164
+ translators = [Translator(translate, ctx) for Translator in self.translators]
135
165
  node = to_type_node(type_)
136
166
  return translate(node, {}), warning
137
167
 
138
- def parse_return_type(self) -> TSType | None:
168
+ def parse_return_types(self, force_inference=False) -> dict[HTTPMethod, TSType]:
139
169
  try:
140
170
  function = self.app.view_functions[self.rule.endpoint]
141
171
  annotations = get_type_hints(function)
142
- if annotations is not None and "return" in annotations:
143
- return_annotations = annotations["return"]
144
- route_return_annotations = self._get_route_annotations(
145
- return_annotations
146
- )
147
- return_type, warning = self.translate_type(route_return_annotations)
148
- else:
149
- return_type = warning = None
150
172
 
151
- if self.inference_enabled and (return_type is None or warning is not None):
152
- route_return_type = infer_return_type(
173
+ ctx = TranslationContext(
174
+ rule=self.rule,
175
+ view_function=function,
176
+ method="GET",
177
+ mode="RETURN",
178
+ inferred=False,
179
+ )
180
+
181
+ return_annotations = None
182
+ if (
183
+ not force_inference
184
+ and annotations is not None
185
+ and "return" in annotations
186
+ ):
187
+ return_annotations = annotations["return"]
188
+ elif self.inference_enabled:
189
+ return_annotations = infer_return_type(
153
190
  function, self.logger, self.inference_can_eval
154
191
  )
155
- if route_return_type is not None:
156
- return_type, warning = self.translate_type(
157
- self._get_route_annotations(route_return_type)
158
- )
192
+ ctx.inferred = True
193
+
194
+ if return_annotations is None and self.skip_unannotated:
195
+ return {}
159
196
 
160
- if warning is not None:
161
- self.logger.warning(warning)
197
+ route_annotations = self._get_route_annotations(return_annotations)
162
198
 
163
- return return_type or TSSimpleType("any")
199
+ results: dict[HTTPMethod, TSType] = {}
200
+ for method in self.rule.methods or set():
201
+ ctx.method = method
202
+ result, warning = self.translate_type(route_annotations, ctx)
203
+
204
+ if warning is not None and not ctx.inferred and self.inference_enabled:
205
+ return self.parse_return_types(force_inference=True)
206
+
207
+ results[method] = result or TSSimpleType("any")
208
+ if warning is not None:
209
+ self.logger.warning(warning)
164
210
 
165
211
  except Exception as e:
166
212
  self.logger.error(
167
213
  f"couldn't parse return type of '{self.rule.endpoint}' ({e})"
168
214
  )
169
- return None
215
+ return {}
170
216
 
171
217
  else:
172
- return TSSimpleType("any") if return_type is None else return_type
218
+ return results
173
219
 
174
- def parse_json_body(self) -> TSType | None:
220
+ def parse_json_body(self) -> dict[HTTPMethod, TSType]:
175
221
  try:
176
222
  function = self.app.view_functions[self.rule.endpoint]
177
223
  json_key = getattr(function, "_typesync", None)
178
224
  if json_key is None:
179
- return None
225
+ return {}
180
226
 
181
227
  annotations = get_type_hints(function)
182
228
  if json_key not in annotations:
@@ -184,21 +230,36 @@ class FlaskRouteTypeExtractor:
184
230
  f"'{self.rule.endpoint}' expected to receive JSON body as keyword "
185
231
  f"argument '{json_key}'"
186
232
  )
187
- return None
233
+ return {}
188
234
 
189
235
  json_body_annotations = annotations[json_key]
190
- json_body_type, warning = self.translate_type(json_body_annotations)
191
- if warning is not None:
192
- self.logger.warning(warning)
236
+
237
+ ctx = TranslationContext(
238
+ rule=self.rule,
239
+ view_function=function,
240
+ method="GET",
241
+ mode="JSON",
242
+ inferred=False,
243
+ )
244
+ results: dict[HTTPMethod, TSType] = {}
245
+
246
+ for method in self.rule.methods or set():
247
+ json_body_type, warning = self.translate_type(
248
+ json_body_annotations, ctx
249
+ )
250
+ if warning is not None:
251
+ self.logger.warning(warning)
252
+
253
+ results[method] = json_body_type
193
254
 
194
255
  except Exception as e:
195
256
  self.logger.error(
196
257
  f"couldn't parse JSON body type of '{self.rule.endpoint}' ({e})"
197
258
  )
198
- return None
259
+ return {}
199
260
 
200
261
  else:
201
- return json_body_type
262
+ return results
202
263
 
203
264
  def _get_route_annotations_from_tuple(self, tp: typing.Any) -> Type:
204
265
  args = typing.get_args(tp)
@@ -210,7 +271,9 @@ class FlaskRouteTypeExtractor:
210
271
  return self._get_route_annotations_from_tuple(tp)
211
272
  return tp
212
273
 
213
- def _get_converter_type(self, arg: str, converter: BaseConverter) -> TSType:
274
+ def _get_converter_type(
275
+ self, arg: str, converter: BaseConverter, method: HTTPMethod
276
+ ) -> TSType:
214
277
  if isinstance(converter, (FloatConverter, IntegerConverter)):
215
278
  return TSSimpleType("number")
216
279
  if isinstance(converter, (UUIDConverter, PathConverter, UnicodeConverter)):
@@ -225,8 +288,16 @@ class FlaskRouteTypeExtractor:
225
288
  )
226
289
  return TSSimpleType("string")
227
290
 
291
+ ctx = TranslationContext(
292
+ rule=self.rule,
293
+ view_function=self.app.view_functions[self.rule.endpoint],
294
+ method=method,
295
+ mode="ARGS",
296
+ inferred=False,
297
+ )
298
+
228
299
  return_annotations = annotations["return"]
229
- return_type, warning = self.translate_type(return_annotations)
300
+ return_type, warning = self.translate_type(return_annotations, ctx)
230
301
  if warning is not None:
231
302
  self.logger.warning(warning)
232
303
  return return_type
@@ -14,7 +14,7 @@ class ASTVisitor(ast.NodeVisitor):
14
14
  def __init__(
15
15
  self, function: typing.Callable, logger: "Logger", can_eval: bool = False
16
16
  ) -> None:
17
- self.function = function
17
+ self.function: typing.Callable = function
18
18
  self.logger = logger
19
19
  self.can_eval = can_eval
20
20
  self.locals: dict[str, typing.Any] = {}
@@ -24,7 +24,8 @@ class ASTVisitor(ast.NodeVisitor):
24
24
  local_var = self.locals.get(name.id, None)
25
25
  if local_var is not None:
26
26
  return local_var
27
- global_var = self.function.__globals__.get(name.id, None)
27
+ globals_dict = getattr(self.function, "__globals__", {})
28
+ global_var = globals_dict.get(name.id, None)
28
29
  if global_var is not None:
29
30
  return global_var
30
31
  builtin = getattr(builtins, name.id, None)
@@ -68,7 +69,11 @@ class ASTVisitor(ast.NodeVisitor):
68
69
  return tuple[types]
69
70
 
70
71
  def get_dict(self, dict_: ast.Dict) -> typing.Any:
71
- keys_type = self.get_type_if_all_equal(dict_.keys)
72
+ if None in dict_.keys:
73
+ # TODO: support unpacking
74
+ return dict
75
+ keys = typing.cast(list[ast.expr], dict_.keys)
76
+ keys_type = self.get_type_if_all_equal(keys)
72
77
  values_type = self.get_type_if_all_equal(dict_.values)
73
78
 
74
79
  if keys_type is None and values_type is None:
@@ -149,6 +154,10 @@ class ASTVisitor(ast.NodeVisitor):
149
154
  self.generic_visit(node)
150
155
 
151
156
  def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
157
+ target = node.target
158
+ if not isinstance(target, ast.Name):
159
+ # TODO
160
+ return
152
161
  annotation = self.get_value(node.annotation)
153
162
  if annotation is None and self.can_eval:
154
163
  annotation_string = ast.unparse(node.annotation)
@@ -159,14 +168,15 @@ class ASTVisitor(ast.NodeVisitor):
159
168
  0,
160
169
  )
161
170
  try:
162
- annotation = eval(annotation_code, self.function.__globals__, {}) # noqa: S307
171
+ function_globals = getattr(self.function, "__globals__", {})
172
+ annotation = eval(annotation_code, function_globals, {}) # noqa: S307
163
173
  except Exception as e:
164
174
  self.logger.warning(
165
175
  f"failed to parse annotation {annotation_string!r}: {e!s}"
166
176
  )
167
177
  annotation = None
168
178
  if annotation is not None:
169
- self.locals[node.target.id] = annotation
179
+ self.locals[target.id] = annotation
170
180
  self.generic_visit(node)
171
181
 
172
182
  def visit_Assign(self, node: ast.Assign) -> None:
@@ -2,10 +2,12 @@ import typing
2
2
 
3
3
  import inflection
4
4
 
5
+ from typesync.ts_types import TSType, TSSimpleType
6
+
5
7
  if typing.TYPE_CHECKING:
6
8
  from io import TextIOBase
7
- from typesync.ts_types import TSType
8
- from .extractor import FlaskRouteTypeExtractor
9
+ from typesync.misc import HTTPMethod
10
+ from .extractor import RouteTypeExtractor
9
11
 
10
12
 
11
13
  def make_rule_name_map(rule_name: str, prefix: str = "") -> dict[str, str]:
@@ -19,6 +21,12 @@ def make_rule_name_map(rule_name: str, prefix: str = "") -> dict[str, str]:
19
21
  }
20
22
 
21
23
 
24
+ class TypesDict(typing.TypedDict):
25
+ return_type: TSType
26
+ args_type: TSType
27
+ json_body_type: TSType
28
+
29
+
22
30
  class CodeWriter:
23
31
  def __init__(
24
32
  self,
@@ -45,41 +53,64 @@ class CodeWriter:
45
53
  {**make_rule_name_map(rule_name, "r_"), **make_rule_name_map(method, "m_")}
46
54
  )
47
55
 
48
- def _return_type_name(self, rule_name: str) -> str:
49
- return self.return_type_format.format_map(make_rule_name_map(rule_name))
56
+ def _return_type_name(self, rule_name: str, method: str) -> str:
57
+ return self.return_type_format.format_map(
58
+ {**make_rule_name_map(rule_name, "r_"), **make_rule_name_map(method, "m_")}
59
+ )
60
+
61
+ def _params_type_name(self, rule_name: str, method: str) -> str:
62
+ return self.params_type_format.format_map(
63
+ {**make_rule_name_map(rule_name, "r_"), **make_rule_name_map(method, "m_")}
64
+ )
50
65
 
51
- def _params_type_name(self, rule_name: str) -> str:
52
- return self.params_type_format.format_map(make_rule_name_map(rule_name))
66
+ def _bundle_types_per_method(
67
+ self,
68
+ return_types: dict["HTTPMethod", TSType],
69
+ args_types: dict["HTTPMethod", TSType],
70
+ json_body_types: dict["HTTPMethod", TSType],
71
+ ) -> dict["HTTPMethod", TypesDict]:
72
+ methods = {*return_types.keys(), *args_types.keys(), *json_body_types.keys()}
73
+ return {
74
+ method: TypesDict(
75
+ return_type=return_types.get(method, TSSimpleType("undefined")),
76
+ args_type=args_types.get(method, TSSimpleType("undefined")),
77
+ json_body_type=json_body_types.get(method, TSSimpleType("undefined")),
78
+ )
79
+ for method in methods
80
+ }
53
81
 
54
- def write(self, parsers: typing.Iterable["FlaskRouteTypeExtractor"]) -> bool:
82
+ def write(self, parsers: typing.Iterable["RouteTypeExtractor"]) -> bool:
55
83
  error = False
56
84
  self._write_types_header()
57
85
  self._write_api_header()
58
86
  names: list[str] = []
59
87
  for parser in parsers:
60
- return_type = parser.parse_return_type()
61
- args_type = parser.parse_args_type()
62
- json_body_type = parser.parse_json_body()
63
- if return_type is None or args_type is None:
64
- error = True
65
- if self.stop_on_error:
66
- return False
67
- continue
68
- return_type_name, params_type_name, has_args, has_json = self._write_types(
69
- parser.rule_name, return_type, args_type, json_body_type
88
+ return_types = parser.parse_return_types()
89
+ args_types = parser.parse_args_types()
90
+ json_body_types = parser.parse_json_body()
91
+
92
+ types_per_method = self._bundle_types_per_method(
93
+ return_types, args_types, json_body_types
70
94
  )
71
- names.extend(
72
- self._write_api_function(
73
- parser.rule_name,
74
- parser.rule_url,
75
- method,
76
- return_type_name,
77
- params_type_name,
78
- has_args,
79
- has_json,
95
+
96
+ for (
97
+ method,
98
+ return_type_name,
99
+ params_type_name,
100
+ has_args,
101
+ has_json,
102
+ ) in self._write_types(parser.rule_name, types_per_method):
103
+ names.append(
104
+ self._write_api_function(
105
+ parser.rule_name,
106
+ parser.rule_url,
107
+ method,
108
+ return_type_name,
109
+ params_type_name,
110
+ has_args,
111
+ has_json,
112
+ )
80
113
  )
81
- for method in (parser.rule.methods or {})
82
- )
83
114
  self._write_api_footer(names)
84
115
  return not error
85
116
 
@@ -153,43 +184,48 @@ class CodeWriter:
153
184
  def _write_types(
154
185
  self,
155
186
  rule_name: str,
156
- return_type: "TSType",
157
- args_type: "TSType",
158
- json_body_type: "TSType | None",
159
- ) -> tuple[str, str, bool, bool]:
160
- return_type_name = self._return_type_name(rule_name)
161
- self.types_file.write(
162
- f"export type {return_type_name} = {return_type.generate(return_type_name)}"
163
- ";\n"
164
- )
165
- params_type_name = self._params_type_name(rule_name)
166
- optional_args = "?" if args_type == "undefined" else ""
167
- optional_body = (
168
- "?" if json_body_type is None or json_body_type == "undefined" else ""
169
- )
187
+ types_per_method: dict["HTTPMethod", TypesDict],
188
+ ) -> typing.Generator[tuple[str, str, str, bool, bool], None, None]:
189
+ for method, types in types_per_method.items():
190
+ return_type_name = self._return_type_name(rule_name, method)
191
+ generated_return_type = types["return_type"].generate(return_type_name)
192
+ self.types_file.write(
193
+ f"export type {return_type_name} = {generated_return_type};\n"
194
+ )
195
+ params_type_name = self._params_type_name(rule_name, method)
196
+ optional_args = "?" if types["args_type"] == "undefined" else ""
197
+ json_body_type = types["json_body_type"]
198
+ optional_body = (
199
+ "?" if json_body_type is None or json_body_type == "undefined" else ""
200
+ )
170
201
 
171
- internal_args_name = f"_{rule_name}Args"
172
- self.types_file.write(
173
- f"type {internal_args_name} = {args_type.generate(internal_args_name)};\n"
174
- )
202
+ internal_args_name = f"_{rule_name}{method}Args"
203
+ generated_args_type = types["args_type"].generate(internal_args_name)
204
+ self.types_file.write(
205
+ f"type {internal_args_name} = {generated_args_type};\n"
206
+ )
175
207
 
176
- internal_body_name = f"_{rule_name}Body"
177
- string_json_body_type = (
178
- "undefined"
179
- if json_body_type is None
180
- else json_body_type.generate(internal_body_name)
181
- )
182
- self.types_file.write(f"type {internal_body_name} = {string_json_body_type};\n")
208
+ internal_body_name = f"_{rule_name}{method}Body"
209
+ string_json_body_type = (
210
+ "undefined"
211
+ if types["json_body_type"] is None
212
+ else types["json_body_type"].generate(internal_body_name)
213
+ )
214
+ self.types_file.write(
215
+ f"type {internal_body_name} = {string_json_body_type};\n"
216
+ )
183
217
 
184
- self.types_file.write(
185
- f"export interface {params_type_name} extends RequestArgs" + " {\n"
186
- f" args{optional_args}: {internal_args_name};\n"
187
- f" body{optional_body}: {internal_body_name};\n"
188
- "}\n\n"
189
- )
190
- return (
191
- return_type_name,
192
- params_type_name,
193
- optional_args == "",
194
- optional_body == "",
195
- )
218
+ self.types_file.write(
219
+ f"export interface {params_type_name} extends RequestArgs" + " {\n"
220
+ f" args{optional_args}: {internal_args_name};\n"
221
+ f" body{optional_body}: {internal_body_name};\n"
222
+ "}\n\n"
223
+ )
224
+
225
+ yield (
226
+ method,
227
+ return_type_name,
228
+ params_type_name,
229
+ optional_args == "",
230
+ optional_body == "",
231
+ )
typesync/misc.py ADDED
@@ -0,0 +1,7 @@
1
+ import typing
2
+
3
+ type HTTPMethodStrict = typing.Literal[
4
+ "GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"
5
+ ]
6
+
7
+ type HTTPMethod = HTTPMethodStrict | str
@@ -1,12 +1,16 @@
1
1
  __all__ = [
2
+ "AnnotationsTranslator",
2
3
  "BaseTranslator",
3
4
  "FlaskTranslator",
5
+ "TranslationContext",
4
6
  "Translator",
5
7
  "TypeNode",
6
8
  "to_type_node",
7
9
  ]
8
10
 
11
+ from .annotations_translator import AnnotationsTranslator
9
12
  from .base_translator import BaseTranslator
10
13
  from .flask_translator import FlaskTranslator
14
+ from .context import TranslationContext
11
15
  from .abstract import Translator
12
16
  from .type_node import TypeNode, to_type_node
@@ -1,19 +1,26 @@
1
1
  import abc
2
2
  import typing
3
3
 
4
+
4
5
  if typing.TYPE_CHECKING:
5
6
  from . import TypeNode
7
+ from .context import TranslationContext
6
8
  from typesync.ts_types import TSType
7
9
 
8
10
 
9
11
  class Translator(abc.ABC):
10
- @abc.abstractmethod
12
+ DEFAULT_PRIORITY: int = 0
13
+ ID: str
14
+
11
15
  def __init__(
12
16
  self,
13
- convert: typing.Callable[
17
+ translate: typing.Callable[
14
18
  ["TypeNode", dict[typing.TypeVar, "TSType"] | None], "TSType"
15
19
  ],
16
- ) -> None: ...
20
+ ctx: "TranslationContext",
21
+ ) -> None:
22
+ self._translate = translate
23
+ self.ctx = ctx
17
24
 
18
25
  @abc.abstractmethod
19
26
  def translate(
@@ -0,0 +1,47 @@
1
+ import typing
2
+
3
+
4
+ from .abstract import Translator
5
+ from .type_node import TypeNode
6
+ from typesync import annotations
7
+ from typesync.ts_types import TSSimpleType, TSType
8
+
9
+
10
+ class AnnotationsTranslator(Translator):
11
+ DEFAULT_PRIORITY = -100
12
+ ID = "typesync.AnnotationsTranslator"
13
+
14
+ def _translate_http_method_annotation(
15
+ self,
16
+ node: TypeNode,
17
+ annotation: annotations.TypesyncHTTPMethodAnnotation,
18
+ generics: dict[typing.TypeVar, TSType] | None,
19
+ ) -> TSType | None:
20
+ if self.ctx.method in annotation.methods:
21
+ return self._translate(node, generics)
22
+ return TSSimpleType("never")
23
+
24
+ def _translate_annotation(
25
+ self,
26
+ node: TypeNode,
27
+ annotation: typing.Any,
28
+ generics: dict[typing.TypeVar, TSType] | None,
29
+ ) -> TSType | None:
30
+ match annotation:
31
+ case annotations.TypesyncHTTPMethodAnnotation():
32
+ return self._translate_http_method_annotation(
33
+ node, annotation, generics
34
+ )
35
+
36
+ return self._translate(node, generics)
37
+
38
+ def translate(
39
+ self, node: TypeNode, generics: dict[typing.TypeVar, TSType] | None
40
+ ) -> TSType | None:
41
+ if node.origin is not typing.Annotated:
42
+ return None
43
+
44
+ if len(node.args) != 1:
45
+ return None
46
+
47
+ return self._translate_annotation(node.args[0], node.annotation, generics)
@@ -17,13 +17,8 @@ from typesync.ts_types import (
17
17
 
18
18
 
19
19
  class BaseTranslator(Translator):
20
- def __init__(
21
- self,
22
- translate: typing.Callable[
23
- [TypeNode, dict[typing.TypeVar, TSType] | None], TSType
24
- ],
25
- ):
26
- self._translate = translate
20
+ DEFAULT_PRIORITY = 0
21
+ ID = "typesync.BaseTranslator"
27
22
 
28
23
  def _unwrap_generic(
29
24
  self, node: TypeNode, generics: dict[typing.TypeVar, TSType]
@@ -71,6 +66,15 @@ class BaseTranslator(Translator):
71
66
  def _translate_complex_type(
72
67
  self, node: TypeNode, generics: dict[typing.TypeVar, TSType]
73
68
  ) -> TSType | None:
69
+ if node.origin not in {
70
+ dict,
71
+ list,
72
+ tuple,
73
+ types.UnionType,
74
+ typing.Union,
75
+ }:
76
+ return None
77
+
74
78
  translated_args = self._translate_args(node.args, generics)
75
79
 
76
80
  if node.origin is dict:
@@ -132,6 +136,9 @@ class BaseTranslator(Translator):
132
136
  if node.origin is RecursiveCall:
133
137
  return self._translate_recursive_call(node, generics)
134
138
 
139
+ if isinstance(node.origin, typing.TypeVar):
140
+ return generics.get(node.origin)
141
+
135
142
  if isinstance(node.origin, typing.TypeAliasType):
136
143
  return self._translate_type_alias(node, generics)
137
144
 
@@ -0,0 +1,16 @@
1
+ import dataclasses
2
+ import typing
3
+
4
+ from flask.typing import RouteCallable
5
+ from werkzeug.routing.rules import Rule
6
+
7
+ from typesync.misc import HTTPMethod
8
+
9
+
10
+ @dataclasses.dataclass
11
+ class TranslationContext:
12
+ rule: Rule
13
+ view_function: RouteCallable
14
+ method: HTTPMethod
15
+ mode: typing.Literal["JSON", "RETURN", "ARGS"]
16
+ inferred: bool
@@ -7,13 +7,8 @@ from typesync.utils import Response
7
7
 
8
8
 
9
9
  class FlaskTranslator(Translator):
10
- def __init__(
11
- self,
12
- translate: typing.Callable[
13
- [TypeNode, dict[typing.TypeVar, TSType] | None], TSType
14
- ],
15
- ) -> None:
16
- self._translate = translate
10
+ DEFAULT_PRIORITY = -10
11
+ ID = "typesync.FlaskTranslator"
17
12
 
18
13
  def translate(
19
14
  self, node: TypeNode, generics: dict[typing.TypeVar, TSType] | None
@@ -17,6 +17,7 @@ class TypeNode:
17
17
  args: tuple["TypeNode", ...]
18
18
  hints: dict[str, "TypeNode"]
19
19
  value: "TypeNode | None"
20
+ annotation: typing.Any
20
21
 
21
22
  def __repr__(self) -> str:
22
23
  s = f"<TypeNode {getattr(self.origin, '__name__', self.origin)}"
@@ -28,6 +29,8 @@ class TypeNode:
28
29
  s += f" hints={self.hints}"
29
30
  if self.value is not None:
30
31
  s += f" value={self.value}"
32
+ if self.annotation is not None:
33
+ s += f" annotation={self.annotation}"
31
34
  return s + ">"
32
35
 
33
36
 
@@ -37,9 +40,17 @@ def to_type_node(
37
40
  mapping = {} if mapping is None else mapping
38
41
  origin = typing.get_origin(type_) or type_
39
42
  params: tuple[typing.TypeVar, ...] = getattr(origin, "__type_params__", ())
40
- args = tuple(
41
- to_type_node(arg, original_type, mapping) for arg in typing.get_args(type_)
42
- )
43
+ args = ()
44
+ annotations = ()
45
+ if origin is not typing.Annotated:
46
+ args = tuple(
47
+ to_type_node(arg, original_type, mapping) for arg in typing.get_args(type_)
48
+ )
49
+ else:
50
+ base_args = typing.get_args(type_)
51
+ if len(base_args) > 0:
52
+ args = (to_type_node(base_args[0], original_type, mapping),)
53
+ annotations = base_args[1:]
43
54
  hints = {
44
55
  key: to_type_node(hint, original_type, mapping)
45
56
  for key, hint in getattr(origin, "__annotations__", {}).items()
@@ -61,11 +72,12 @@ def to_type_node(
61
72
  ):
62
73
  origin = RecursiveCall
63
74
 
64
- return TypeNode(
75
+ node = TypeNode(
65
76
  origin=origin,
66
77
  params=params,
67
78
  args=args,
68
79
  hints=hints,
80
+ annotation=annotations[0] if len(annotations) else None,
69
81
  value=(
70
82
  to_type_node(
71
83
  origin.__value__,
@@ -75,6 +87,7 @@ def to_type_node(
75
87
  args=patched_args,
76
88
  hints=hints,
77
89
  value=None,
90
+ annotation=(),
78
91
  ),
79
92
  mapping=mapping,
80
93
  )
@@ -82,3 +95,14 @@ def to_type_node(
82
95
  else None
83
96
  ),
84
97
  )
98
+ for annotation in annotations[1:]:
99
+ new_node = TypeNode(
100
+ origin=typing.Annotated,
101
+ params=params,
102
+ args=(node,),
103
+ hints={},
104
+ annotation=annotation,
105
+ value=None,
106
+ )
107
+ node = new_node
108
+ return node
@@ -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
@@ -0,0 +1,23 @@
1
+ typesync/__init__.py,sha256=cxlYkEhU2kEV8AB0elVJoS45IZkQ7FF-8el_NuY7c6o,267
2
+ typesync/annotations.py,sha256=ZGy_kcuXZMYV-Zv9CKcMJNH4y6fDpMS3BsnVvEedaAk,1135
3
+ typesync/argument_types.py,sha256=x_2kYlMq2fH7B_8wwmpWa_tcduU76fJmKlMH1s9txKQ,3687
4
+ typesync/cli.py,sha256=GdNDQYHBZ6lAbtYwMLFAZDdWIr73hbUmEA4TDY0beo8,5435
5
+ typesync/misc.py,sha256=ye2FQJC7cBU6RtEgGjMkZUXRAHGPOrQIYXwcuDVoNo0,182
6
+ typesync/ts_types.py,sha256=A5mJElZg7RyM58x9HGO9_XzXb9pznEPMDfO1yRUUzE8,3448
7
+ typesync/utils.py,sha256=0Mp8D9eLz12MJb5a3u2x0hqUffiLK-OUBpUhP4LuXv0,2131
8
+ typesync/codegen/__init__.py,sha256=mS7UpHXh6pQdRyXN3-KvRL0DgAulTE9hRDnqIvl6zQ8,132
9
+ typesync/codegen/extractor.py,sha256=Ho9hk2FnegNccXw3TN0-qmTdPNxMjg5blXXwwmyYsCA,10056
10
+ typesync/codegen/inference.py,sha256=U7t3a6LJldQPvpChOAtFffL0Tu2jB0063G3E4C81rbE,8320
11
+ typesync/codegen/writer.py,sha256=T_eliJ3d0Iu9B9bw3PmnsPajTN1sDOTag_t9-LKkuio,8500
12
+ typesync/type_translators/__init__.py,sha256=ugOt2XsDXc0B-Sp5HjbApZyyCmbAW4SKHKb2B6nCfz4,436
13
+ typesync/type_translators/abstract.py,sha256=WvMNcIMnzwy12eObck00GFklsMbr6bkPv1odQzkLCA4,652
14
+ typesync/type_translators/annotations_translator.py,sha256=LOFFBCK1-bIgscrtM7vVIaLFyM30fKC8CgUBf6_3AJE,1416
15
+ typesync/type_translators/base_translator.py,sha256=FSEt2mshN6QdUude65PvNfKgueRH6v2npLxNTlGSajk,5122
16
+ typesync/type_translators/context.py,sha256=qbffuT5UAmCbkN7KFzUPkYijQiuFl4PCCcv3uv1XpMc,343
17
+ typesync/type_translators/flask_translator.py,sha256=8CSg1VdszkfSksSDHEnXARHugExaov52hopqLhOzhEE,552
18
+ typesync/type_translators/type_node.py,sha256=ef02XyeHFn5EXJ59DYaQ8zA_MAfpoI1_1mF-CFHm1iM,3217
19
+ typesync-0.0.1a2.dist-info/licenses/LICENSE,sha256=7MsKRg4kFMMFoz-h2zVg6sZ1rxQU6HuvrECEz1evDAY,1059
20
+ typesync-0.0.1a2.dist-info/METADATA,sha256=jFoeoDYgxLMEAKN7quyj9TjDj9TPIHjEV6ScS844IL0,4376
21
+ typesync-0.0.1a2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
22
+ typesync-0.0.1a2.dist-info/top_level.txt,sha256=clLTcTcqWa7iQrgM1b_v3GHC1Z7W1uqiLUzXeGInWRs,9
23
+ typesync-0.0.1a2.dist-info/RECORD,,
@@ -1,18 +0,0 @@
1
- typesync/__init__.py,sha256=KhtqS2tUcczOFAMQIkuQdRi7bBY21IuwcMeR5V45g6g,222
2
- typesync/cli.py,sha256=ATa0O1Gr2q9j1MxfmcBrIWij6-6iiAkIW1OC5mQLKM0,3655
3
- typesync/ts_types.py,sha256=A5mJElZg7RyM58x9HGO9_XzXb9pznEPMDfO1yRUUzE8,3448
4
- typesync/utils.py,sha256=0Mp8D9eLz12MJb5a3u2x0hqUffiLK-OUBpUhP4LuXv0,2131
5
- typesync/codegen/__init__.py,sha256=TD41Nti-hXO7xJi9HHr4QTFKIY4Wv_3QJtLSl718lpw,142
6
- typesync/codegen/extractor.py,sha256=HK8-FbmO-memDtZEbNsV3ukuBo2oC7GD9lspFZgYL3M,7938
7
- typesync/codegen/inference.py,sha256=JZcictBq_jDGW6bJycQi53oQtJkkNBdk5cDp2WYXj60,7934
8
- typesync/codegen/writer.py,sha256=a8mE3cdKmpBmXLnqM0IkpXwZlRc713E_AzQtTHnaEmY,7051
9
- typesync/type_translators/__init__.py,sha256=mNpgJLMZW_p_7bp3l23gZ6BUrqSt_mrjXJixhOKZzeI,283
10
- typesync/type_translators/abstract.py,sha256=nTg6sjJCxXGJD5hW9FAtgel8ZUWJPNgD6y7q7xUy54o,496
11
- typesync/type_translators/base_translator.py,sha256=wD9PUkBLJP8gz5l5WoelP2m3evJyXgQrdUocUgT628Q,4976
12
- typesync/type_translators/flask_translator.py,sha256=PezojREhL9GShJn4cXHrmoxs3Rrsgpk0miopqUB3nzQ,687
13
- typesync/type_translators/type_node.py,sha256=wF60KWXuXtNExq_ZlcxHj99rEnIw0VyxtNGhSW6Vhrk,2428
14
- typesync-0.0.1a1.dist-info/licenses/LICENSE,sha256=7MsKRg4kFMMFoz-h2zVg6sZ1rxQU6HuvrECEz1evDAY,1059
15
- typesync-0.0.1a1.dist-info/METADATA,sha256=MPEuQMs0m8NUKDUATdI9IlNOHzLvxCNAV0sR8MQHcaw,4323
16
- typesync-0.0.1a1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
17
- typesync-0.0.1a1.dist-info/top_level.txt,sha256=clLTcTcqWa7iQrgM1b_v3GHC1Z7W1uqiLUzXeGInWRs,9
18
- typesync-0.0.1a1.dist-info/RECORD,,