msra-codegen 0.1.0__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.
Files changed (68) hide show
  1. msra_codegen/README.md +23 -0
  2. msra_codegen/__init__.py +6 -0
  3. msra_codegen/__main__.py +5 -0
  4. msra_codegen/bridge.py +29 -0
  5. msra_codegen/cli.py +105 -0
  6. msra_codegen/codegen_context.py +1690 -0
  7. msra_codegen/config.toml +164 -0
  8. msra_codegen/core_naming.py +155 -0
  9. msra_codegen/docs_generator.py +346 -0
  10. msra_codegen/file_utils.py +8 -0
  11. msra_codegen/funcresult.py +156 -0
  12. msra_codegen/generator.py +6 -0
  13. msra_codegen/generator_config.py +35 -0
  14. msra_codegen/github_workflows.py +129 -0
  15. msra_codegen/gitignore.py +31 -0
  16. msra_codegen/issue_templates.py +100 -0
  17. msra_codegen/logo_assets.py +99 -0
  18. msra_codegen/msra_serializer.py +205 -0
  19. msra_codegen/node_export.js +296 -0
  20. msra_codegen/package_metadata.py +306 -0
  21. msra_codegen/package_writer.py +175 -0
  22. msra_codegen/project_model.py +490 -0
  23. msra_codegen/python_formatting.py +88 -0
  24. msra_codegen/python_render.py +242 -0
  25. msra_codegen/readme_pipeline.py +519 -0
  26. msra_codegen/requirements.txt +5 -0
  27. msra_codegen/template_engine.py +26 -0
  28. msra_codegen/templates/Makefile.tpl +44 -0
  29. msra_codegen/templates/README.md.tpl +55 -0
  30. msra_codegen/templates/abstraction/__init__.py.tpl +188 -0
  31. msra_codegen/templates/abstraction/regexes.py.tpl +25 -0
  32. msra_codegen/templates/docs/requirements.txt.tpl +3 -0
  33. msra_codegen/templates/docs/source/Makefile.tpl +20 -0
  34. msra_codegen/templates/docs/source/api.rst.tpl +9 -0
  35. msra_codegen/templates/docs/source/conf.py.tpl +88 -0
  36. msra_codegen/templates/docs/source/index.rst.tpl +14 -0
  37. msra_codegen/templates/docs/source/module.rst.tpl +34 -0
  38. msra_codegen/templates/docs/source/quick_start.rst.tpl +19 -0
  39. msra_codegen/templates/endpoints_init.py.tpl +15 -0
  40. msra_codegen/templates/example.py.tpl +1 -0
  41. msra_codegen/templates/function.py.tpl +364 -0
  42. msra_codegen/templates/github/issue_templates/bug_report.yml.tpl +55 -0
  43. msra_codegen/templates/github/issue_templates/config.yml.tpl +8 -0
  44. msra_codegen/templates/github/issue_templates/documentation_issue.yml.tpl +33 -0
  45. msra_codegen/templates/github/issue_templates/feature_request.yml.tpl +36 -0
  46. msra_codegen/templates/github/workflows/publish.yml.tpl +100 -0
  47. msra_codegen/templates/github/workflows/source-sync.yml.tpl +177 -0
  48. msra_codegen/templates/github/workflows/tests.yml.tpl +69 -0
  49. msra_codegen/templates/gitignore.tpl +3 -0
  50. msra_codegen/templates/group.py.tpl +56 -0
  51. msra_codegen/templates/group_init.py.tpl +14 -0
  52. msra_codegen/templates/init.py.tpl +4 -0
  53. msra_codegen/templates/licenses/GPL-3.0-or-later.txt.tpl +674 -0
  54. msra_codegen/templates/licenses/MIT.txt.tpl +21 -0
  55. msra_codegen/templates/manager.py.tpl +257 -0
  56. msra_codegen/templates/pyproject.toml.tpl +38 -0
  57. msra_codegen/templates/tests/api_test.py.tpl +49 -0
  58. msra_codegen/templates/tests/conftest.py.tpl +21 -0
  59. msra_codegen/templates/variable.py.tpl +54 -0
  60. msra_codegen/tests_generator.py +988 -0
  61. msra_codegen/typespec.py +275 -0
  62. msra_codegen/validation.py +118 -0
  63. msra_codegen-0.1.0.dist-info/METADATA +47 -0
  64. msra_codegen-0.1.0.dist-info/RECORD +68 -0
  65. msra_codegen-0.1.0.dist-info/WHEEL +5 -0
  66. msra_codegen-0.1.0.dist-info/entry_points.txt +2 -0
  67. msra_codegen-0.1.0.dist-info/licenses/LICENSE +674 -0
  68. msra_codegen-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) {{ year }} {{ copyright_holders }}
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,257 @@
1
+ from collections import defaultdict
2
+ from dataclasses import dataclass, field
3
+ from time import perf_counter, time
4
+ from typing import Any, cast
5
+ {% if has_autotests %}
6
+ from human_requests import autotest
7
+ {% endif %}
8
+ {% if uses_classvar_import %}
9
+ from typing import ClassVar
10
+ {% endif %}
11
+ {% if uses_literal_import %}
12
+ from typing import Literal
13
+ {% endif %}
14
+ {% if imports.overload %}
15
+ from typing import overload
16
+ {% endif %}
17
+ {% if imports.json %}
18
+ import json
19
+ {% endif %}
20
+ {% if imports.path %}
21
+ from pathlib import Path
22
+ {% endif %}
23
+ {% if imports.re %}
24
+ import re
25
+ {% endif %}
26
+ {% if imports.urlencode %}
27
+ from urllib.parse import urlencode
28
+ {% endif %}
29
+
30
+ from aiohttp_retry import ExponentialRetry, RetryClient
31
+ from camoufox import AsyncCamoufox, DefaultAddons
32
+ from human_requests import HumanBrowser, HumanPage
33
+ from human_requests.abstraction import HttpMethod, Proxy, Warmup
34
+ {% if imports.method_pipeline_error %}
35
+ from human_requests.abstraction import MethodPipelineError
36
+ {% endif %}
37
+ {% if uses_warmup_error_import %}
38
+ from human_requests.abstraction import WarmupError
39
+ {% endif %}
40
+ from human_requests.network_analyzer.anomaly_sniffer import HeaderAnomalySniffer
41
+
42
+ from . import abstraction
43
+ {% for group in top_groups %}
44
+ from .endpoints.{{ group.module_name }} import {{ group.class_name }}
45
+ {% endfor %}
46
+
47
+
48
+ @dataclass
49
+ class {{ client_class_name }}:
50
+ {% if app_description %}
51
+ """{{ app_description }}"""
52
+
53
+ {% endif %}
54
+ timeout_ms: int = {{ app.timeout_ms }}
55
+ """Global timeout, in milliseconds, used by warmup and browser-backed requests."""
56
+ headless: bool = {{ "False" if app.disallow_headless else "True" }}
57
+ """Whether the browser is started without a visible window."""
58
+ test_mode: bool = False
59
+ """Enable the test-only warmup branch and its extra state."""
60
+ proxy: str | dict | Proxy | None = None
61
+ """Proxy settings for browser startup and direct requests. When omitted or set to None, the client reads the proxy from the environment."""
62
+ browser_opts: dict[str, Any] | None = None
63
+ """Extra keyword arguments forwarded to AsyncCamoufox during browser startup."""
64
+ {% if has_root_functions %}
65
+ _parent: Any = field(init=False, repr=False)
66
+ {% endif %}
67
+
68
+ {% for prefix in prefixes %}
69
+ {{ prefix.attr_name }}: ClassVar[str] = {{ prefix.value }}
70
+ {% endfor %}
71
+
72
+ {% for group in top_groups %}
73
+ {{ group.field_name }}: {{ group.class_name }} = field(init=False)
74
+ {% if group.description %}
75
+ """{{ group.description }}"""
76
+ {% endif %}
77
+ {% endfor %}
78
+
79
+ def __post_init__(self):
80
+ self.proxy = Proxy.from_env() if self.proxy is None else self.proxy
81
+ browser_opts: dict[str, Any] = {} if self.browser_opts is None else dict(self.browser_opts)
82
+ self.browser_opts = browser_opts
83
+ {% if has_root_functions %}
84
+ self._parent = self
85
+ {% endif %}
86
+ self.session = None
87
+ self.ctx = None
88
+ self.page = None
89
+ self.unstandard_headers = {}
90
+ self.unstandard_urls = {}
91
+
92
+ {% for variable in variables %}
93
+ self.{{ variable.backing_name }} = None
94
+ {% endfor %}
95
+
96
+ {% for group in top_groups %}
97
+ self.{{ group.field_name }} = {{ group.class_name }}(self)
98
+ {% endfor %}
99
+
100
+ {% for func in functions %}
101
+ {{ func.code }}
102
+
103
+ {% endfor %}
104
+ async def __aenter__(self):
105
+ await self._warmup()
106
+ return self
107
+
108
+ async def _warmup(self) -> None:
109
+ {% if app.disallow_headless %}
110
+ if self.headless:
111
+ raise ValueError("headless=True is not allowed when @DisallowHeadless is set")
112
+ {% endif %}
113
+ px = self.proxy if isinstance(self.proxy, Proxy) else Proxy(self.proxy)
114
+ browser_opts: dict[str, Any] = {} if self.browser_opts is None else dict(self.browser_opts)
115
+ br = await AsyncCamoufox(
116
+ headless=self.headless,
117
+ proxy=px.as_dict(),
118
+ humanize={{ app.humanize }},
119
+ **browser_opts,
120
+ block_images={{ app.block_images }},
121
+ i_know_what_im_doing=True,
122
+ exclude_addons=[DefaultAddons.UBO],
123
+ ).start()
124
+
125
+ self.session = HumanBrowser.replace(cast(Any, br))
126
+ self.ctx = await self.session.new_context()
127
+ self.page = await self.ctx.new_page()
128
+ self.page.on_error_screenshot_path = {{ warmup.on_error_screenshot_path }}
129
+
130
+ {% if warmup.headers_sniffer %}
131
+ sniffer = HeaderAnomalySniffer(
132
+ include_subresources=True,
133
+ )
134
+ await sniffer.start(self.ctx)
135
+
136
+ {% else %}
137
+ sniffer = None
138
+
139
+ {% endif %}
140
+ {% if warmup.script_module and warmup.script_function and warmup.script_path_expr %}
141
+ warmup = self._make_warmup_context(page=self.page, sniffer=sniffer)
142
+ from .{{ warmup.script_module }} import {{ warmup.script_function }} as warmup_runner
143
+ try:
144
+ await warmup_runner(warmup)
145
+ except WarmupError:
146
+ raise
147
+ except Exception as exc:
148
+ raise WarmupError(str(exc)) from exc
149
+ {% endif %}
150
+
151
+ result_sniffer: dict[str, Any] = await sniffer.complete() if sniffer else {"request": {}}
152
+
153
+ result = defaultdict(set)
154
+
155
+ for _url, headers in result_sniffer.get("request", {}).items():
156
+ for header, values in headers.items():
157
+ result[header].update(values)
158
+
159
+ self.unstandard_headers = {k: list(v)[0] for k, v in result.items()}
160
+ {% for variable in variables %}
161
+ {{ variable.warmup_code }}
162
+
163
+ {% endfor %}
164
+ self.unstandard_urls = result_sniffer.get("request", {})
165
+
166
+ async def __aexit__(self, *exc):
167
+ await self.close()
168
+
169
+ async def close(self):
170
+ await self.session.close()
171
+
172
+ def _make_warmup_context(
173
+ self,
174
+ *,
175
+ page: HumanPage,
176
+ sniffer: HeaderAnomalySniffer | None,
177
+ ) -> Warmup:
178
+ return Warmup(
179
+ browser=self.session,
180
+ context=self.ctx,
181
+ page=page,
182
+ sniffer=sniffer,
183
+ timeout_ms=self.timeout_ms,
184
+ test_mode=self.test_mode,
185
+ prefixes={
186
+ {% for prefix in prefixes %}
187
+ {{ prefix.name | tojson }}: self.{{ prefix.attr_name }},
188
+ {% endfor %}
189
+ },
190
+ )
191
+
192
+ async def _create_pipeline_sniffer(self) -> HeaderAnomalySniffer:
193
+ sniffer = HeaderAnomalySniffer(
194
+ include_subresources=True,
195
+ )
196
+ await sniffer.start(self.ctx)
197
+ return sniffer
198
+
199
+ {% for variable in variables %}
200
+ {{ variable.code }}
201
+
202
+ {% endfor %}
203
+ async def _request(
204
+ self,
205
+ method: HttpMethod,
206
+ url: str,
207
+ *,
208
+ json_body: Any | None = None,
209
+ mode: str | None = None,
210
+ credentials: str | None = None,
211
+ referrer: str | None = None,
212
+ headers: dict[str, Any] | None = None,
213
+ ) -> abstraction.Output:
214
+ request_headers = headers if headers is not None else {{ request.headers_expr }}
215
+ fetch_kwargs: dict[str, Any] = {
216
+ "url": url,
217
+ "method": method,
218
+ "body": json_body,
219
+ "mode": mode if mode is not None else {{ request.cors_mode_expr }},
220
+ "credentials": credentials if credentials is not None else {{ request.credentials_expr }},
221
+ "timeout_ms": self.timeout_ms,
222
+ "headers": request_headers,
223
+ }
224
+ if referrer is not None:
225
+ fetch_kwargs["referrer"] = referrer
226
+ response = await self.page.fetch(**fetch_kwargs)
227
+ return abstraction.Output.from_fetch_response(response)
228
+
229
+ async def _direct_request(
230
+ self,
231
+ url: str,
232
+ *,
233
+ retry_attempts: int = 3,
234
+ timeout: float = 10,
235
+ ) -> abstraction.Output:
236
+ start_t = perf_counter()
237
+ retry_options = ExponentialRetry(
238
+ attempts=retry_attempts, start_timeout=3.0, max_timeout=timeout
239
+ )
240
+ px = self.proxy if isinstance(self.proxy, Proxy) else Proxy(self.proxy)
241
+ async with (
242
+ RetryClient(retry_options=retry_options) as retry_client,
243
+ retry_client.get(url, raise_for_status=True, proxy=px.as_str()) as resp,
244
+ ):
245
+ body = await resp.read()
246
+ return abstraction.Output.from_raw(
247
+ body,
248
+ url=str(resp.url),
249
+ headers=dict(resp.headers),
250
+ status_code=resp.status,
251
+ status_text=resp.reason,
252
+ redirected=bool(resp.history),
253
+ response_type="basic",
254
+ duration=perf_counter() - start_t,
255
+ end_time=time(),
256
+ page=self.page,
257
+ )
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "{{ package_name }}"
7
+ dynamic = ["version"]
8
+ description = {{ description }}
9
+ readme = "README.md"
10
+ requires-python = "{{ requires_python }}"
11
+ license = "{{ license }}"
12
+ {{ authors_block }}
13
+ {{ keywords_block }}
14
+ {{ classifiers_block }}
15
+ {{ dependencies_block }}
16
+ {{ mypy_block }}
17
+ {{ ruff_block }}
18
+
19
+ [tool.setuptools]
20
+ include-package-data = true
21
+
22
+ [tool.setuptools.package-data]
23
+ {{ package_name }} = ["extractors/*.js", "extractors/**/*.js"]
24
+
25
+ [tool.setuptools.dynamic]
26
+ version = { attr = "{{ package_name }}.__version__" }
27
+
28
+ [tool.pytest.ini_options]
29
+ pythonpath = ["."]
30
+ testpaths = ["tests"]
31
+ python_files = ["*_test.py", "*_tests.py"]
32
+ filterwarnings = [
33
+ "ignore::pytest.PytestUnraisableExceptionWarning",
34
+ "ignore:Event loop is closed:RuntimeWarning",
35
+ ]
36
+ anyio_mode = "auto"
37
+ autotest_start_class = "{{ autotest_start_class }}"
38
+ addopts = "-v --tb=short --disable-warnings"
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ {% if hooks or providers or data_cases %}
4
+ from typing import TYPE_CHECKING
5
+ {% endif %}
6
+
7
+ {% if providers %}
8
+ import pytest
9
+ {% endif %}
10
+ {% if hooks or providers %}
11
+ from human_requests import autotest_depends_on, autotest_hook, autotest_params
12
+ {% endif %}
13
+ {% if data_cases %}
14
+ from human_requests import autotest_data
15
+ {% endif %}
16
+
17
+ {% for import in imports %}
18
+ from {{ import.module }} import {{ import.class_name }}
19
+ {% endfor %}
20
+
21
+ {% if hooks or providers or data_cases %}
22
+ if TYPE_CHECKING:
23
+ {% if hooks %}
24
+ from human_requests.autotest import AutotestContext
25
+ {% endif %}
26
+ {% if providers %}
27
+ from human_requests.autotest import AutotestCallContext
28
+ {% endif %}
29
+ {% if data_cases %}
30
+ from human_requests.autotest import AutotestDataContext
31
+ {% endif %}
32
+
33
+ {% endif %}
34
+ {% for hook in hooks %}
35
+ {{ hook.hook_code }}
36
+
37
+ {% endfor %}
38
+ {% for provider in providers %}
39
+ {{ provider.provider_code }}
40
+
41
+ {% endfor %}
42
+ {% for data_case in data_cases %}
43
+ {{ data_case.code }}
44
+
45
+ {% endfor %}
46
+ {% for manual_test in manual_tests %}
47
+ {{ manual_test.code }}
48
+
49
+ {% endfor %}
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from {{ package_name }} import {{ client_class_name }}
6
+
7
+
8
+ @pytest.fixture(scope="session")
9
+ def anyio_backend():
10
+ return "asyncio"
11
+
12
+
13
+ @pytest.fixture(scope="session")
14
+ async def api():
15
+ async with {{ client_class_name }}(test_mode=True) as client:
16
+ yield client
17
+ {{ "\n" if fixtures else "" }}
18
+ {% for fixture in fixtures %}
19
+ {{ fixture.code }}
20
+ {{ "\n" if not loop.last else "" }}
21
+ {% endfor %}
@@ -0,0 +1,54 @@
1
+ @property
2
+ def {{ name }}(self) -> {{ getter_return }}:
3
+ {% if description %}
4
+ """{{ description }}"""
5
+ {% endif %}
6
+ return self.{{ backing_name }}
7
+
8
+ {% if setter_enabled %}
9
+ @{{ name }}.setter
10
+ def {{ name }}(self, value: {{ getter_return }}) -> None:
11
+ {% if has_null %}
12
+ if value is None:
13
+ self.{{ backing_name }} = None
14
+ return
15
+
16
+ {% endif %}
17
+ {% if has_integer %}
18
+ if not isinstance(value, int) or isinstance(value, bool):
19
+ raise TypeError("`{{ name }}` must be int")
20
+ {% elif has_boolean %}
21
+ if not isinstance(value, bool):
22
+ raise TypeError("`{{ name }}` must be bool")
23
+ {% elif has_number %}
24
+ if not isinstance(value, (int, float)) or isinstance(value, bool):
25
+ raise TypeError("`{{ name }}` must be number")
26
+ {% else %}
27
+ if not isinstance(value, str):
28
+ raise TypeError("`{{ name }}` must be str")
29
+ {% endif %}
30
+ {% if match_check_expr %}
31
+ if not ({{ match_check_expr }}):
32
+ {% if match_error %}
33
+ raise ValueError({{ match_error }})
34
+ {% else %}
35
+ raise ValueError("`{{ name }}` does not match the expected format")
36
+ {% endif %}
37
+ {% elif match_range_lower is not none or match_range_upper is not none %}
38
+ {% if match_range_lower is not none and match_range_upper is not none %}
39
+ if float(value) < {{ match_range_lower }} or float(value) > {{ match_range_upper }}:
40
+ raise ValueError("`{{ name }}` must be between {{ match_range_lower }} and {{ match_range_upper }}")
41
+ {% elif match_range_lower is not none %}
42
+ if float(value) < {{ match_range_lower }}:
43
+ raise ValueError("`{{ name }}` must be greater than or equal to {{ match_range_lower }}")
44
+ {% else %}
45
+ if float(value) > {{ match_range_upper }}:
46
+ raise ValueError("`{{ name }}` must be less than or equal to {{ match_range_upper }}")
47
+ {% endif %}
48
+ {% elif match_values_expr %}
49
+ allowed_values = {{ match_values_expr }}
50
+ if value not in allowed_values:
51
+ raise ValueError(f"`{{ name }}` must be one of {allowed_values}")
52
+ {% endif %}
53
+ self.{{ backing_name }} = value
54
+ {% endif %}