polyapi-python 0.3.6.dev0__tar.gz → 0.3.7__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.6.dev0/polyapi_python.egg-info → polyapi_python-0.3.7}/PKG-INFO +1 -1
- polyapi_python-0.3.7/polyapi/__init__.py +101 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/api.py +10 -2
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/config.py +30 -1
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/execute.py +51 -5
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/function_cli.py +2 -3
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/generate.py +110 -29
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/schema.py +5 -1
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/utils.py +66 -15
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/webhook.py +1 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7/polyapi_python.egg-info}/PKG-INFO +1 -1
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/pyproject.toml +1 -1
- polyapi_python-0.3.7/tests/test_generate.py +289 -0
- polyapi_python-0.3.6.dev0/polyapi/__init__.py +0 -23
- polyapi_python-0.3.6.dev0/tests/test_generate.py +0 -83
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/LICENSE +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/README.md +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/__main__.py +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/auth.py +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/cli.py +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/client.py +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/constants.py +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/deployables.py +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/error_handler.py +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/exceptions.py +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/parser.py +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/poly_schemas.py +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/prepare.py +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/py.typed +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/rendered_spec.py +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/server.py +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/sync.py +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/typedefs.py +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/variables.py +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi_python.egg-info/SOURCES.txt +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi_python.egg-info/dependency_links.txt +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi_python.egg-info/requires.txt +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi_python.egg-info/top_level.txt +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/setup.cfg +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/tests/test_api.py +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/tests/test_auth.py +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/tests/test_deployables.py +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/tests/test_parser.py +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/tests/test_rendered_spec.py +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/tests/test_schema.py +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/tests/test_server.py +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/tests/test_utils.py +0 -0
- {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/tests/test_variables.py +0 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import copy
|
|
4
|
+
import truststore
|
|
5
|
+
from typing import Any, Dict, Optional, overload, Literal
|
|
6
|
+
from typing_extensions import TypedDict
|
|
7
|
+
truststore.inject_into_ssl()
|
|
8
|
+
from .cli import CLI_COMMANDS
|
|
9
|
+
|
|
10
|
+
__all__ = ["poly"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
if len(sys.argv) > 1 and sys.argv[1] not in CLI_COMMANDS:
|
|
14
|
+
currdir = os.path.dirname(os.path.abspath(__file__))
|
|
15
|
+
if not os.path.isdir(os.path.join(currdir, "poly")):
|
|
16
|
+
print("No 'poly' found. Please run 'python3 -m polyapi generate' to generate the 'poly' library for your tenant.")
|
|
17
|
+
sys.exit(1)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PolyCustomDict(TypedDict, total=False):
|
|
21
|
+
"""Type definition for polyCustom dictionary."""
|
|
22
|
+
executionId: Optional[str] # Read-only
|
|
23
|
+
executionApiKey: Optional[str]
|
|
24
|
+
responseStatusCode: int
|
|
25
|
+
responseContentType: Optional[str]
|
|
26
|
+
responseHeaders: Dict[str, str]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _PolyCustom:
|
|
30
|
+
def __init__(self):
|
|
31
|
+
self._internal_store = {
|
|
32
|
+
"executionId": None,
|
|
33
|
+
"executionApiKey": None,
|
|
34
|
+
"responseStatusCode": 200,
|
|
35
|
+
"responseContentType": None,
|
|
36
|
+
"responseHeaders": {},
|
|
37
|
+
}
|
|
38
|
+
self._execution_id_locked = False
|
|
39
|
+
|
|
40
|
+
def set_once(self, key: str, value: Any) -> None:
|
|
41
|
+
if key == "executionId" and self._execution_id_locked:
|
|
42
|
+
# Silently ignore attempts to overwrite locked executionId
|
|
43
|
+
return
|
|
44
|
+
self._internal_store[key] = value
|
|
45
|
+
if key == "executionId":
|
|
46
|
+
# Lock executionId after setting it
|
|
47
|
+
self.lock_execution_id()
|
|
48
|
+
|
|
49
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
50
|
+
return self._internal_store.get(key, default)
|
|
51
|
+
|
|
52
|
+
def lock_execution_id(self) -> None:
|
|
53
|
+
self._execution_id_locked = True
|
|
54
|
+
|
|
55
|
+
def unlock_execution_id(self) -> None:
|
|
56
|
+
self._execution_id_locked = False
|
|
57
|
+
|
|
58
|
+
@overload
|
|
59
|
+
def __getitem__(self, key: Literal["executionId"]) -> Optional[str]: ...
|
|
60
|
+
|
|
61
|
+
@overload
|
|
62
|
+
def __getitem__(self, key: Literal["executionApiKey"]) -> Optional[str]: ...
|
|
63
|
+
|
|
64
|
+
@overload
|
|
65
|
+
def __getitem__(self, key: Literal["responseStatusCode"]) -> int: ...
|
|
66
|
+
|
|
67
|
+
@overload
|
|
68
|
+
def __getitem__(self, key: Literal["responseContentType"]) -> Optional[str]: ...
|
|
69
|
+
|
|
70
|
+
@overload
|
|
71
|
+
def __getitem__(self, key: Literal["responseHeaders"]) -> Dict[str, str]: ...
|
|
72
|
+
|
|
73
|
+
def __getitem__(self, key: str) -> Any:
|
|
74
|
+
return self.get(key)
|
|
75
|
+
|
|
76
|
+
@overload
|
|
77
|
+
def __setitem__(self, key: Literal["executionApiKey"], value: Optional[str]) -> None: ...
|
|
78
|
+
|
|
79
|
+
@overload
|
|
80
|
+
def __setitem__(self, key: Literal["responseStatusCode"], value: int) -> None: ...
|
|
81
|
+
|
|
82
|
+
@overload
|
|
83
|
+
def __setitem__(self, key: Literal["responseContentType"], value: Optional[str]) -> None: ...
|
|
84
|
+
|
|
85
|
+
@overload
|
|
86
|
+
def __setitem__(self, key: Literal["responseHeaders"], value: Dict[str, str]) -> None: ...
|
|
87
|
+
|
|
88
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
89
|
+
self.set_once(key, value)
|
|
90
|
+
|
|
91
|
+
def __repr__(self) -> str:
|
|
92
|
+
return f"PolyCustom({self._internal_store})"
|
|
93
|
+
|
|
94
|
+
def copy(self) -> '_PolyCustom':
|
|
95
|
+
new = _PolyCustom()
|
|
96
|
+
new._internal_store = copy.deepcopy(self._internal_store)
|
|
97
|
+
new._execution_id_locked = self._execution_id_locked
|
|
98
|
+
return new
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
polyCustom: PolyCustomDict = _PolyCustom()
|
|
@@ -23,8 +23,16 @@ def {function_name}(
|
|
|
23
23
|
|
|
24
24
|
Function ID: {function_id}
|
|
25
25
|
\"""
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
if get_direct_execute_config():
|
|
27
|
+
resp = direct_execute("{function_type}", "{function_id}", {data})
|
|
28
|
+
return {api_response_type}({{
|
|
29
|
+
"status": resp.status_code,
|
|
30
|
+
"headers": dict(resp.headers),
|
|
31
|
+
"data": resp.json()
|
|
32
|
+
}}) # type: ignore
|
|
33
|
+
else:
|
|
34
|
+
resp = execute("{function_type}", "{function_id}", {data})
|
|
35
|
+
return {api_response_type}(resp.json()) # type: ignore
|
|
28
36
|
|
|
29
37
|
|
|
30
38
|
"""
|
|
@@ -8,6 +8,10 @@ from polyapi.utils import is_valid_polyapi_url, print_green, print_yellow
|
|
|
8
8
|
# cached values
|
|
9
9
|
API_KEY = None
|
|
10
10
|
API_URL = None
|
|
11
|
+
API_FUNCTION_DIRECT_EXECUTE = None
|
|
12
|
+
MTLS_CERT_PATH = None
|
|
13
|
+
MTLS_KEY_PATH = None
|
|
14
|
+
MTLS_CA_PATH = None
|
|
11
15
|
|
|
12
16
|
|
|
13
17
|
def get_config_file_path() -> str:
|
|
@@ -45,6 +49,13 @@ def get_api_key_and_url() -> Tuple[str | None, str | None]:
|
|
|
45
49
|
API_KEY = key
|
|
46
50
|
API_URL = url
|
|
47
51
|
|
|
52
|
+
# Read and cache MTLS and direct execute settings
|
|
53
|
+
global API_FUNCTION_DIRECT_EXECUTE, MTLS_CERT_PATH, MTLS_KEY_PATH, MTLS_CA_PATH
|
|
54
|
+
API_FUNCTION_DIRECT_EXECUTE = config.get("polyapi", "api_function_direct_execute", fallback="false").lower() == "true"
|
|
55
|
+
MTLS_CERT_PATH = config.get("polyapi", "mtls_cert_path", fallback=None)
|
|
56
|
+
MTLS_KEY_PATH = config.get("polyapi", "mtls_key_path", fallback=None)
|
|
57
|
+
MTLS_CA_PATH = config.get("polyapi", "mtls_ca_path", fallback=None)
|
|
58
|
+
|
|
48
59
|
return key, url
|
|
49
60
|
|
|
50
61
|
|
|
@@ -104,4 +115,22 @@ def clear_config():
|
|
|
104
115
|
|
|
105
116
|
path = get_config_file_path()
|
|
106
117
|
if os.path.exists(path):
|
|
107
|
-
os.remove(path)
|
|
118
|
+
os.remove(path)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def get_mtls_config() -> Tuple[bool, str | None, str | None, str | None]:
|
|
122
|
+
"""Return MTLS configuration settings"""
|
|
123
|
+
global MTLS_CERT_PATH, MTLS_KEY_PATH, MTLS_CA_PATH
|
|
124
|
+
if MTLS_CERT_PATH is None or MTLS_KEY_PATH is None or MTLS_CA_PATH is None:
|
|
125
|
+
# Force a config read if values aren't cached
|
|
126
|
+
get_api_key_and_url()
|
|
127
|
+
return bool(MTLS_CERT_PATH and MTLS_KEY_PATH and MTLS_CA_PATH), MTLS_CERT_PATH, MTLS_KEY_PATH, MTLS_CA_PATH
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def get_direct_execute_config() -> bool:
|
|
131
|
+
"""Return whether direct execute is enabled"""
|
|
132
|
+
global API_FUNCTION_DIRECT_EXECUTE
|
|
133
|
+
if API_FUNCTION_DIRECT_EXECUTE is None:
|
|
134
|
+
# Force a config read if value isn't cached
|
|
135
|
+
get_api_key_and_url()
|
|
136
|
+
return bool(API_FUNCTION_DIRECT_EXECUTE)
|
|
@@ -1,22 +1,68 @@
|
|
|
1
|
-
from typing import Dict
|
|
1
|
+
from typing import Dict, Optional
|
|
2
2
|
import requests
|
|
3
3
|
from requests import Response
|
|
4
|
-
from polyapi.config import get_api_key_and_url
|
|
4
|
+
from polyapi.config import get_api_key_and_url, get_mtls_config
|
|
5
5
|
from polyapi.exceptions import PolyApiException
|
|
6
6
|
|
|
7
|
+
def direct_execute(function_type, function_id, data) -> Response:
|
|
8
|
+
""" execute a specific function id/type
|
|
9
|
+
"""
|
|
10
|
+
api_key, api_url = get_api_key_and_url()
|
|
11
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
12
|
+
url = f"{api_url}/functions/{function_type}/{function_id}/direct-execute"
|
|
13
|
+
|
|
14
|
+
endpoint_info = requests.post(url, json=data, headers=headers)
|
|
15
|
+
if endpoint_info.status_code < 200 or endpoint_info.status_code >= 300:
|
|
16
|
+
raise PolyApiException(f"{endpoint_info.status_code}: {endpoint_info.content.decode('utf-8', errors='ignore')}")
|
|
17
|
+
|
|
18
|
+
endpoint_info_data = endpoint_info.json()
|
|
19
|
+
request_params = endpoint_info_data.copy()
|
|
20
|
+
request_params.pop("url", None)
|
|
21
|
+
|
|
22
|
+
if "maxRedirects" in request_params:
|
|
23
|
+
request_params["allow_redirects"] = request_params.pop("maxRedirects") > 0
|
|
24
|
+
|
|
25
|
+
has_mtls, cert_path, key_path, ca_path = get_mtls_config()
|
|
26
|
+
|
|
27
|
+
if has_mtls:
|
|
28
|
+
resp = requests.request(
|
|
29
|
+
url=endpoint_info_data["url"],
|
|
30
|
+
cert=(cert_path, key_path),
|
|
31
|
+
verify=ca_path,
|
|
32
|
+
**request_params
|
|
33
|
+
)
|
|
34
|
+
else:
|
|
35
|
+
resp = requests.request(
|
|
36
|
+
url=endpoint_info_data["url"],
|
|
37
|
+
verify=False,
|
|
38
|
+
**request_params
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if resp.status_code < 200 or resp.status_code >= 300:
|
|
42
|
+
error_content = resp.content.decode("utf-8", errors="ignore")
|
|
43
|
+
raise PolyApiException(f"{resp.status_code}: {error_content}")
|
|
44
|
+
|
|
45
|
+
return resp
|
|
7
46
|
|
|
8
47
|
def execute(function_type, function_id, data) -> Response:
|
|
9
48
|
""" execute a specific function id/type
|
|
10
49
|
"""
|
|
11
50
|
api_key, api_url = get_api_key_and_url()
|
|
12
51
|
headers = {"Authorization": f"Bearer {api_key}"}
|
|
52
|
+
|
|
13
53
|
url = f"{api_url}/functions/{function_type}/{function_id}/execute"
|
|
14
|
-
|
|
15
|
-
#
|
|
16
|
-
|
|
54
|
+
|
|
55
|
+
# Make the request
|
|
56
|
+
resp = requests.post(
|
|
57
|
+
url,
|
|
58
|
+
json=data,
|
|
59
|
+
headers=headers,
|
|
60
|
+
)
|
|
61
|
+
|
|
17
62
|
if resp.status_code < 200 or resp.status_code >= 300:
|
|
18
63
|
error_content = resp.content.decode("utf-8", errors="ignore")
|
|
19
64
|
raise PolyApiException(f"{resp.status_code}: {error_content}")
|
|
65
|
+
|
|
20
66
|
return resp
|
|
21
67
|
|
|
22
68
|
|
|
@@ -86,13 +86,12 @@ def function_add_or_update(
|
|
|
86
86
|
|
|
87
87
|
headers = get_auth_headers(api_key)
|
|
88
88
|
resp = requests.post(url, headers=headers, json=data)
|
|
89
|
-
if resp.status_code
|
|
89
|
+
if resp.status_code in [200, 201]:
|
|
90
90
|
print_green("DEPLOYED")
|
|
91
91
|
function_id = resp.json()["id"]
|
|
92
92
|
print(f"Function ID: {function_id}")
|
|
93
93
|
if generate:
|
|
94
|
-
|
|
95
|
-
generate_library(contexts=contexts)
|
|
94
|
+
generate_library()
|
|
96
95
|
else:
|
|
97
96
|
print("Error adding function.")
|
|
98
97
|
print(resp.status_code)
|
|
@@ -14,7 +14,7 @@ from .api import render_api_function
|
|
|
14
14
|
from .server import render_server_function
|
|
15
15
|
from .utils import add_import_to_init, get_auth_headers, init_the_init, print_green, to_func_namespace
|
|
16
16
|
from .variables import generate_variables
|
|
17
|
-
from .config import get_api_key_and_url
|
|
17
|
+
from .config import get_api_key_and_url, get_direct_execute_config
|
|
18
18
|
|
|
19
19
|
SUPPORTED_FUNCTION_TYPES = {
|
|
20
20
|
"apiFunction",
|
|
@@ -46,6 +46,10 @@ def get_specs(contexts=Optional[List[str]], no_types: bool = False) -> List:
|
|
|
46
46
|
if contexts:
|
|
47
47
|
params["contexts"] = contexts
|
|
48
48
|
|
|
49
|
+
# Add apiFunctionDirectExecute parameter if direct execute is enabled
|
|
50
|
+
if get_direct_execute_config():
|
|
51
|
+
params["apiFunctionDirectExecute"] = "true"
|
|
52
|
+
|
|
49
53
|
resp = requests.get(url, headers=headers, params=params)
|
|
50
54
|
if resp.status_code == 200:
|
|
51
55
|
return resp.json()
|
|
@@ -125,24 +129,26 @@ def parse_function_specs(
|
|
|
125
129
|
) -> List[SpecificationDto]:
|
|
126
130
|
functions = []
|
|
127
131
|
for spec in specs:
|
|
128
|
-
if not spec
|
|
129
|
-
continue
|
|
130
|
-
|
|
131
|
-
if not spec["function"]:
|
|
132
|
-
continue
|
|
133
|
-
|
|
134
|
-
if limit_ids and spec["id"] not in limit_ids:
|
|
132
|
+
if not spec:
|
|
135
133
|
continue
|
|
136
134
|
|
|
135
|
+
# For no_types mode, we might not have function data, but we still want to include the spec
|
|
136
|
+
# if it's a supported function type
|
|
137
137
|
if spec["type"] not in SUPPORTED_FUNCTION_TYPES:
|
|
138
138
|
continue
|
|
139
139
|
|
|
140
|
-
if
|
|
141
|
-
|
|
140
|
+
# Skip if we have a limit and this spec is not in it
|
|
141
|
+
if limit_ids and spec.get("id") not in limit_ids:
|
|
142
142
|
continue
|
|
143
143
|
|
|
144
|
+
# For customFunction, check language if we have function data
|
|
145
|
+
if spec["type"] == "customFunction":
|
|
146
|
+
if spec.get("language") and spec["language"] != "python":
|
|
147
|
+
# poly libraries only support client functions of same language
|
|
148
|
+
continue
|
|
149
|
+
|
|
144
150
|
# Functions with serverSideAsync True will always return a Dict with execution ID
|
|
145
|
-
if spec.get('serverSideAsync'):
|
|
151
|
+
if spec.get('serverSideAsync') and spec.get("function"):
|
|
146
152
|
spec['function']['returnType'] = {'kind': 'plain', 'value': 'object'}
|
|
147
153
|
|
|
148
154
|
functions.append(spec)
|
|
@@ -201,6 +207,63 @@ def remove_old_library():
|
|
|
201
207
|
shutil.rmtree(path)
|
|
202
208
|
|
|
203
209
|
|
|
210
|
+
def create_empty_schemas_module():
|
|
211
|
+
"""Create an empty schemas module for no-types mode so user code can still import from polyapi.schemas"""
|
|
212
|
+
currdir = os.path.dirname(os.path.abspath(__file__))
|
|
213
|
+
schemas_path = os.path.join(currdir, "schemas")
|
|
214
|
+
|
|
215
|
+
# Create the schemas directory
|
|
216
|
+
if not os.path.exists(schemas_path):
|
|
217
|
+
os.makedirs(schemas_path)
|
|
218
|
+
|
|
219
|
+
# Create an __init__.py file with dynamic schema resolution
|
|
220
|
+
init_path = os.path.join(schemas_path, "__init__.py")
|
|
221
|
+
with open(init_path, "w") as f:
|
|
222
|
+
f.write('''"""Empty schemas module for no-types mode"""
|
|
223
|
+
from typing import Any, Dict
|
|
224
|
+
|
|
225
|
+
class _GenericSchema(Dict[str, Any]):
|
|
226
|
+
"""Generic schema type that acts like a Dict for no-types mode"""
|
|
227
|
+
def __init__(self, *args, **kwargs):
|
|
228
|
+
super().__init__(*args, **kwargs)
|
|
229
|
+
|
|
230
|
+
class _SchemaModule:
|
|
231
|
+
"""Dynamic module that returns itself for attribute access, allowing infinite nesting"""
|
|
232
|
+
|
|
233
|
+
def __getattr__(self, name: str):
|
|
234
|
+
# For callable access (like schemas.Response()), return the generic schema class
|
|
235
|
+
# For further attribute access (like schemas.random.random2), return self to allow nesting
|
|
236
|
+
return _NestedSchemaAccess()
|
|
237
|
+
|
|
238
|
+
def __call__(self, *args, **kwargs):
|
|
239
|
+
# If someone tries to call the module itself, return a generic schema
|
|
240
|
+
return _GenericSchema(*args, **kwargs)
|
|
241
|
+
|
|
242
|
+
def __dir__(self):
|
|
243
|
+
# Return common schema names for introspection
|
|
244
|
+
return ['Response', 'Request', 'Error', 'Data', 'Result']
|
|
245
|
+
|
|
246
|
+
class _NestedSchemaAccess:
|
|
247
|
+
"""Handles nested attribute access and final callable resolution"""
|
|
248
|
+
|
|
249
|
+
def __getattr__(self, name: str):
|
|
250
|
+
# Continue allowing nested access
|
|
251
|
+
return _NestedSchemaAccess()
|
|
252
|
+
|
|
253
|
+
def __call__(self, *args, **kwargs):
|
|
254
|
+
# When finally called, return a generic schema instance
|
|
255
|
+
return _GenericSchema(*args, **kwargs)
|
|
256
|
+
|
|
257
|
+
def __class_getitem__(cls, item):
|
|
258
|
+
# Support type annotations like schemas.Response[str]
|
|
259
|
+
return _GenericSchema
|
|
260
|
+
|
|
261
|
+
# Replace this module with our dynamic module
|
|
262
|
+
import sys
|
|
263
|
+
sys.modules[__name__] = _SchemaModule()
|
|
264
|
+
''')
|
|
265
|
+
|
|
266
|
+
|
|
204
267
|
def generate(contexts: Optional[List[str]] = None, no_types: bool = False) -> None:
|
|
205
268
|
generate_msg = f"Generating Poly Python SDK for contexts ${contexts}..." if contexts else "Generating Poly Python SDK..."
|
|
206
269
|
print(generate_msg, end="", flush=True)
|
|
@@ -212,14 +275,23 @@ def generate(contexts: Optional[List[str]] = None, no_types: bool = False) -> No
|
|
|
212
275
|
limit_ids: List[str] = [] # useful for narrowing down generation to a single function to debug
|
|
213
276
|
functions = parse_function_specs(specs, limit_ids=limit_ids)
|
|
214
277
|
|
|
215
|
-
schemas
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
278
|
+
# Only process schemas if no_types is False
|
|
279
|
+
if not no_types:
|
|
280
|
+
schemas = get_schemas()
|
|
281
|
+
schema_index = build_schema_index(schemas)
|
|
282
|
+
if schemas:
|
|
283
|
+
schema_limit_ids: List[str] = [] # useful for narrowing down generation to a single function to debug
|
|
284
|
+
schemas = replace_poly_refs_in_schemas(schemas, schema_index)
|
|
285
|
+
generate_schemas(schemas, limit_ids=schema_limit_ids)
|
|
286
|
+
|
|
287
|
+
functions = replace_poly_refs_in_functions(functions, schema_index)
|
|
288
|
+
else:
|
|
289
|
+
# When no_types is True, we still need to process functions but without schema resolution
|
|
290
|
+
# Use an empty schema index to avoid poly-ref resolution
|
|
291
|
+
schema_index = {}
|
|
292
|
+
|
|
293
|
+
# Create an empty schemas module so user code can still import from polyapi.schemas
|
|
294
|
+
create_empty_schemas_module()
|
|
223
295
|
|
|
224
296
|
if functions:
|
|
225
297
|
generate_functions(functions)
|
|
@@ -229,10 +301,11 @@ def generate(contexts: Optional[List[str]] = None, no_types: bool = False) -> No
|
|
|
229
301
|
)
|
|
230
302
|
exit()
|
|
231
303
|
|
|
232
|
-
variables
|
|
233
|
-
if
|
|
234
|
-
|
|
235
|
-
|
|
304
|
+
# Only process variables if no_types is False
|
|
305
|
+
if not no_types:
|
|
306
|
+
variables = get_variables()
|
|
307
|
+
if variables:
|
|
308
|
+
generate_variables(variables)
|
|
236
309
|
|
|
237
310
|
# indicator to vscode extension that this is a polyapi-python project
|
|
238
311
|
file_path = os.path.join(os.getcwd(), ".polyapi-python")
|
|
@@ -262,11 +335,19 @@ def render_spec(spec: SpecificationDto) -> Tuple[str, str]:
|
|
|
262
335
|
|
|
263
336
|
arguments: List[PropertySpecification] = []
|
|
264
337
|
return_type = {}
|
|
265
|
-
if spec
|
|
266
|
-
arguments
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
338
|
+
if spec.get("function"):
|
|
339
|
+
# Handle cases where arguments might be missing or None
|
|
340
|
+
if spec["function"].get("arguments"):
|
|
341
|
+
arguments = [
|
|
342
|
+
arg for arg in spec["function"]["arguments"]
|
|
343
|
+
]
|
|
344
|
+
|
|
345
|
+
# Handle cases where returnType might be missing or None
|
|
346
|
+
if spec["function"].get("returnType"):
|
|
347
|
+
return_type = spec["function"]["returnType"]
|
|
348
|
+
else:
|
|
349
|
+
# Provide a fallback return type when missing
|
|
350
|
+
return_type = {"kind": "any"}
|
|
270
351
|
|
|
271
352
|
if function_type == "apiFunction":
|
|
272
353
|
func_str, func_type_defs = render_api_function(
|
|
@@ -280,7 +361,7 @@ def render_spec(spec: SpecificationDto) -> Tuple[str, str]:
|
|
|
280
361
|
elif function_type == "customFunction":
|
|
281
362
|
func_str, func_type_defs = render_client_function(
|
|
282
363
|
function_name,
|
|
283
|
-
spec
|
|
364
|
+
spec.get("code", ""),
|
|
284
365
|
arguments,
|
|
285
366
|
return_type,
|
|
286
367
|
)
|
|
@@ -126,4 +126,8 @@ def clean_title(title: str) -> str:
|
|
|
126
126
|
|
|
127
127
|
def map_primitive_types(type_: str) -> str:
|
|
128
128
|
# Define your mapping logic here
|
|
129
|
-
return JSONSCHEMA_TO_PYTHON_TYPE_MAP.get(type_, "Any")
|
|
129
|
+
return JSONSCHEMA_TO_PYTHON_TYPE_MAP.get(type_, "Any")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def is_primitive(type_: str) -> bool:
|
|
133
|
+
return type_ in JSONSCHEMA_TO_PYTHON_TYPE_MAP
|
|
@@ -11,12 +11,13 @@ from polyapi.schema import (
|
|
|
11
11
|
wrapped_generate_schema_types,
|
|
12
12
|
clean_title,
|
|
13
13
|
map_primitive_types,
|
|
14
|
+
is_primitive
|
|
14
15
|
)
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
# this string should be in every __init__ file.
|
|
18
19
|
# it contains all the imports needed for the function or variable code to run
|
|
19
|
-
CODE_IMPORTS = "from typing import List, Dict, Any, Optional, Callable\nfrom typing_extensions import TypedDict, NotRequired\nimport logging\nimport requests\nimport socketio # type: ignore\nfrom polyapi.config import get_api_key_and_url\nfrom polyapi.execute import execute, execute_post, variable_get, variable_update\n\n"
|
|
20
|
+
CODE_IMPORTS = "from typing import List, Dict, Any, Optional, Callable\nfrom typing_extensions import TypedDict, NotRequired\nimport logging\nimport requests\nimport socketio # type: ignore\nfrom polyapi.config import get_api_key_and_url, get_direct_execute_config\nfrom polyapi.execute import execute, execute_post, variable_get, variable_update, direct_execute\n\n"
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def init_the_init(full_path: str, code_imports="") -> None:
|
|
@@ -97,20 +98,32 @@ def get_type_and_def(
|
|
|
97
98
|
) -> Tuple[str, str]:
|
|
98
99
|
""" returns type and type definition for a given PropertyType
|
|
99
100
|
"""
|
|
101
|
+
# Handle cases where type_spec might be None or empty
|
|
102
|
+
if not type_spec:
|
|
103
|
+
return "Any", ""
|
|
104
|
+
|
|
105
|
+
# Handle cases where kind might be missing
|
|
106
|
+
if "kind" not in type_spec:
|
|
107
|
+
return "Any", ""
|
|
108
|
+
|
|
100
109
|
if type_spec["kind"] == "plain":
|
|
101
|
-
value = type_spec
|
|
110
|
+
value = type_spec.get("value", "")
|
|
102
111
|
if value.endswith("[]"):
|
|
103
112
|
primitive = map_primitive_types(value[:-2])
|
|
104
113
|
return f"List[{primitive}]", ""
|
|
105
114
|
else:
|
|
106
115
|
return map_primitive_types(value), ""
|
|
107
116
|
elif type_spec["kind"] == "primitive":
|
|
108
|
-
return map_primitive_types(type_spec
|
|
117
|
+
return map_primitive_types(type_spec.get("type", "any")), ""
|
|
109
118
|
elif type_spec["kind"] == "array":
|
|
110
119
|
if type_spec.get("items"):
|
|
111
120
|
items = type_spec["items"]
|
|
112
121
|
if items.get("$ref"):
|
|
113
|
-
|
|
122
|
+
# For no-types mode, avoid complex schema generation
|
|
123
|
+
try:
|
|
124
|
+
return wrapped_generate_schema_types(type_spec, "ResponseType", "Dict") # type: ignore
|
|
125
|
+
except:
|
|
126
|
+
return "List[Dict]", ""
|
|
114
127
|
else:
|
|
115
128
|
item_type, _ = get_type_and_def(items)
|
|
116
129
|
title = f"List[{item_type}]"
|
|
@@ -128,15 +141,28 @@ def get_type_and_def(
|
|
|
128
141
|
# TODO fix me
|
|
129
142
|
# we don't use ReturnType as name for the list type here, we use _ReturnTypeItem
|
|
130
143
|
return "List", ""
|
|
144
|
+
elif title and title == "ReturnType" and schema.get("type"):
|
|
145
|
+
assert isinstance(title, str)
|
|
146
|
+
schema_type = schema.get("type", "Any")
|
|
147
|
+
root_type, generated_code = wrapped_generate_schema_types(schema, schema_type, "Dict") # type: ignore
|
|
148
|
+
return (map_primitive_types(root_type), "") if is_primitive(root_type) else (root_type, generated_code) # type: ignore
|
|
131
149
|
elif title:
|
|
132
150
|
assert isinstance(title, str)
|
|
133
|
-
|
|
151
|
+
# For no-types mode, avoid complex schema generation
|
|
152
|
+
try:
|
|
153
|
+
root_type, generated_code = wrapped_generate_schema_types(schema, title, "Dict") # type: ignore
|
|
154
|
+
return ("Any", "") if root_type == "ReturnType" else wrapped_generate_schema_types(schema, title, "Dict") # type: ignore
|
|
155
|
+
except:
|
|
156
|
+
return "Dict", ""
|
|
134
157
|
elif schema.get("allOf") and len(schema["allOf"]):
|
|
135
158
|
# we are in a case of a single allOf, lets strip off the allOf and move on!
|
|
136
159
|
# our library doesn't handle allOf well yet
|
|
137
160
|
allOf = schema["allOf"][0]
|
|
138
161
|
title = allOf.get("title", allOf.get("name", title_fallback))
|
|
139
|
-
|
|
162
|
+
try:
|
|
163
|
+
return wrapped_generate_schema_types(allOf, title, "Dict")
|
|
164
|
+
except:
|
|
165
|
+
return "Dict", ""
|
|
140
166
|
elif schema.get("items"):
|
|
141
167
|
# fallback to schema $ref name if no explicit title
|
|
142
168
|
items = schema.get("items") # type: ignore
|
|
@@ -150,9 +176,15 @@ def get_type_and_def(
|
|
|
150
176
|
return "List", ""
|
|
151
177
|
|
|
152
178
|
title = f"List[{title}]"
|
|
153
|
-
|
|
179
|
+
try:
|
|
180
|
+
return wrapped_generate_schema_types(schema, title, "List")
|
|
181
|
+
except:
|
|
182
|
+
return "List[Dict]", ""
|
|
183
|
+
elif schema.get("properties"):
|
|
184
|
+
result = wrapped_generate_schema_types(schema, "ResponseType", "Dict") # type: ignore
|
|
185
|
+
return result
|
|
154
186
|
else:
|
|
155
|
-
return "
|
|
187
|
+
return "Dict", ""
|
|
156
188
|
else:
|
|
157
189
|
return "Dict", ""
|
|
158
190
|
elif type_spec["kind"] == "function":
|
|
@@ -187,12 +219,22 @@ def get_type_and_def(
|
|
|
187
219
|
|
|
188
220
|
|
|
189
221
|
def _maybe_add_fallback_schema_name(a: PropertySpecification):
|
|
190
|
-
|
|
222
|
+
# Handle cases where type might be missing
|
|
223
|
+
if not a.get("type"):
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
if a["type"].get("kind") == "object" and a["type"].get("schema"):
|
|
191
227
|
schema = a["type"].get("schema", {})
|
|
192
|
-
if not schema.get("title") and not schema.get("name") and a
|
|
228
|
+
if not schema.get("title") and not schema.get("name") and a.get("name"):
|
|
193
229
|
schema["title"] = a["name"].title()
|
|
194
230
|
|
|
195
231
|
|
|
232
|
+
def _clean_description(text: str) -> str:
|
|
233
|
+
"""Flatten new-lines and collapse excess whitespace."""
|
|
234
|
+
text = text.replace("\\n", " ").replace("\n", " ")
|
|
235
|
+
return re.sub(r"\s+", " ", text).strip()
|
|
236
|
+
|
|
237
|
+
|
|
196
238
|
def parse_arguments(
|
|
197
239
|
function_name: str, arguments: List[PropertySpecification]
|
|
198
240
|
) -> Tuple[str, str]:
|
|
@@ -200,18 +242,27 @@ def parse_arguments(
|
|
|
200
242
|
arg_string = ""
|
|
201
243
|
for idx, a in enumerate(arguments):
|
|
202
244
|
_maybe_add_fallback_schema_name(a)
|
|
203
|
-
|
|
245
|
+
|
|
246
|
+
# Handle cases where type might be missing
|
|
247
|
+
arg_type_spec = a.get("type", {"kind": "any"})
|
|
248
|
+
arg_type, arg_def = get_type_and_def(arg_type_spec)
|
|
204
249
|
if arg_def:
|
|
205
250
|
args_def.append(arg_def)
|
|
206
|
-
|
|
251
|
+
|
|
252
|
+
# Handle cases where name might be missing
|
|
253
|
+
arg_name = a.get("name", f"arg{idx}")
|
|
254
|
+
a["name"] = rewrite_arg_name(arg_name)
|
|
255
|
+
|
|
207
256
|
arg_string += (
|
|
208
257
|
f" {a['name']}: {add_type_import_path(function_name, arg_type)}"
|
|
209
258
|
)
|
|
210
|
-
|
|
259
|
+
|
|
260
|
+
# Handle cases where required might be missing
|
|
261
|
+
if not a.get("required", True):
|
|
211
262
|
arg_string += " = None"
|
|
212
263
|
|
|
213
|
-
description = a.get("description", "")
|
|
214
|
-
|
|
264
|
+
description = _clean_description(a.get("description", ""))
|
|
265
|
+
|
|
215
266
|
if description:
|
|
216
267
|
if idx == len(arguments) - 1:
|
|
217
268
|
arg_string += f" # {description}\n"
|
|
@@ -3,7 +3,7 @@ requires = ["setuptools>=61.2", "wheel"]
|
|
|
3
3
|
|
|
4
4
|
[project]
|
|
5
5
|
name = "polyapi-python"
|
|
6
|
-
version = "0.3.
|
|
6
|
+
version = "0.3.7"
|
|
7
7
|
description = "The Python Client for PolyAPI, the IPaaS by Developers for Developers"
|
|
8
8
|
authors = [{ name = "Dan Fellin", email = "dan@polyapi.io" }]
|
|
9
9
|
dependencies = [
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import importlib.util
|
|
5
|
+
from polyapi.utils import get_type_and_def, rewrite_reserved
|
|
6
|
+
from polyapi.generate import render_spec, create_empty_schemas_module
|
|
7
|
+
|
|
8
|
+
OPENAPI_FUNCTION = {
|
|
9
|
+
"kind": "function",
|
|
10
|
+
"spec": {
|
|
11
|
+
"arguments": [
|
|
12
|
+
{
|
|
13
|
+
"name": "event",
|
|
14
|
+
"required": False,
|
|
15
|
+
"type": {
|
|
16
|
+
"kind": "object",
|
|
17
|
+
"schema": {
|
|
18
|
+
"$schema": "http://json-schema.org/draft-06/schema#",
|
|
19
|
+
"type": "array",
|
|
20
|
+
"items": {"$ref": "#/definitions/WebhookEventTypeElement"},
|
|
21
|
+
"definitions": {
|
|
22
|
+
"WebhookEventTypeElement": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"additionalProperties": False,
|
|
25
|
+
"properties": {
|
|
26
|
+
"title": {"type": "string"},
|
|
27
|
+
"manufacturerName": {"type": "string"},
|
|
28
|
+
"carType": {"type": "string"},
|
|
29
|
+
"id": {"type": "integer"},
|
|
30
|
+
},
|
|
31
|
+
"required": [
|
|
32
|
+
"carType",
|
|
33
|
+
"id",
|
|
34
|
+
"manufacturerName",
|
|
35
|
+
"title",
|
|
36
|
+
],
|
|
37
|
+
"title": "WebhookEventTypeElement",
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"name": "headers",
|
|
45
|
+
"required": False,
|
|
46
|
+
"type": {"kind": "object", "typeName": "Record<string, any>"},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "params",
|
|
50
|
+
"required": False,
|
|
51
|
+
"type": {"kind": "object", "typeName": "Record<string, any>"},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "polyCustom",
|
|
55
|
+
"required": False,
|
|
56
|
+
"type": {
|
|
57
|
+
"kind": "object",
|
|
58
|
+
"properties": [
|
|
59
|
+
{
|
|
60
|
+
"name": "responseStatusCode",
|
|
61
|
+
"type": {"type": "number", "kind": "primitive"},
|
|
62
|
+
"required": True,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "responseContentType",
|
|
66
|
+
"type": {"type": "string", "kind": "primitive"},
|
|
67
|
+
"required": True,
|
|
68
|
+
"nullable": True,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
"returnType": {"kind": "void"},
|
|
75
|
+
"synchronous": True,
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Test spec with missing function data (simulating no_types=true)
|
|
80
|
+
NO_TYPES_SPEC = {
|
|
81
|
+
"id": "test-id-123",
|
|
82
|
+
"type": "serverFunction",
|
|
83
|
+
"context": "test",
|
|
84
|
+
"name": "testFunction",
|
|
85
|
+
"description": "A test function for no-types mode",
|
|
86
|
+
# Note: no "function" field, simulating no_types=true response
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Test spec with minimal function data
|
|
90
|
+
MINIMAL_FUNCTION_SPEC = {
|
|
91
|
+
"id": "test-id-456",
|
|
92
|
+
"type": "apiFunction",
|
|
93
|
+
"context": "test",
|
|
94
|
+
"name": "minimalFunction",
|
|
95
|
+
"description": "A minimal function spec",
|
|
96
|
+
"function": {
|
|
97
|
+
# Note: no "arguments" or "returnType" fields
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class T(unittest.TestCase):
|
|
103
|
+
def test_get_type_and_def(self):
|
|
104
|
+
arg_type, arg_def = get_type_and_def(OPENAPI_FUNCTION)
|
|
105
|
+
self.assertEqual(arg_type, "Callable[[List[WebhookEventTypeElement], Dict, Dict, Dict], None]")
|
|
106
|
+
|
|
107
|
+
def test_rewrite_reserved(self):
|
|
108
|
+
rv = rewrite_reserved("from")
|
|
109
|
+
self.assertEqual(rv, "_from")
|
|
110
|
+
|
|
111
|
+
def test_render_spec_no_function_data(self):
|
|
112
|
+
"""Test that render_spec handles specs with no function data gracefully"""
|
|
113
|
+
func_str, func_type_defs = render_spec(NO_TYPES_SPEC)
|
|
114
|
+
|
|
115
|
+
# Should generate a function even without function data
|
|
116
|
+
self.assertIsNotNone(func_str)
|
|
117
|
+
self.assertIsNotNone(func_type_defs)
|
|
118
|
+
self.assertIn("testFunction", func_str)
|
|
119
|
+
self.assertIn("test-id-123", func_str)
|
|
120
|
+
|
|
121
|
+
def test_render_spec_minimal_function_data(self):
|
|
122
|
+
"""Test that render_spec handles specs with minimal function data"""
|
|
123
|
+
func_str, func_type_defs = render_spec(MINIMAL_FUNCTION_SPEC)
|
|
124
|
+
|
|
125
|
+
# Should generate a function with fallback types
|
|
126
|
+
self.assertIsNotNone(func_str)
|
|
127
|
+
self.assertIsNotNone(func_type_defs)
|
|
128
|
+
self.assertIn("minimalFunction", func_str)
|
|
129
|
+
self.assertIn("test-id-456", func_str)
|
|
130
|
+
# Should use Any as fallback return type in the type definitions
|
|
131
|
+
self.assertIn("Any", func_type_defs)
|
|
132
|
+
|
|
133
|
+
def test_create_empty_schemas_module(self):
|
|
134
|
+
"""Test that create_empty_schemas_module creates the necessary files"""
|
|
135
|
+
# Clean up any existing schemas directory
|
|
136
|
+
schemas_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "polyapi", "schemas")
|
|
137
|
+
if os.path.exists(schemas_path):
|
|
138
|
+
shutil.rmtree(schemas_path)
|
|
139
|
+
|
|
140
|
+
# Create empty schemas module
|
|
141
|
+
create_empty_schemas_module()
|
|
142
|
+
|
|
143
|
+
# Verify the directory and __init__.py file were created
|
|
144
|
+
self.assertTrue(os.path.exists(schemas_path))
|
|
145
|
+
init_path = os.path.join(schemas_path, "__init__.py")
|
|
146
|
+
self.assertTrue(os.path.exists(init_path))
|
|
147
|
+
|
|
148
|
+
# Verify the content of __init__.py includes dynamic schema handling
|
|
149
|
+
with open(init_path, "r") as f:
|
|
150
|
+
content = f.read()
|
|
151
|
+
self.assertIn("Empty schemas module for no-types mode", content)
|
|
152
|
+
self.assertIn("_GenericSchema", content)
|
|
153
|
+
self.assertIn("_SchemaModule", content)
|
|
154
|
+
self.assertIn("__getattr__", content)
|
|
155
|
+
|
|
156
|
+
# Clean up
|
|
157
|
+
shutil.rmtree(schemas_path)
|
|
158
|
+
|
|
159
|
+
def test_no_types_workflow(self):
|
|
160
|
+
"""Test the complete no-types workflow including schema imports and function parsing"""
|
|
161
|
+
import tempfile
|
|
162
|
+
import sys
|
|
163
|
+
from unittest.mock import patch
|
|
164
|
+
|
|
165
|
+
# Clean up any existing schemas directory
|
|
166
|
+
schemas_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "polyapi", "schemas")
|
|
167
|
+
if os.path.exists(schemas_path):
|
|
168
|
+
shutil.rmtree(schemas_path)
|
|
169
|
+
|
|
170
|
+
# Mock get_specs to return empty list (simulating no functions)
|
|
171
|
+
with patch('polyapi.generate.get_specs', return_value=[]):
|
|
172
|
+
try:
|
|
173
|
+
# This should exit with SystemExit due to no functions
|
|
174
|
+
from polyapi.generate import generate
|
|
175
|
+
generate(no_types=True)
|
|
176
|
+
except SystemExit:
|
|
177
|
+
pass # Expected when no functions exist
|
|
178
|
+
|
|
179
|
+
# Verify schemas module was created
|
|
180
|
+
self.assertTrue(os.path.exists(schemas_path))
|
|
181
|
+
init_path = os.path.join(schemas_path, "__init__.py")
|
|
182
|
+
self.assertTrue(os.path.exists(init_path))
|
|
183
|
+
|
|
184
|
+
# Test that we can import schemas and use arbitrary schema names
|
|
185
|
+
from polyapi import schemas
|
|
186
|
+
|
|
187
|
+
# Test various schema access
|
|
188
|
+
Response = schemas.Response
|
|
189
|
+
CustomType = schemas.CustomType
|
|
190
|
+
AnyName = schemas.SomeArbitrarySchemaName
|
|
191
|
+
|
|
192
|
+
# All should return the same generic schema class type
|
|
193
|
+
self.assertEqual(type(Response).__name__, '_NestedSchemaAccess')
|
|
194
|
+
self.assertEqual(type(CustomType).__name__, '_NestedSchemaAccess')
|
|
195
|
+
self.assertEqual(type(AnyName).__name__, '_NestedSchemaAccess')
|
|
196
|
+
|
|
197
|
+
# Test creating instances
|
|
198
|
+
response_instance = Response()
|
|
199
|
+
custom_instance = CustomType()
|
|
200
|
+
|
|
201
|
+
self.assertIsInstance(response_instance, dict)
|
|
202
|
+
self.assertIsInstance(custom_instance, dict)
|
|
203
|
+
|
|
204
|
+
# Test that function code with schema references can be parsed
|
|
205
|
+
test_code = '''
|
|
206
|
+
from polyapi import polyCustom, schemas
|
|
207
|
+
|
|
208
|
+
def test_function() -> schemas.Response:
|
|
209
|
+
polyCustom["executionId"] = "123"
|
|
210
|
+
return polyCustom
|
|
211
|
+
'''
|
|
212
|
+
|
|
213
|
+
# Create a temporary file with the test code
|
|
214
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
|
215
|
+
f.write(test_code)
|
|
216
|
+
temp_file = f.name
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
# Test that the parser can handle this code
|
|
220
|
+
from polyapi.parser import parse_function_code
|
|
221
|
+
result = parse_function_code(test_code, 'test_function', 'test_context')
|
|
222
|
+
|
|
223
|
+
self.assertEqual(result['name'], 'test_function')
|
|
224
|
+
self.assertEqual(result['context'], 'test_context')
|
|
225
|
+
# Return type should be Any since we're in no-types mode
|
|
226
|
+
self.assertEqual(result['types']['returns']['type'], 'Any')
|
|
227
|
+
|
|
228
|
+
finally:
|
|
229
|
+
# Clean up temp file
|
|
230
|
+
os.unlink(temp_file)
|
|
231
|
+
|
|
232
|
+
# Clean up schemas directory
|
|
233
|
+
shutil.rmtree(schemas_path)
|
|
234
|
+
|
|
235
|
+
def test_nested_schema_access(self):
|
|
236
|
+
"""Test that nested schema access like schemas.random.random2.random3 works"""
|
|
237
|
+
# Clean up any existing schemas directory
|
|
238
|
+
schemas_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "polyapi", "schemas")
|
|
239
|
+
if os.path.exists(schemas_path):
|
|
240
|
+
shutil.rmtree(schemas_path)
|
|
241
|
+
|
|
242
|
+
# Create empty schemas module
|
|
243
|
+
create_empty_schemas_module()
|
|
244
|
+
|
|
245
|
+
# Test that we can import and use nested schemas
|
|
246
|
+
from polyapi import schemas
|
|
247
|
+
|
|
248
|
+
# Test various levels of nesting
|
|
249
|
+
simple = schemas.Response
|
|
250
|
+
nested = schemas.random.random2
|
|
251
|
+
deep_nested = schemas.api.v1.user.profile.data
|
|
252
|
+
very_deep = schemas.some.very.deep.nested.schema.access
|
|
253
|
+
|
|
254
|
+
# All should be _NestedSchemaAccess instances
|
|
255
|
+
self.assertEqual(type(simple).__name__, '_NestedSchemaAccess')
|
|
256
|
+
self.assertEqual(type(nested).__name__, '_NestedSchemaAccess')
|
|
257
|
+
self.assertEqual(type(deep_nested).__name__, '_NestedSchemaAccess')
|
|
258
|
+
self.assertEqual(type(very_deep).__name__, '_NestedSchemaAccess')
|
|
259
|
+
|
|
260
|
+
# Test that they can be called and return generic schemas
|
|
261
|
+
simple_instance = simple()
|
|
262
|
+
nested_instance = nested()
|
|
263
|
+
deep_instance = deep_nested()
|
|
264
|
+
very_deep_instance = very_deep()
|
|
265
|
+
|
|
266
|
+
# All should be dictionaries
|
|
267
|
+
self.assertIsInstance(simple_instance, dict)
|
|
268
|
+
self.assertIsInstance(nested_instance, dict)
|
|
269
|
+
self.assertIsInstance(deep_instance, dict)
|
|
270
|
+
self.assertIsInstance(very_deep_instance, dict)
|
|
271
|
+
|
|
272
|
+
# Test that function code with nested schemas can be parsed
|
|
273
|
+
test_code = '''
|
|
274
|
+
from polyapi import polyCustom, schemas
|
|
275
|
+
|
|
276
|
+
def test_nested_function() -> schemas.api.v1.user.profile:
|
|
277
|
+
return schemas.api.v1.user.profile()
|
|
278
|
+
'''
|
|
279
|
+
|
|
280
|
+
from polyapi.parser import parse_function_code
|
|
281
|
+
result = parse_function_code(test_code, 'test_nested_function', 'test_context')
|
|
282
|
+
|
|
283
|
+
self.assertEqual(result['name'], 'test_nested_function')
|
|
284
|
+
self.assertEqual(result['context'], 'test_context')
|
|
285
|
+
# Return type should be Any since we're in no-types mode
|
|
286
|
+
self.assertEqual(result['types']['returns']['type'], 'Any')
|
|
287
|
+
|
|
288
|
+
# Clean up schemas directory
|
|
289
|
+
shutil.rmtree(schemas_path)
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import sys
|
|
3
|
-
import truststore
|
|
4
|
-
from typing import Dict, Any
|
|
5
|
-
truststore.inject_into_ssl()
|
|
6
|
-
from .cli import CLI_COMMANDS
|
|
7
|
-
|
|
8
|
-
__all__ = ["poly"]
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if len(sys.argv) > 1 and sys.argv[1] not in CLI_COMMANDS:
|
|
12
|
-
currdir = os.path.dirname(os.path.abspath(__file__))
|
|
13
|
-
if not os.path.isdir(os.path.join(currdir, "poly")):
|
|
14
|
-
print("No 'poly' found. Please run 'python3 -m polyapi generate' to generate the 'poly' library for your tenant.")
|
|
15
|
-
sys.exit(1)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
polyCustom: Dict[str, Any] = {
|
|
19
|
-
"executionId": None,
|
|
20
|
-
"executionApiKey": None,
|
|
21
|
-
"responseStatusCode": 200,
|
|
22
|
-
"responseContentType": None,
|
|
23
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import unittest
|
|
2
|
-
from polyapi.utils import get_type_and_def, rewrite_reserved
|
|
3
|
-
|
|
4
|
-
OPENAPI_FUNCTION = {
|
|
5
|
-
"kind": "function",
|
|
6
|
-
"spec": {
|
|
7
|
-
"arguments": [
|
|
8
|
-
{
|
|
9
|
-
"name": "event",
|
|
10
|
-
"required": False,
|
|
11
|
-
"type": {
|
|
12
|
-
"kind": "object",
|
|
13
|
-
"schema": {
|
|
14
|
-
"$schema": "http://json-schema.org/draft-06/schema#",
|
|
15
|
-
"type": "array",
|
|
16
|
-
"items": {"$ref": "#/definitions/WebhookEventTypeElement"},
|
|
17
|
-
"definitions": {
|
|
18
|
-
"WebhookEventTypeElement": {
|
|
19
|
-
"type": "object",
|
|
20
|
-
"additionalProperties": False,
|
|
21
|
-
"properties": {
|
|
22
|
-
"title": {"type": "string"},
|
|
23
|
-
"manufacturerName": {"type": "string"},
|
|
24
|
-
"carType": {"type": "string"},
|
|
25
|
-
"id": {"type": "integer"},
|
|
26
|
-
},
|
|
27
|
-
"required": [
|
|
28
|
-
"carType",
|
|
29
|
-
"id",
|
|
30
|
-
"manufacturerName",
|
|
31
|
-
"title",
|
|
32
|
-
],
|
|
33
|
-
"title": "WebhookEventTypeElement",
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
"name": "headers",
|
|
41
|
-
"required": False,
|
|
42
|
-
"type": {"kind": "object", "typeName": "Record<string, any>"},
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
"name": "params",
|
|
46
|
-
"required": False,
|
|
47
|
-
"type": {"kind": "object", "typeName": "Record<string, any>"},
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
"name": "polyCustom",
|
|
51
|
-
"required": False,
|
|
52
|
-
"type": {
|
|
53
|
-
"kind": "object",
|
|
54
|
-
"properties": [
|
|
55
|
-
{
|
|
56
|
-
"name": "responseStatusCode",
|
|
57
|
-
"type": {"type": "number", "kind": "primitive"},
|
|
58
|
-
"required": True,
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
"name": "responseContentType",
|
|
62
|
-
"type": {"type": "string", "kind": "primitive"},
|
|
63
|
-
"required": True,
|
|
64
|
-
"nullable": True,
|
|
65
|
-
},
|
|
66
|
-
],
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
],
|
|
70
|
-
"returnType": {"kind": "void"},
|
|
71
|
-
"synchronous": True,
|
|
72
|
-
},
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
class T(unittest.TestCase):
|
|
77
|
-
def test_get_type_and_def(self):
|
|
78
|
-
arg_type, arg_def = get_type_and_def(OPENAPI_FUNCTION)
|
|
79
|
-
self.assertEqual(arg_type, "Callable[[List[WebhookEventTypeElement], Dict, Dict, Dict], None]")
|
|
80
|
-
|
|
81
|
-
def test_rewrite_reserved(self):
|
|
82
|
-
rv = rewrite_reserved("from")
|
|
83
|
-
self.assertEqual(rv, "_from")
|
|
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.6.dev0 → polyapi_python-0.3.7}/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
|