polyapi-python 0.3.12.dev1__tar.gz → 0.3.13__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.
- {polyapi_python-0.3.12.dev1/polyapi_python.egg-info → polyapi_python-0.3.13}/PKG-INFO +9 -1
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/README.md +8 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi/config.py +2 -1
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi/deployables.py +2 -1
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi/parser.py +13 -3
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi/poly_tables.py +109 -41
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi/utils.py +16 -2
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi/webhook.py +1 -5
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13/polyapi_python.egg-info}/PKG-INFO +9 -1
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/pyproject.toml +4 -6
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/tests/test_tabi.py +86 -40
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/LICENSE +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi/__init__.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi/__main__.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi/api.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi/auth.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi/cli.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi/client.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi/constants.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi/error_handler.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi/exceptions.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi/execute.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi/function_cli.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi/generate.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi/poly_schemas.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi/prepare.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi/py.typed +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi/rendered_spec.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi/schema.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi/server.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi/sync.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi/typedefs.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi/variables.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi_python.egg-info/SOURCES.txt +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi_python.egg-info/dependency_links.txt +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi_python.egg-info/requires.txt +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi_python.egg-info/top_level.txt +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/setup.cfg +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/tests/test_api.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/tests/test_auth.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/tests/test_deployables.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/tests/test_generate.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/tests/test_parser.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/tests/test_rendered_spec.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/tests/test_schema.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/tests/test_server.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/tests/test_utils.py +0 -0
- {polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/tests/test_variables.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: polyapi-python
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.13
|
|
4
4
|
Summary: The Python Client for PolyAPI, the IPaaS by Developers for Developers
|
|
5
5
|
Author-email: Dan Fellin <dan@polyapi.io>
|
|
6
6
|
License: MIT License
|
|
@@ -185,6 +185,14 @@ This script is handy for checking for any mypy types:
|
|
|
185
185
|
|
|
186
186
|
Please ignore \[name-defined\] errors for now. This is a known bug we are working to fix!
|
|
187
187
|
|
|
188
|
+
## Strategies for QA'ing Changes To Generate Or Other Core Functionality
|
|
189
|
+
|
|
190
|
+
Our https://na1.polyapi.io has a large OOB catalog (as does eu1/na2). We also have several big internal PolyAPI projects with Python (message @eupharis if you need a pointer to which ones).
|
|
191
|
+
|
|
192
|
+
Running `python -m polyapi generate` in all these projects and then checking the flake8 and check_mypy steps above is a great way to build confidence that the `generate` changes has no gotchas.
|
|
193
|
+
|
|
194
|
+
Of course all this is in addition to the changes passing through normal unittests and integration tests!
|
|
195
|
+
|
|
188
196
|
## Support
|
|
189
197
|
|
|
190
198
|
If you run into any issues or want help getting started with this project, please contact support@polyapi.io
|
|
@@ -145,6 +145,14 @@ This script is handy for checking for any mypy types:
|
|
|
145
145
|
|
|
146
146
|
Please ignore \[name-defined\] errors for now. This is a known bug we are working to fix!
|
|
147
147
|
|
|
148
|
+
## Strategies for QA'ing Changes To Generate Or Other Core Functionality
|
|
149
|
+
|
|
150
|
+
Our https://na1.polyapi.io has a large OOB catalog (as does eu1/na2). We also have several big internal PolyAPI projects with Python (message @eupharis if you need a pointer to which ones).
|
|
151
|
+
|
|
152
|
+
Running `python -m polyapi generate` in all these projects and then checking the flake8 and check_mypy steps above is a great way to build confidence that the `generate` changes has no gotchas.
|
|
153
|
+
|
|
154
|
+
Of course all this is in addition to the changes passing through normal unittests and integration tests!
|
|
155
|
+
|
|
148
156
|
## Support
|
|
149
157
|
|
|
150
158
|
If you run into any issues or want help getting started with this project, please contact support@polyapi.io
|
|
@@ -96,7 +96,8 @@ def initialize_config(force=False):
|
|
|
96
96
|
if not key:
|
|
97
97
|
key = input("? Poly App Key or User Key: ").strip()
|
|
98
98
|
else:
|
|
99
|
-
|
|
99
|
+
display_key = '*' * 8 + key[-4:]
|
|
100
|
+
key_input = input(f"? Poly App Key or User Key ({display_key}): ").strip()
|
|
100
101
|
key = key_input if key_input else key
|
|
101
102
|
|
|
102
103
|
if url and key:
|
|
@@ -6,6 +6,7 @@ import json
|
|
|
6
6
|
import hashlib
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import TypedDict, List, Dict, Tuple, Optional, Any, Union
|
|
9
|
+
from typing_extensions import Required
|
|
9
10
|
from subprocess import check_output, CalledProcessError
|
|
10
11
|
|
|
11
12
|
|
|
@@ -54,7 +55,7 @@ class DeployableRecord(ParsedDeployableConfig, total=False):
|
|
|
54
55
|
gitRevision: str
|
|
55
56
|
fileRevision: str
|
|
56
57
|
file: str
|
|
57
|
-
types: DeployableFunctionTypes
|
|
58
|
+
types: Required[DeployableFunctionTypes]
|
|
58
59
|
typeSchemas: Dict[str, Any]
|
|
59
60
|
dependencies: List[str]
|
|
60
61
|
deployments: List[Deployment]
|
|
@@ -3,7 +3,7 @@ import json
|
|
|
3
3
|
import types
|
|
4
4
|
import sys
|
|
5
5
|
import re
|
|
6
|
-
from typing import Dict, List, Mapping, Optional, Tuple, Any
|
|
6
|
+
from typing import Dict, List, Mapping, Optional, Tuple, Any, Union
|
|
7
7
|
from typing import _TypedDictMeta as BaseTypedDict # type: ignore
|
|
8
8
|
from typing_extensions import _TypedDictMeta, cast # type: ignore
|
|
9
9
|
from stdlib_list import stdlib_list
|
|
@@ -390,7 +390,7 @@ def parse_function_code(code: str, name: Optional[str] = "", context: Optional[s
|
|
|
390
390
|
deployable["config"] = _parse_dict(node.value)
|
|
391
391
|
self._name = deployable["config"]["name"]
|
|
392
392
|
|
|
393
|
-
def _extract_docstring_from_function(self, node: ast.FunctionDef):
|
|
393
|
+
def _extract_docstring_from_function(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]):
|
|
394
394
|
start_lineno = (node.body[0].lineno if node.body else node.lineno) - 1
|
|
395
395
|
start_offset = self._line_offsets[start_lineno]
|
|
396
396
|
end_offset = start_offset
|
|
@@ -452,15 +452,19 @@ def parse_function_code(code: str, name: Optional[str] = "", context: Optional[s
|
|
|
452
452
|
for name in node.names:
|
|
453
453
|
req = _get_req_name_if_not_in_base(name.name, pip_name_lookup)
|
|
454
454
|
if req:
|
|
455
|
+
if "dependencies" not in deployable or deployable["dependencies"] is None:
|
|
456
|
+
deployable["dependencies"] = []
|
|
455
457
|
deployable["dependencies"].append(req)
|
|
456
458
|
|
|
457
459
|
def visit_ImportFrom(self, node: ast.ImportFrom):
|
|
458
460
|
if node.module:
|
|
459
461
|
req = _get_req_name_if_not_in_base(node.module, pip_name_lookup)
|
|
460
462
|
if req:
|
|
463
|
+
if "dependencies" not in deployable or deployable["dependencies"] is None:
|
|
464
|
+
deployable["dependencies"] = []
|
|
461
465
|
deployable["dependencies"].append(req)
|
|
462
466
|
|
|
463
|
-
def
|
|
467
|
+
def _handle_function_def(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]):
|
|
464
468
|
if node.name == self._name:
|
|
465
469
|
# Parse docstring which may contain param types and descriptions
|
|
466
470
|
self._extract_docstring_from_function(node)
|
|
@@ -506,6 +510,12 @@ def parse_function_code(code: str, name: Optional[str] = "", context: Optional[s
|
|
|
506
510
|
else:
|
|
507
511
|
deployable["types"]["returns"]["type"] = "Any"
|
|
508
512
|
|
|
513
|
+
def visit_FunctionDef(self, node: ast.FunctionDef):
|
|
514
|
+
self._handle_function_def(node)
|
|
515
|
+
|
|
516
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
|
|
517
|
+
self._handle_function_def(node)
|
|
518
|
+
|
|
509
519
|
def generic_visit(self, node):
|
|
510
520
|
if hasattr(node, 'lineno') and hasattr(node, 'col_offset'):
|
|
511
521
|
self._current_offset = self._line_offsets[node.lineno - 1] + node.col_offset
|
|
@@ -1,27 +1,64 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import requests
|
|
3
3
|
from typing_extensions import NotRequired, TypedDict
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import (
|
|
5
|
+
List,
|
|
6
|
+
Union,
|
|
7
|
+
Type,
|
|
8
|
+
Dict,
|
|
9
|
+
Any,
|
|
10
|
+
Literal,
|
|
11
|
+
Tuple,
|
|
12
|
+
Optional,
|
|
13
|
+
get_args,
|
|
14
|
+
get_origin,
|
|
15
|
+
)
|
|
5
16
|
from polyapi.utils import add_import_to_init, init_the_init
|
|
6
17
|
from polyapi.typedefs import TableSpecDto
|
|
7
18
|
from polyapi.constants import JSONSCHEMA_TO_PYTHON_TYPE_MAP
|
|
19
|
+
from polyapi.config import get_api_key_and_url
|
|
8
20
|
|
|
9
|
-
|
|
10
|
-
|
|
21
|
+
TABI_MODULE_IMPORTS = "\n".join(
|
|
22
|
+
[
|
|
23
|
+
"from typing_extensions import NotRequired, TypedDict",
|
|
24
|
+
"from typing import Union, List, Dict, Any, Literal, Optional, Required, overload",
|
|
25
|
+
"from polyapi.poly_tables import execute_query, first_result, transform_query, delete_one_response",
|
|
26
|
+
"from polyapi.typedefs import Table, PolyCountResult, PolyDeleteResult, PolyDeleteResults, SortOrder, StringFilter, NullableStringFilter, NumberFilter, NullableNumberFilter, BooleanFilter, NullableBooleanFilter, NullableObjectFilter",
|
|
27
|
+
]
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def scrub(data: Any) -> Any:
|
|
32
|
+
if not data or not isinstance(data, (Dict, List)):
|
|
33
|
+
return data
|
|
11
34
|
if isinstance(data, List):
|
|
12
35
|
return [scrub(item) for item in data]
|
|
13
36
|
else:
|
|
14
37
|
temp = {}
|
|
15
|
-
secrets = [
|
|
38
|
+
secrets = [
|
|
39
|
+
"x_api_key",
|
|
40
|
+
"x-api-key",
|
|
41
|
+
"access_token",
|
|
42
|
+
"access-token",
|
|
43
|
+
"authorization",
|
|
44
|
+
"api_key",
|
|
45
|
+
"api-key",
|
|
46
|
+
"apikey",
|
|
47
|
+
"accesstoken",
|
|
48
|
+
"token",
|
|
49
|
+
"password",
|
|
50
|
+
"key",
|
|
51
|
+
]
|
|
16
52
|
for key, value in data.items():
|
|
17
53
|
if isinstance(value, (Dict, List)):
|
|
18
54
|
temp[key] = scrub(data[key])
|
|
19
55
|
elif key.lower() in secrets:
|
|
20
|
-
temp[key] =
|
|
56
|
+
temp[key] = "********"
|
|
21
57
|
else:
|
|
22
58
|
temp[key] = data[key]
|
|
23
59
|
return temp
|
|
24
60
|
|
|
61
|
+
|
|
25
62
|
def scrub_keys(e: Exception) -> Dict[str, Any]:
|
|
26
63
|
"""
|
|
27
64
|
Scrub the keys of an exception to remove sensitive information.
|
|
@@ -31,18 +68,26 @@ def scrub_keys(e: Exception) -> Dict[str, Any]:
|
|
|
31
68
|
"error": str(e),
|
|
32
69
|
"type": type(e).__name__,
|
|
33
70
|
"message": str(e),
|
|
34
|
-
"args": scrub(getattr(e,
|
|
71
|
+
"args": scrub(getattr(e, "args", None)),
|
|
35
72
|
}
|
|
36
73
|
|
|
37
74
|
|
|
38
75
|
def execute_query(table_id, method, query):
|
|
39
76
|
from polyapi import polyCustom
|
|
40
77
|
from polyapi.poly.client_id import client_id
|
|
78
|
+
|
|
41
79
|
try:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
80
|
+
api_key, base_url = get_api_key_and_url()
|
|
81
|
+
if not base_url:
|
|
82
|
+
raise ValueError(
|
|
83
|
+
"PolyAPI Instance URL is not configured, run `python -m polyapi setup`."
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
auth_key = polyCustom.get("executionApiKey") or api_key
|
|
87
|
+
url = f"{base_url.rstrip('/')}/tables/{table_id}/{method}?clientId={client_id}"
|
|
88
|
+
headers = {"x-poly-execution-id": polyCustom.get("executionId")}
|
|
89
|
+
if auth_key:
|
|
90
|
+
headers["Authorization"] = f"Bearer {auth_key}"
|
|
46
91
|
response = requests.post(url, json=query, headers=headers)
|
|
47
92
|
response.raise_for_status()
|
|
48
93
|
return response.json()
|
|
@@ -51,14 +96,16 @@ def execute_query(table_id, method, query):
|
|
|
51
96
|
|
|
52
97
|
|
|
53
98
|
def first_result(rsp):
|
|
54
|
-
if isinstance(rsp, dict) and isinstance(rsp.get(
|
|
55
|
-
return rsp[
|
|
99
|
+
if isinstance(rsp, dict) and isinstance(rsp.get("results"), list):
|
|
100
|
+
return rsp["results"][0] if rsp["results"] else None
|
|
56
101
|
return rsp
|
|
57
102
|
|
|
103
|
+
|
|
58
104
|
def delete_one_response(rsp):
|
|
59
|
-
if isinstance(rsp, dict) and isinstance(rsp.get(
|
|
60
|
-
return {
|
|
61
|
-
return {
|
|
105
|
+
if isinstance(rsp, dict) and isinstance(rsp.get("deleted"), int):
|
|
106
|
+
return {"deleted": bool(rsp.get("deleted"))}
|
|
107
|
+
return {"deleted": False}
|
|
108
|
+
|
|
62
109
|
|
|
63
110
|
_key_transform_map = {
|
|
64
111
|
"not_": "not",
|
|
@@ -72,8 +119,7 @@ _key_transform_map = {
|
|
|
72
119
|
def _transform_keys(obj: Any) -> Any:
|
|
73
120
|
if isinstance(obj, dict):
|
|
74
121
|
return {
|
|
75
|
-
_key_transform_map.get(k, k): _transform_keys(v)
|
|
76
|
-
for k, v in obj.items()
|
|
122
|
+
_key_transform_map.get(k, k): _transform_keys(v) for k, v in obj.items()
|
|
77
123
|
}
|
|
78
124
|
|
|
79
125
|
elif isinstance(obj, list):
|
|
@@ -88,13 +134,13 @@ def transform_query(query: dict) -> dict:
|
|
|
88
134
|
return {
|
|
89
135
|
**query,
|
|
90
136
|
"where": _transform_keys(query["where"]) if query["where"] else None,
|
|
91
|
-
"orderBy": query["order_by"] if query["order_by"] else None
|
|
137
|
+
"orderBy": query["order_by"] if query["order_by"] else None,
|
|
92
138
|
}
|
|
93
139
|
|
|
94
140
|
return query
|
|
95
141
|
|
|
96
142
|
|
|
97
|
-
TABI_TABLE_TEMPLATE =
|
|
143
|
+
TABI_TABLE_TEMPLATE = """
|
|
98
144
|
{table_name}Columns = Literal[{table_columns}]
|
|
99
145
|
|
|
100
146
|
|
|
@@ -369,7 +415,7 @@ class {table_name}:{table_description}
|
|
|
369
415
|
query["where"]["id"] = kwargs["id"]
|
|
370
416
|
query.pop("id", None)
|
|
371
417
|
return delete_one_response(execute_query({table_name}.table_id, "delete", transform_query(query)))
|
|
372
|
-
|
|
418
|
+
"""
|
|
373
419
|
|
|
374
420
|
|
|
375
421
|
def _get_column_type_str(name: str, schema: Dict[str, Any], is_required: bool) -> str:
|
|
@@ -377,11 +423,17 @@ def _get_column_type_str(name: str, schema: Dict[str, Any], is_required: bool) -
|
|
|
377
423
|
|
|
378
424
|
col_type = schema.get("type", "object")
|
|
379
425
|
if isinstance(col_type, list):
|
|
380
|
-
subtypes = [
|
|
426
|
+
subtypes = [
|
|
427
|
+
_get_column_type_str(name, {**schema, "type": t}, is_required)
|
|
428
|
+
for t in col_type
|
|
429
|
+
]
|
|
381
430
|
result = f"Union[{', '.join(subtypes)}]"
|
|
382
431
|
elif col_type == "array":
|
|
383
432
|
if isinstance(schema["items"], list):
|
|
384
|
-
subtypes = [
|
|
433
|
+
subtypes = [
|
|
434
|
+
_get_column_type_str(f"{name}{i}", s, True)
|
|
435
|
+
for i, s in enumerate(schema["items"])
|
|
436
|
+
]
|
|
385
437
|
result = f"Tuple[{', '.join(subtypes)}]"
|
|
386
438
|
elif isinstance(schema["items"], dict):
|
|
387
439
|
result = f"List[{_get_column_type_str(name, schema['items'], True)}]"
|
|
@@ -391,7 +443,10 @@ def _get_column_type_str(name: str, schema: Dict[str, Any], is_required: bool) -
|
|
|
391
443
|
if isinstance(schema.get("patternProperties"), dict):
|
|
392
444
|
# TODO: Handle multiple pattern properties
|
|
393
445
|
result = f"Dict[str, {_get_column_type_str(f'{name}_', schema['patternProperties'], True)}]"
|
|
394
|
-
elif
|
|
446
|
+
elif (
|
|
447
|
+
isinstance(schema.get("properties"), dict)
|
|
448
|
+
and len(schema["properties"].values()) > 0
|
|
449
|
+
):
|
|
395
450
|
# TODO: Handle x-poly-refs
|
|
396
451
|
result = f'"{name}"'
|
|
397
452
|
else:
|
|
@@ -413,24 +468,32 @@ def _render_table_row_classes(table_name: str, schema: Dict[str, Any]) -> str:
|
|
|
413
468
|
return output[1].split("\n", 1)[1].strip()
|
|
414
469
|
|
|
415
470
|
|
|
416
|
-
def _render_table_subset_class(
|
|
471
|
+
def _render_table_subset_class(
|
|
472
|
+
table_name: str, columns: List[Tuple[str, Dict[str, Any]]], required: List[str]
|
|
473
|
+
) -> str:
|
|
417
474
|
# Generate class which can match any subset of a table row
|
|
418
475
|
lines = [f"class {table_name}Subset(TypedDict):"]
|
|
419
476
|
|
|
420
477
|
for name, schema in columns:
|
|
421
|
-
type_str = _get_column_type_str(
|
|
478
|
+
type_str = _get_column_type_str(
|
|
479
|
+
f"_{table_name}Row{name}", schema, name in required
|
|
480
|
+
)
|
|
422
481
|
lines.append(f" {name}: NotRequired[{type_str}]")
|
|
423
482
|
|
|
424
483
|
return "\n".join(lines)
|
|
425
484
|
|
|
426
485
|
|
|
427
|
-
def _render_table_where_class(
|
|
486
|
+
def _render_table_where_class(
|
|
487
|
+
table_name: str, columns: List[Tuple[str, Dict[str, Any]]], required: List[str]
|
|
488
|
+
) -> str:
|
|
428
489
|
# Generate class for the 'where' part of the query
|
|
429
490
|
lines = [f"class {table_name}WhereFilter(TypedDict):"]
|
|
430
491
|
|
|
431
492
|
for name, schema in columns:
|
|
432
493
|
ftype_str = ""
|
|
433
|
-
type_str = _get_column_type_str(
|
|
494
|
+
type_str = _get_column_type_str(
|
|
495
|
+
f"_{table_name}Row{name}", schema, True
|
|
496
|
+
) # force required to avoid wrapping type in Optional[]
|
|
434
497
|
is_required = name in required
|
|
435
498
|
if type_str == "bool":
|
|
436
499
|
ftype_str = "BooleanFilter" if is_required else "NullableBooleanFilter"
|
|
@@ -445,24 +508,34 @@ def _render_table_where_class(table_name: str, columns: List[Tuple[str, Dict[str
|
|
|
445
508
|
if ftype_str:
|
|
446
509
|
lines.append(f" {name}: NotRequired[Union[{type_str}, {ftype_str}]]")
|
|
447
510
|
|
|
448
|
-
lines.append(
|
|
511
|
+
lines.append(
|
|
512
|
+
f' AND: NotRequired[Union["{table_name}WhereFilter", List["{table_name}WhereFilter"]]]'
|
|
513
|
+
)
|
|
449
514
|
lines.append(f' OR: NotRequired[List["{table_name}WhereFilter"]]')
|
|
450
|
-
lines.append(
|
|
515
|
+
lines.append(
|
|
516
|
+
f' NOT: NotRequired[Union["{table_name}WhereFilter", List["{table_name}WhereFilter"]]]'
|
|
517
|
+
)
|
|
451
518
|
|
|
452
519
|
return "\n".join(lines)
|
|
453
520
|
|
|
454
521
|
|
|
455
522
|
def _render_table(table: TableSpecDto) -> str:
|
|
456
523
|
columns = list(table["schema"]["properties"].items())
|
|
457
|
-
|
|
524
|
+
required_columns = table["schema"].get("required", [])
|
|
458
525
|
|
|
459
|
-
table_columns = ",".join([
|
|
526
|
+
table_columns = ",".join([f'"{k}"' for k, _ in columns])
|
|
460
527
|
table_row_classes = _render_table_row_classes(table["name"], table["schema"])
|
|
461
|
-
table_row_subset_class = _render_table_subset_class(
|
|
462
|
-
|
|
528
|
+
table_row_subset_class = _render_table_subset_class(
|
|
529
|
+
table["name"], columns, required_columns
|
|
530
|
+
)
|
|
531
|
+
table_where_class = _render_table_where_class(
|
|
532
|
+
table["name"], columns, required_columns
|
|
533
|
+
)
|
|
463
534
|
if table.get("description", ""):
|
|
464
|
-
table_description =
|
|
465
|
-
table_description +=
|
|
535
|
+
table_description = '\n """'
|
|
536
|
+
table_description += "\n ".join(
|
|
537
|
+
table["description"].replace('"', "'").split("\n")
|
|
538
|
+
)
|
|
466
539
|
table_description += '\n """'
|
|
467
540
|
else:
|
|
468
541
|
table_description = ""
|
|
@@ -502,12 +575,7 @@ def _create_table(table: TableSpecDto) -> None:
|
|
|
502
575
|
|
|
503
576
|
init_path = os.path.join(full_path, "__init__.py")
|
|
504
577
|
|
|
505
|
-
imports =
|
|
506
|
-
"from typing_extensions import NotRequired, TypedDict",
|
|
507
|
-
"from typing import Union, List, Dict, Any, Literal, Optional, Required, overload",
|
|
508
|
-
"from polyapi.poly_tables import execute_query, first_result, transform_query",
|
|
509
|
-
"from polyapi.typedefs import Table, PolyCountResult, PolyDeleteResults, SortOrder, StringFilter, NullableStringFilter, NumberFilter, NullableNumberFilter, BooleanFilter, NullableBooleanFilter, NullableObjectFilter",
|
|
510
|
-
])
|
|
578
|
+
imports = TABI_MODULE_IMPORTS
|
|
511
579
|
table_contents = _render_table(table)
|
|
512
580
|
|
|
513
581
|
file_contents = ""
|
|
@@ -73,9 +73,23 @@ def print_red(s: str):
|
|
|
73
73
|
|
|
74
74
|
def add_type_import_path(function_name: str, arg: str) -> str:
|
|
75
75
|
"""if not basic type, coerce to camelCase and add the import path"""
|
|
76
|
-
# for now, just treat Callables as basic types
|
|
76
|
+
# outdated og comment - for now, just treat Callables as basic types
|
|
77
|
+
# from now, we start qualifying non-basic types :))
|
|
78
|
+
# e.g. Callable[[EmailAddress, Dict, Dict, Dict], None]
|
|
79
|
+
# becomes Callable[[Set_profile_email.EmailAddress, Dict, Dict, Dict], None]
|
|
80
|
+
|
|
77
81
|
if arg.startswith("Callable"):
|
|
78
|
-
|
|
82
|
+
inner = arg[len("Callable["):-1] # strip outer Callable[...]
|
|
83
|
+
parts = [p.strip() for p in inner.split(",")]
|
|
84
|
+
qualified = []
|
|
85
|
+
for p in parts:
|
|
86
|
+
clean = p.strip("[] ")
|
|
87
|
+
if clean and clean not in BASIC_PYTHON_TYPES:
|
|
88
|
+
replacement = f"{to_func_namespace(function_name)}.{camelCase(clean)}"
|
|
89
|
+
p = p.replace(clean, replacement)
|
|
90
|
+
qualified.append(p)
|
|
91
|
+
return "Callable[" + ",".join(qualified) + "]"
|
|
92
|
+
# return arg
|
|
79
93
|
|
|
80
94
|
if arg in BASIC_PYTHON_TYPES:
|
|
81
95
|
return arg
|
|
@@ -124,11 +124,7 @@ def render_webhook_handle(
|
|
|
124
124
|
) -> Tuple[str, str]:
|
|
125
125
|
try:
|
|
126
126
|
function_args, function_args_def = parse_arguments(function_name, arguments)
|
|
127
|
-
|
|
128
|
-
if "WebhookEventType" in function_args:
|
|
129
|
-
# let's add the function name import!
|
|
130
|
-
function_args = function_args.replace("WebhookEventType", f"{to_func_namespace(function_name)}.WebhookEventType")
|
|
131
|
-
|
|
127
|
+
|
|
132
128
|
func_str = WEBHOOK_TEMPLATE.format(
|
|
133
129
|
description=function_description,
|
|
134
130
|
function_id=function_id,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: polyapi-python
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.13
|
|
4
4
|
Summary: The Python Client for PolyAPI, the IPaaS by Developers for Developers
|
|
5
5
|
Author-email: Dan Fellin <dan@polyapi.io>
|
|
6
6
|
License: MIT License
|
|
@@ -185,6 +185,14 @@ This script is handy for checking for any mypy types:
|
|
|
185
185
|
|
|
186
186
|
Please ignore \[name-defined\] errors for now. This is a known bug we are working to fix!
|
|
187
187
|
|
|
188
|
+
## Strategies for QA'ing Changes To Generate Or Other Core Functionality
|
|
189
|
+
|
|
190
|
+
Our https://na1.polyapi.io has a large OOB catalog (as does eu1/na2). We also have several big internal PolyAPI projects with Python (message @eupharis if you need a pointer to which ones).
|
|
191
|
+
|
|
192
|
+
Running `python -m polyapi generate` in all these projects and then checking the flake8 and check_mypy steps above is a great way to build confidence that the `generate` changes has no gotchas.
|
|
193
|
+
|
|
194
|
+
Of course all this is in addition to the changes passing through normal unittests and integration tests!
|
|
195
|
+
|
|
188
196
|
## Support
|
|
189
197
|
|
|
190
198
|
If you run into any issues or want help getting started with this project, please contact support@polyapi.io
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
[build-system]
|
|
2
2
|
requires = ["setuptools>=61.2", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
3
4
|
|
|
4
5
|
[project]
|
|
5
6
|
name = "polyapi-python"
|
|
6
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.13"
|
|
7
8
|
description = "The Python Client for PolyAPI, the IPaaS by Developers for Developers"
|
|
8
9
|
authors = [{ name = "Dan Fellin", email = "dan@polyapi.io" }]
|
|
9
10
|
dependencies = [
|
|
@@ -23,14 +24,11 @@ requires-python = ">=3.10"
|
|
|
23
24
|
[project.urls]
|
|
24
25
|
Homepage = "https://github.com/polyapi/polyapi-python"
|
|
25
26
|
|
|
26
|
-
[tool.setuptools]
|
|
27
|
-
packages = ["polyapi"]
|
|
28
|
-
|
|
29
|
-
[tools.setuptools.packages.find]
|
|
27
|
+
[tool.setuptools.packages.find]
|
|
30
28
|
include = ["polyapi"]
|
|
31
29
|
exclude = ["polyapi/poly*", "polyapi/vari*", "polyapi/.config.env", "polyapi/cached_deployables*", "polyapi/deployments_revision"] # exclude the generated libraries from builds
|
|
32
30
|
|
|
33
31
|
[tool.mypy]
|
|
34
32
|
# for now redef errors happen sometimes, we will clean this up in the future!
|
|
35
33
|
disable_error_code = "no-redef,name-defined"
|
|
36
|
-
implicit_optional = true
|
|
34
|
+
implicit_optional = true
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import unittest
|
|
2
|
-
from
|
|
2
|
+
from unittest.mock import Mock, patch
|
|
3
|
+
from polyapi.poly_tables import _render_table, TABI_MODULE_IMPORTS, execute_query
|
|
4
|
+
from polyapi.typedefs import TableSpecDto
|
|
3
5
|
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
def _normalize_type_notation(value: str) -> str:
|
|
8
|
+
# Python/runtime/tooling versions may emit either built-in generic style
|
|
9
|
+
# (dict[str, Any]) or typing style (Dict[str, Any]) for the same schema.
|
|
10
|
+
return value.replace("dict[str, Any]", "Dict[str, Any]")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
TABLE_SPEC_SIMPLE: TableSpecDto = {
|
|
6
14
|
"type": "table",
|
|
7
15
|
"id": "123456789",
|
|
8
16
|
"name": "MyTable",
|
|
@@ -13,24 +21,18 @@ TABLE_SPEC_SIMPLE = {
|
|
|
13
21
|
"$schema": "http://json-schema.org/draft-06/schema#",
|
|
14
22
|
"type": "object",
|
|
15
23
|
"properties": {
|
|
16
|
-
"id": {
|
|
17
|
-
"createdAt": {
|
|
18
|
-
"updatedAt": {
|
|
19
|
-
"name": {
|
|
20
|
-
"age": {
|
|
21
|
-
"active": {
|
|
22
|
-
"optional": {
|
|
24
|
+
"id": {"type": "string"},
|
|
25
|
+
"createdAt": {"type": "string"},
|
|
26
|
+
"updatedAt": {"type": "string"},
|
|
27
|
+
"name": {"type": "string"},
|
|
28
|
+
"age": {"type": "integer"},
|
|
29
|
+
"active": {"type": "boolean"},
|
|
30
|
+
"optional": {"type": "object"},
|
|
23
31
|
},
|
|
24
|
-
"required": [
|
|
25
|
-
"id",
|
|
26
|
-
"createdAt",
|
|
27
|
-
"updatedAt",
|
|
28
|
-
"name",
|
|
29
|
-
"age",
|
|
30
|
-
"active"
|
|
31
|
-
],
|
|
32
|
+
"required": ["id", "createdAt", "updatedAt", "name", "age", "active"],
|
|
32
33
|
"additionalProperties": False,
|
|
33
|
-
}
|
|
34
|
+
},
|
|
35
|
+
"unresolvedPolySchemaRefs": [],
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
EXPECTED_SIMPLE = '''
|
|
@@ -57,7 +59,7 @@ class MyTableRow(TypedDict, total=False):
|
|
|
57
59
|
active: Required[bool]
|
|
58
60
|
""" Required property """
|
|
59
61
|
|
|
60
|
-
optional:
|
|
62
|
+
optional: Dict[str, Any]
|
|
61
63
|
|
|
62
64
|
|
|
63
65
|
|
|
@@ -351,43 +353,40 @@ class MyTable:
|
|
|
351
353
|
return delete_one_response(execute_query(MyTable.table_id, "delete", transform_query(query)))
|
|
352
354
|
'''
|
|
353
355
|
|
|
354
|
-
TABLE_SPEC_COMPLEX = {
|
|
356
|
+
TABLE_SPEC_COMPLEX: TableSpecDto = {
|
|
355
357
|
"type": "table",
|
|
356
358
|
"id": "123456789",
|
|
357
359
|
"name": "MyTable",
|
|
358
360
|
"context": "some.context.here",
|
|
359
361
|
"contextName": "some.context.here.MyTable",
|
|
362
|
+
"description": "",
|
|
360
363
|
"schema": {
|
|
361
364
|
"$schema": "http://json-schema.org/draft-06/schema#",
|
|
362
365
|
"type": "object",
|
|
363
366
|
"properties": {
|
|
364
|
-
"id": {
|
|
365
|
-
"createdAt": {
|
|
366
|
-
"updatedAt": {
|
|
367
|
+
"id": {"type": "string"},
|
|
368
|
+
"createdAt": {"type": "string"},
|
|
369
|
+
"updatedAt": {"type": "string"},
|
|
367
370
|
"data": {
|
|
368
371
|
"type": "object",
|
|
369
372
|
"properties": {
|
|
370
|
-
"foo": {
|
|
373
|
+
"foo": {"type": "string"},
|
|
371
374
|
"nested": {
|
|
372
375
|
"type": "array",
|
|
373
376
|
"items": {
|
|
374
377
|
"type": "object",
|
|
375
|
-
"properties": {
|
|
376
|
-
"required": ["name"]
|
|
377
|
-
}
|
|
378
|
+
"properties": {"name": {"type": "string"}},
|
|
379
|
+
"required": ["name"],
|
|
380
|
+
},
|
|
378
381
|
},
|
|
379
|
-
"other": {
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
+
"other": {"x-poly-ref": {"path": "some.other.Schema"}},
|
|
383
|
+
},
|
|
384
|
+
},
|
|
382
385
|
},
|
|
383
|
-
"required": [
|
|
384
|
-
"id",
|
|
385
|
-
"createdAt",
|
|
386
|
-
"updatedAt",
|
|
387
|
-
"data"
|
|
388
|
-
],
|
|
386
|
+
"required": ["id", "createdAt", "updatedAt", "data"],
|
|
389
387
|
"additionalProperties": False,
|
|
390
|
-
}
|
|
388
|
+
},
|
|
389
|
+
"unresolvedPolySchemaRefs": [],
|
|
391
390
|
}
|
|
392
391
|
|
|
393
392
|
EXPECTED_COMPLEX = '''
|
|
@@ -657,14 +656,61 @@ class MyTable:
|
|
|
657
656
|
return execute_query(MyTable.table_id, "delete", query)
|
|
658
657
|
'''
|
|
659
658
|
|
|
659
|
+
|
|
660
660
|
class T(unittest.TestCase):
|
|
661
661
|
def test_render_simple(self):
|
|
662
662
|
self.maxDiff = 20000
|
|
663
663
|
output = _render_table(TABLE_SPEC_SIMPLE)
|
|
664
|
-
self.assertEqual(
|
|
665
|
-
|
|
664
|
+
self.assertEqual(
|
|
665
|
+
_normalize_type_notation(output),
|
|
666
|
+
_normalize_type_notation(EXPECTED_SIMPLE),
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
def test_execute_query_does_not_return_unhashable_dict_error(self):
|
|
670
|
+
result = execute_query("test-table", "select", {})
|
|
671
|
+
self.assertIsInstance(result, dict)
|
|
672
|
+
self.assertNotIn("unhashable type: 'dict'", str(result))
|
|
673
|
+
|
|
674
|
+
def test_execute_query_uses_absolute_url_and_auth_header(self):
|
|
675
|
+
response = Mock()
|
|
676
|
+
response.raise_for_status.return_value = None
|
|
677
|
+
response.json.return_value = {"ok": True}
|
|
678
|
+
|
|
679
|
+
with (
|
|
680
|
+
patch(
|
|
681
|
+
"polyapi.poly_tables.get_api_key_and_url",
|
|
682
|
+
return_value=("test-api-key", "https://na1.polyapi.io"),
|
|
683
|
+
),
|
|
684
|
+
patch(
|
|
685
|
+
"polyapi.poly_tables.requests.post", return_value=response
|
|
686
|
+
) as post_mock,
|
|
687
|
+
):
|
|
688
|
+
result = execute_query("table-id-123", "select", {"where": {"id": "abc"}})
|
|
689
|
+
|
|
690
|
+
self.assertEqual(result, {"ok": True})
|
|
691
|
+
post_mock.assert_called_once()
|
|
692
|
+
called_url = (
|
|
693
|
+
post_mock.call_args.kwargs["url"]
|
|
694
|
+
if "url" in post_mock.call_args.kwargs
|
|
695
|
+
else post_mock.call_args.args[0]
|
|
696
|
+
)
|
|
697
|
+
called_headers = post_mock.call_args.kwargs["headers"]
|
|
698
|
+
self.assertEqual(
|
|
699
|
+
called_url.split("?")[0],
|
|
700
|
+
"https://na1.polyapi.io/tables/table-id-123/select",
|
|
701
|
+
)
|
|
702
|
+
self.assertEqual(called_headers["Authorization"], "Bearer test-api-key")
|
|
703
|
+
|
|
704
|
+
def test_generated_module_executes_with_delete_one_types(self):
|
|
705
|
+
source = f"{TABI_MODULE_IMPORTS}\n\n\n{_render_table(TABLE_SPEC_SIMPLE)}"
|
|
706
|
+
generated_scope = {}
|
|
707
|
+
exec(source, generated_scope)
|
|
708
|
+
self.assertIn("MyTable", generated_scope)
|
|
709
|
+
self.assertIn("PolyDeleteResult", generated_scope)
|
|
710
|
+
self.assertIn("delete_one_response", generated_scope)
|
|
711
|
+
|
|
666
712
|
@unittest.skip("too brittle, will restore later")
|
|
667
713
|
def test_render_complex(self):
|
|
668
714
|
self.maxDiff = 20000
|
|
669
715
|
output = _render_table(TABLE_SPEC_COMPLEX)
|
|
670
|
-
self.assertEqual(output, EXPECTED_COMPLEX)
|
|
716
|
+
self.assertEqual(output, EXPECTED_COMPLEX)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{polyapi_python-0.3.12.dev1 → polyapi_python-0.3.13}/polyapi_python.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|