polyapi-python 0.3.7.dev6__tar.gz → 0.3.8__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.7.dev6/polyapi_python.egg-info → polyapi_python-0.3.8}/PKG-INFO +3 -2
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/README.md +2 -1
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi/cli.py +42 -1
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi/client.py +4 -4
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi/config.py +69 -1
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi/deployables.py +10 -7
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi/function_cli.py +4 -2
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi/generate.py +141 -44
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi/parser.py +7 -1
- polyapi_python-0.3.8/polyapi/poly_schemas.py +220 -0
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi/prepare.py +13 -2
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi/schema.py +16 -2
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi/typedefs.py +1 -0
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi/variables.py +89 -15
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi/webhook.py +22 -16
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8/polyapi_python.egg-info}/PKG-INFO +3 -2
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi_python.egg-info/requires.txt +1 -1
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/pyproject.toml +2 -2
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/tests/test_deployables.py +3 -3
- polyapi_python-0.3.8/tests/test_generate.py +662 -0
- polyapi_python-0.3.7.dev6/polyapi/poly_schemas.py +0 -100
- polyapi_python-0.3.7.dev6/tests/test_generate.py +0 -289
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/LICENSE +0 -0
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi/__init__.py +0 -0
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi/__main__.py +0 -0
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi/api.py +0 -0
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi/auth.py +0 -0
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi/constants.py +0 -0
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi/error_handler.py +0 -0
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi/exceptions.py +0 -0
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi/execute.py +0 -0
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi/py.typed +0 -0
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi/rendered_spec.py +0 -0
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi/server.py +0 -0
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi/sync.py +0 -0
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi/utils.py +0 -0
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi_python.egg-info/SOURCES.txt +0 -0
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi_python.egg-info/dependency_links.txt +0 -0
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/polyapi_python.egg-info/top_level.txt +0 -0
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/setup.cfg +0 -0
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/tests/test_api.py +0 -0
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/tests/test_auth.py +0 -0
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/tests/test_parser.py +0 -0
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/tests/test_rendered_spec.py +0 -0
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/tests/test_schema.py +0 -0
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/tests/test_server.py +0 -0
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/tests/test_utils.py +0 -0
- {polyapi_python-0.3.7.dev6 → polyapi_python-0.3.8}/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.8
|
|
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
|
|
@@ -31,7 +31,7 @@ License-File: LICENSE
|
|
|
31
31
|
Requires-Dist: requests>=2.32.3
|
|
32
32
|
Requires-Dist: typing_extensions>=4.12.2
|
|
33
33
|
Requires-Dist: jsonschema-gentypes==2.6.0
|
|
34
|
-
Requires-Dist: pydantic
|
|
34
|
+
Requires-Dist: pydantic>=2.8.0
|
|
35
35
|
Requires-Dist: stdlib_list==0.10.0
|
|
36
36
|
Requires-Dist: colorama==0.4.4
|
|
37
37
|
Requires-Dist: python-socketio[asyncio_client]==5.11.1
|
|
@@ -206,3 +206,4 @@ Please ignore \[name-defined\] errors for now. This is a known bug we are workin
|
|
|
206
206
|
## Support
|
|
207
207
|
|
|
208
208
|
If you run into any issues or want help getting started with this project, please contact support@polyapi.io
|
|
209
|
+
.
|
|
@@ -165,4 +165,5 @@ Please ignore \[name-defined\] errors for now. This is a known bug we are workin
|
|
|
165
165
|
|
|
166
166
|
## Support
|
|
167
167
|
|
|
168
|
-
If you run into any issues or want help getting started with this project, please contact support@polyapi.io
|
|
168
|
+
If you run into any issues or want help getting started with this project, please contact support@polyapi.io
|
|
169
|
+
.
|
|
@@ -14,6 +14,16 @@ from .sync import sync_deployables
|
|
|
14
14
|
CLI_COMMANDS = ["setup", "generate", "function", "clear", "help", "update_rendered_spec"]
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def _get_version_string():
|
|
18
|
+
"""Get the version string for the package."""
|
|
19
|
+
try:
|
|
20
|
+
import importlib.metadata
|
|
21
|
+
version = importlib.metadata.version('polyapi-python')
|
|
22
|
+
return version
|
|
23
|
+
except Exception:
|
|
24
|
+
return "Unknown"
|
|
25
|
+
|
|
26
|
+
|
|
17
27
|
def execute_from_cli():
|
|
18
28
|
# First we setup all our argument parsing logic
|
|
19
29
|
# Then we parse the arguments (waaay at the bottom)
|
|
@@ -22,6 +32,11 @@ def execute_from_cli():
|
|
|
22
32
|
description="Manage your Poly API configurations and functions",
|
|
23
33
|
formatter_class=argparse.RawTextHelpFormatter
|
|
24
34
|
)
|
|
35
|
+
|
|
36
|
+
# Add global --version/-v flag
|
|
37
|
+
parser.add_argument('-v', '--version', action='version',
|
|
38
|
+
version=_get_version_string(),
|
|
39
|
+
help="Show version information")
|
|
25
40
|
|
|
26
41
|
subparsers = parser.add_subparsers(help="Available commands")
|
|
27
42
|
|
|
@@ -36,6 +51,9 @@ def execute_from_cli():
|
|
|
36
51
|
set_api_key_and_url(args.url, args.api_key)
|
|
37
52
|
else:
|
|
38
53
|
initialize_config(force=True)
|
|
54
|
+
# setup command should have default cache values
|
|
55
|
+
from .config import cache_generate_args
|
|
56
|
+
cache_generate_args(contexts=None, names=None, function_ids=None, no_types=False)
|
|
39
57
|
generate()
|
|
40
58
|
|
|
41
59
|
setup_parser.set_defaults(command=setup)
|
|
@@ -46,11 +64,34 @@ def execute_from_cli():
|
|
|
46
64
|
generate_parser = subparsers.add_parser("generate", help="Generates Poly library")
|
|
47
65
|
generate_parser.add_argument("--no-types", action="store_true", help="Generate SDK without type definitions")
|
|
48
66
|
generate_parser.add_argument("--contexts", type=str, required=False, help="Contexts to generate")
|
|
67
|
+
generate_parser.add_argument("--names", type=str, required=False, help="Resource names to generate (comma-separated)")
|
|
68
|
+
generate_parser.add_argument("--function-ids", type=str, required=False, help="Function IDs to generate (comma-separated)")
|
|
49
69
|
|
|
50
70
|
def generate_command(args):
|
|
71
|
+
from .config import cache_generate_args
|
|
72
|
+
|
|
51
73
|
initialize_config()
|
|
74
|
+
|
|
52
75
|
contexts = args.contexts.split(",") if args.contexts else None
|
|
53
|
-
|
|
76
|
+
names = args.names.split(",") if args.names else None
|
|
77
|
+
function_ids = args.function_ids.split(",") if args.function_ids else None
|
|
78
|
+
no_types = args.no_types
|
|
79
|
+
|
|
80
|
+
# overwrite all cached values with the values passed in from the command line
|
|
81
|
+
final_contexts = contexts
|
|
82
|
+
final_names = names
|
|
83
|
+
final_function_ids = function_ids
|
|
84
|
+
final_no_types = no_types
|
|
85
|
+
|
|
86
|
+
# cache the values used for this explicit generate command
|
|
87
|
+
cache_generate_args(
|
|
88
|
+
contexts=final_contexts,
|
|
89
|
+
names=final_names,
|
|
90
|
+
function_ids=final_function_ids,
|
|
91
|
+
no_types=final_no_types
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
generate(contexts=final_contexts, names=final_names, function_ids=final_function_ids, no_types=final_no_types)
|
|
54
95
|
|
|
55
96
|
generate_parser.set_defaults(command=generate_command)
|
|
56
97
|
|
|
@@ -10,7 +10,7 @@ from typing import List, Dict, Any, TypedDict
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def _wrap_code_in_try_except(code: str) -> str:
|
|
13
|
+
def _wrap_code_in_try_except(function_name: str, code: str) -> str:
|
|
14
14
|
""" this is necessary because client functions with imports will blow up ALL server functions,
|
|
15
15
|
even if they don't use them.
|
|
16
16
|
because the server function will try to load all client functions when loading the library
|
|
@@ -18,8 +18,8 @@ def _wrap_code_in_try_except(code: str) -> str:
|
|
|
18
18
|
prefix = """logger = logging.getLogger("poly")
|
|
19
19
|
try:
|
|
20
20
|
"""
|
|
21
|
-
suffix = """except ImportError as e:
|
|
22
|
-
logger.
|
|
21
|
+
suffix = f"""except ImportError as e:
|
|
22
|
+
logger.warning("Failed to import client function '{function_name}', function unavailable: " + str(e))"""
|
|
23
23
|
|
|
24
24
|
lines = code.split("\n")
|
|
25
25
|
code = "\n ".join(lines)
|
|
@@ -39,6 +39,6 @@ def render_client_function(
|
|
|
39
39
|
return_type_def=return_type_def,
|
|
40
40
|
)
|
|
41
41
|
|
|
42
|
-
code = _wrap_code_in_try_except(code)
|
|
42
|
+
code = _wrap_code_in_try_except(function_name, code)
|
|
43
43
|
|
|
44
44
|
return code + "\n\n", func_type_defs
|
|
@@ -12,6 +12,10 @@ API_FUNCTION_DIRECT_EXECUTE = None
|
|
|
12
12
|
MTLS_CERT_PATH = None
|
|
13
13
|
MTLS_KEY_PATH = None
|
|
14
14
|
MTLS_CA_PATH = None
|
|
15
|
+
LAST_GENERATE_CONTEXTS = None
|
|
16
|
+
LAST_GENERATE_NAMES = None
|
|
17
|
+
LAST_GENERATE_FUNCTION_IDS = None
|
|
18
|
+
LAST_GENERATE_NO_TYPES = None
|
|
15
19
|
|
|
16
20
|
|
|
17
21
|
def get_config_file_path() -> str:
|
|
@@ -55,6 +59,16 @@ def get_api_key_and_url() -> Tuple[str | None, str | None]:
|
|
|
55
59
|
MTLS_CERT_PATH = config.get("polyapi", "mtls_cert_path", fallback=None)
|
|
56
60
|
MTLS_KEY_PATH = config.get("polyapi", "mtls_key_path", fallback=None)
|
|
57
61
|
MTLS_CA_PATH = config.get("polyapi", "mtls_ca_path", fallback=None)
|
|
62
|
+
|
|
63
|
+
# Read and cache generate command arguments
|
|
64
|
+
global LAST_GENERATE_CONTEXTS, LAST_GENERATE_NAMES, LAST_GENERATE_FUNCTION_IDS, LAST_GENERATE_NO_TYPES
|
|
65
|
+
contexts_str = config.get("polyapi", "last_generate_contexts_used", fallback=None)
|
|
66
|
+
LAST_GENERATE_CONTEXTS = contexts_str.split(",") if contexts_str else None
|
|
67
|
+
names_str = config.get("polyapi", "last_generate_names_used", fallback=None)
|
|
68
|
+
LAST_GENERATE_NAMES = names_str.split(",") if names_str else None
|
|
69
|
+
function_ids_str = config.get("polyapi", "last_generate_function_ids_used", fallback=None)
|
|
70
|
+
LAST_GENERATE_FUNCTION_IDS = function_ids_str.split(",") if function_ids_str else None
|
|
71
|
+
LAST_GENERATE_NO_TYPES = config.get("polyapi", "last_generate_no_types_used", fallback="false").lower() == "true"
|
|
58
72
|
|
|
59
73
|
return key, url
|
|
60
74
|
|
|
@@ -133,4 +147,58 @@ def get_direct_execute_config() -> bool:
|
|
|
133
147
|
if API_FUNCTION_DIRECT_EXECUTE is None:
|
|
134
148
|
# Force a config read if value isn't cached
|
|
135
149
|
get_api_key_and_url()
|
|
136
|
-
return bool(API_FUNCTION_DIRECT_EXECUTE)
|
|
150
|
+
return bool(API_FUNCTION_DIRECT_EXECUTE)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def get_cached_generate_args() -> Tuple[list | None, list | None, list | None, bool]:
|
|
154
|
+
"""Return cached generate command arguments"""
|
|
155
|
+
global LAST_GENERATE_CONTEXTS, LAST_GENERATE_NAMES, LAST_GENERATE_FUNCTION_IDS, LAST_GENERATE_NO_TYPES
|
|
156
|
+
if LAST_GENERATE_CONTEXTS is None and LAST_GENERATE_NAMES is None and LAST_GENERATE_FUNCTION_IDS is None and LAST_GENERATE_NO_TYPES is None:
|
|
157
|
+
# Force a config read if values aren't cached
|
|
158
|
+
get_api_key_and_url()
|
|
159
|
+
return LAST_GENERATE_CONTEXTS, LAST_GENERATE_NAMES, LAST_GENERATE_FUNCTION_IDS, bool(LAST_GENERATE_NO_TYPES)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def cache_generate_args(contexts: list | None = None, names: list | None = None, function_ids: list | None = None, no_types: bool = False):
|
|
163
|
+
"""Cache generate command arguments to config file"""
|
|
164
|
+
from typing import List
|
|
165
|
+
|
|
166
|
+
# Read existing config
|
|
167
|
+
path = get_config_file_path()
|
|
168
|
+
config = configparser.ConfigParser()
|
|
169
|
+
|
|
170
|
+
if os.path.exists(path):
|
|
171
|
+
with open(path, "r") as f:
|
|
172
|
+
config.read_file(f)
|
|
173
|
+
|
|
174
|
+
# Ensure polyapi section exists
|
|
175
|
+
if "polyapi" not in config:
|
|
176
|
+
config["polyapi"] = {}
|
|
177
|
+
|
|
178
|
+
# Update cached values
|
|
179
|
+
global LAST_GENERATE_CONTEXTS, LAST_GENERATE_NAMES, LAST_GENERATE_FUNCTION_IDS, LAST_GENERATE_NO_TYPES
|
|
180
|
+
LAST_GENERATE_CONTEXTS = contexts
|
|
181
|
+
LAST_GENERATE_NAMES = names
|
|
182
|
+
LAST_GENERATE_FUNCTION_IDS = function_ids
|
|
183
|
+
LAST_GENERATE_NO_TYPES = no_types
|
|
184
|
+
|
|
185
|
+
# Write values to config
|
|
186
|
+
if contexts is not None:
|
|
187
|
+
config.set("polyapi", "last_generate_contexts_used", ",".join(contexts))
|
|
188
|
+
elif config.has_option("polyapi", "last_generate_contexts_used"):
|
|
189
|
+
config.remove_option("polyapi", "last_generate_contexts_used")
|
|
190
|
+
|
|
191
|
+
if names is not None:
|
|
192
|
+
config.set("polyapi", "last_generate_names_used", ",".join(names))
|
|
193
|
+
elif config.has_option("polyapi", "last_generate_names_used"):
|
|
194
|
+
config.remove_option("polyapi", "last_generate_names_used")
|
|
195
|
+
|
|
196
|
+
if function_ids is not None:
|
|
197
|
+
config.set("polyapi", "last_generate_function_ids_used", ",".join(function_ids))
|
|
198
|
+
elif config.has_option("polyapi", "last_generate_function_ids_used"):
|
|
199
|
+
config.remove_option("polyapi", "last_generate_function_ids_used")
|
|
200
|
+
|
|
201
|
+
config.set("polyapi", "last_generate_no_types_used", str(no_types).lower())
|
|
202
|
+
|
|
203
|
+
with open(path, "w") as f:
|
|
204
|
+
config.write(f)
|
|
@@ -31,6 +31,7 @@ class ParsedDeployableConfig(TypedDict):
|
|
|
31
31
|
context: str
|
|
32
32
|
name: str
|
|
33
33
|
type: DeployableTypes
|
|
34
|
+
description: Optional[str]
|
|
34
35
|
disableAi: Optional[bool]
|
|
35
36
|
config: Dict[str, Any]
|
|
36
37
|
|
|
@@ -112,20 +113,22 @@ class PolyDeployConfig(TypedDict):
|
|
|
112
113
|
|
|
113
114
|
def get_all_deployable_files_windows(config: PolyDeployConfig) -> List[str]:
|
|
114
115
|
# Constructing the Windows command using dir and findstr
|
|
115
|
-
include_pattern = " ".join(f"*.{f}"
|
|
116
|
-
exclude_pattern = '
|
|
117
|
-
pattern = '
|
|
116
|
+
include_pattern = " ".join(f"*.{f}" for f in config["include_files_or_extensions"]) or "*"
|
|
117
|
+
exclude_pattern = ' '.join(f"\\{f}" for f in config["exclude_dirs"])
|
|
118
|
+
pattern = ' '.join(f"/C:\"polyConfig: {name}\"" for name in config["type_names"]) or '/C:"polyConfig"'
|
|
118
119
|
|
|
119
120
|
exclude_command = f" | findstr /V /I \"{exclude_pattern}\"" if exclude_pattern else ''
|
|
120
|
-
search_command = f" | findstr /M /I /F:/
|
|
121
|
+
search_command = f" | findstr /S /M /I /F:/ {pattern} *.*"
|
|
121
122
|
|
|
122
123
|
result = []
|
|
123
124
|
for dir_path in config["include_dirs"]:
|
|
124
|
-
|
|
125
|
+
if dir_path != '.':
|
|
126
|
+
include_pattern = " ".join(f"{dir_path}*.{f}" for f in config["include_files_or_extensions"]) or "*"
|
|
127
|
+
dir_command = f"dir {include_pattern} /S /P /B > NUL"
|
|
125
128
|
full_command = f"{dir_command}{exclude_command}{search_command}"
|
|
126
129
|
try:
|
|
127
130
|
output = subprocess.check_output(full_command, shell=True, text=True)
|
|
128
|
-
result.extend(output.strip().split('\
|
|
131
|
+
result.extend(output.strip().split('\n'))
|
|
129
132
|
except subprocess.CalledProcessError:
|
|
130
133
|
pass
|
|
131
134
|
return result
|
|
@@ -154,7 +157,7 @@ def get_all_deployable_files(config: PolyDeployConfig) -> List[str]:
|
|
|
154
157
|
if not config.get("include_files_or_extensions"):
|
|
155
158
|
config["include_files_or_extensions"] = ["py"]
|
|
156
159
|
if not config.get("exclude_dirs"):
|
|
157
|
-
config["exclude_dirs"] = ["
|
|
160
|
+
config["exclude_dirs"] = ["Lib", "node_modules", "dist", "build", "output", ".vscode", ".poly", ".github", ".husky", ".yarn", ".venv"]
|
|
158
161
|
|
|
159
162
|
is_windows = os.name == "nt"
|
|
160
163
|
if is_windows:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
from typing import Any, List, Optional
|
|
3
3
|
import requests
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
from polyapi.config import get_api_key_and_url
|
|
6
6
|
from polyapi.utils import get_auth_headers, print_green, print_red, print_yellow
|
|
7
7
|
from polyapi.parser import parse_function_code, get_jsonschema_type
|
|
@@ -91,7 +91,9 @@ def function_add_or_update(
|
|
|
91
91
|
function_id = resp.json()["id"]
|
|
92
92
|
print(f"Function ID: {function_id}")
|
|
93
93
|
if generate:
|
|
94
|
-
|
|
94
|
+
# Use cached generate arguments when regenerating after function deployment
|
|
95
|
+
from polyapi.generate import generate_from_cache
|
|
96
|
+
generate_from_cache()
|
|
95
97
|
else:
|
|
96
98
|
print("Error adding function.")
|
|
97
99
|
print(resp.status_code)
|
|
@@ -2,7 +2,9 @@ import json
|
|
|
2
2
|
import requests
|
|
3
3
|
import os
|
|
4
4
|
import shutil
|
|
5
|
-
|
|
5
|
+
import logging
|
|
6
|
+
import tempfile
|
|
7
|
+
from typing import Any, List, Optional, Tuple, cast
|
|
6
8
|
|
|
7
9
|
from .auth import render_auth_function
|
|
8
10
|
from .client import render_client_function
|
|
@@ -14,7 +16,7 @@ from .api import render_api_function
|
|
|
14
16
|
from .server import render_server_function
|
|
15
17
|
from .utils import add_import_to_init, get_auth_headers, init_the_init, print_green, to_func_namespace
|
|
16
18
|
from .variables import generate_variables
|
|
17
|
-
from .config import get_api_key_and_url, get_direct_execute_config
|
|
19
|
+
from .config import get_api_key_and_url, get_direct_execute_config, get_cached_generate_args
|
|
18
20
|
|
|
19
21
|
SUPPORTED_FUNCTION_TYPES = {
|
|
20
22
|
"apiFunction",
|
|
@@ -36,15 +38,21 @@ Unresolved schema, please add the following schema to complete it:
|
|
|
36
38
|
path:'''
|
|
37
39
|
|
|
38
40
|
|
|
39
|
-
def get_specs(contexts=Optional[List[str]], no_types: bool = False) -> List:
|
|
41
|
+
def get_specs(contexts: Optional[List[str]] = None, names: Optional[List[str]] = None, function_ids: Optional[List[str]] = None, no_types: bool = False) -> List:
|
|
40
42
|
api_key, api_url = get_api_key_and_url()
|
|
41
43
|
assert api_key
|
|
42
44
|
headers = get_auth_headers(api_key)
|
|
43
45
|
url = f"{api_url}/specs"
|
|
44
|
-
params = {"noTypes": str(no_types).lower()}
|
|
46
|
+
params: Any = {"noTypes": str(no_types).lower()}
|
|
45
47
|
|
|
46
48
|
if contexts:
|
|
47
49
|
params["contexts"] = contexts
|
|
50
|
+
|
|
51
|
+
if names:
|
|
52
|
+
params["names"] = names
|
|
53
|
+
|
|
54
|
+
if function_ids:
|
|
55
|
+
params["functionIds"] = function_ids
|
|
48
56
|
|
|
49
57
|
# Add apiFunctionDirectExecute parameter if direct execute is enabled
|
|
50
58
|
if get_direct_execute_config():
|
|
@@ -149,7 +157,7 @@ def parse_function_specs(
|
|
|
149
157
|
|
|
150
158
|
# Functions with serverSideAsync True will always return a Dict with execution ID
|
|
151
159
|
if spec.get('serverSideAsync') and spec.get("function"):
|
|
152
|
-
spec['function']['returnType'] = {'kind': 'plain', 'value': 'object'}
|
|
160
|
+
spec['function']['returnType'] = {'kind': 'plain', 'value': 'object'} # type: ignore
|
|
153
161
|
|
|
154
162
|
functions.append(spec)
|
|
155
163
|
|
|
@@ -264,12 +272,26 @@ sys.modules[__name__] = _SchemaModule()
|
|
|
264
272
|
''')
|
|
265
273
|
|
|
266
274
|
|
|
267
|
-
def
|
|
275
|
+
def generate_from_cache() -> None:
|
|
276
|
+
"""
|
|
277
|
+
Generate using cached values after non-explicit call.
|
|
278
|
+
"""
|
|
279
|
+
cached_contexts, cached_names, cached_function_ids, cached_no_types = get_cached_generate_args()
|
|
280
|
+
|
|
281
|
+
generate(
|
|
282
|
+
contexts=cached_contexts,
|
|
283
|
+
names=cached_names,
|
|
284
|
+
function_ids=cached_function_ids,
|
|
285
|
+
no_types=cached_no_types
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def generate(contexts: Optional[List[str]] = None, names: Optional[List[str]] = None, function_ids: Optional[List[str]] = None, no_types: bool = False) -> None:
|
|
268
290
|
generate_msg = f"Generating Poly Python SDK for contexts ${contexts}..." if contexts else "Generating Poly Python SDK..."
|
|
269
291
|
print(generate_msg, end="", flush=True)
|
|
270
292
|
remove_old_library()
|
|
271
293
|
|
|
272
|
-
specs = get_specs(
|
|
294
|
+
specs = get_specs(contexts=contexts, names=names, function_ids=function_ids, no_types=no_types)
|
|
273
295
|
cache_specs(specs)
|
|
274
296
|
|
|
275
297
|
limit_ids: List[str] = [] # useful for narrowing down generation to a single function to debug
|
|
@@ -301,11 +323,9 @@ def generate(contexts: Optional[List[str]] = None, no_types: bool = False) -> No
|
|
|
301
323
|
)
|
|
302
324
|
exit()
|
|
303
325
|
|
|
304
|
-
|
|
305
|
-
if
|
|
306
|
-
variables
|
|
307
|
-
if variables:
|
|
308
|
-
generate_variables(variables)
|
|
326
|
+
variables = get_variables()
|
|
327
|
+
if variables:
|
|
328
|
+
generate_variables(variables)
|
|
309
329
|
|
|
310
330
|
# indicator to vscode extension that this is a polyapi-python project
|
|
311
331
|
file_path = os.path.join(os.getcwd(), ".polyapi-python")
|
|
@@ -334,8 +354,9 @@ def render_spec(spec: SpecificationDto) -> Tuple[str, str]:
|
|
|
334
354
|
function_id = spec["id"]
|
|
335
355
|
|
|
336
356
|
arguments: List[PropertySpecification] = []
|
|
337
|
-
return_type = {}
|
|
357
|
+
return_type: Any = {}
|
|
338
358
|
if spec.get("function"):
|
|
359
|
+
assert spec["function"]
|
|
339
360
|
# Handle cases where arguments might be missing or None
|
|
340
361
|
if spec["function"].get("arguments"):
|
|
341
362
|
arguments = [
|
|
@@ -407,48 +428,124 @@ def add_function_file(
|
|
|
407
428
|
function_name: str,
|
|
408
429
|
spec: SpecificationDto,
|
|
409
430
|
):
|
|
410
|
-
|
|
411
|
-
|
|
431
|
+
"""
|
|
432
|
+
Atomically add a function file to prevent partial corruption during generation failures.
|
|
433
|
+
|
|
434
|
+
This function generates all content first, then writes files atomically using temporary files
|
|
435
|
+
to ensure that either the entire operation succeeds or no changes are made to the filesystem.
|
|
436
|
+
"""
|
|
437
|
+
try:
|
|
438
|
+
# first lets add the import to the __init__
|
|
439
|
+
init_the_init(full_path)
|
|
412
440
|
|
|
413
|
-
|
|
441
|
+
func_str, func_type_defs = render_spec(spec)
|
|
414
442
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
with open(init_path, "a") as f:
|
|
419
|
-
f.write(f"\n\nfrom . import {to_func_namespace(function_name)}\n\n{func_str}")
|
|
443
|
+
if not func_str:
|
|
444
|
+
# If render_spec failed and returned empty string, don't create any files
|
|
445
|
+
raise Exception("Function rendering failed - empty function string returned")
|
|
420
446
|
|
|
421
|
-
#
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
447
|
+
# Prepare all content first before writing any files
|
|
448
|
+
func_namespace = to_func_namespace(function_name)
|
|
449
|
+
init_path = os.path.join(full_path, "__init__.py")
|
|
450
|
+
func_file_path = os.path.join(full_path, f"{func_namespace}.py")
|
|
451
|
+
|
|
452
|
+
# Read current __init__.py content if it exists
|
|
453
|
+
init_content = ""
|
|
454
|
+
if os.path.exists(init_path):
|
|
455
|
+
with open(init_path, "r") as f:
|
|
456
|
+
init_content = f.read()
|
|
457
|
+
|
|
458
|
+
# Prepare new content to append to __init__.py
|
|
459
|
+
new_init_content = init_content + f"\n\nfrom . import {func_namespace}\n\n{func_str}"
|
|
460
|
+
|
|
461
|
+
# Use temporary files for atomic writes
|
|
462
|
+
# Write to __init__.py atomically
|
|
463
|
+
with tempfile.NamedTemporaryFile(mode="w", delete=False, dir=full_path, suffix=".tmp") as temp_init:
|
|
464
|
+
temp_init.write(new_init_content)
|
|
465
|
+
temp_init_path = temp_init.name
|
|
466
|
+
|
|
467
|
+
# Write to function file atomically
|
|
468
|
+
with tempfile.NamedTemporaryFile(mode="w", delete=False, dir=full_path, suffix=".tmp") as temp_func:
|
|
469
|
+
temp_func.write(func_type_defs)
|
|
470
|
+
temp_func_path = temp_func.name
|
|
471
|
+
|
|
472
|
+
# Atomic operations: move temp files to final locations
|
|
473
|
+
shutil.move(temp_init_path, init_path)
|
|
474
|
+
shutil.move(temp_func_path, func_file_path)
|
|
475
|
+
|
|
476
|
+
except Exception as e:
|
|
477
|
+
# Clean up any temporary files that might have been created
|
|
478
|
+
try:
|
|
479
|
+
if 'temp_init_path' in locals() and os.path.exists(temp_init_path):
|
|
480
|
+
os.unlink(temp_init_path)
|
|
481
|
+
if 'temp_func_path' in locals() and os.path.exists(temp_func_path):
|
|
482
|
+
os.unlink(temp_func_path)
|
|
483
|
+
except:
|
|
484
|
+
pass # Best effort cleanup
|
|
485
|
+
|
|
486
|
+
# Re-raise the original exception
|
|
487
|
+
raise e
|
|
425
488
|
|
|
426
489
|
|
|
427
490
|
def create_function(
|
|
428
491
|
spec: SpecificationDto
|
|
429
492
|
) -> None:
|
|
493
|
+
"""
|
|
494
|
+
Create a function with atomic directory and file operations.
|
|
495
|
+
|
|
496
|
+
Tracks directory creation to enable cleanup on failure.
|
|
497
|
+
"""
|
|
430
498
|
full_path = os.path.dirname(os.path.abspath(__file__))
|
|
431
499
|
folders = f"poly.{spec['context']}.{spec['name']}".split(".")
|
|
432
|
-
for
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
500
|
+
created_dirs = [] # Track directories we create for cleanup on failure
|
|
501
|
+
|
|
502
|
+
try:
|
|
503
|
+
for idx, folder in enumerate(folders):
|
|
504
|
+
if idx + 1 == len(folders):
|
|
505
|
+
# special handling for final level
|
|
506
|
+
add_function_file(
|
|
507
|
+
full_path,
|
|
508
|
+
folder,
|
|
509
|
+
spec,
|
|
510
|
+
)
|
|
511
|
+
else:
|
|
512
|
+
full_path = os.path.join(full_path, folder)
|
|
513
|
+
if not os.path.exists(full_path):
|
|
514
|
+
os.makedirs(full_path)
|
|
515
|
+
created_dirs.append(full_path) # Track for cleanup
|
|
516
|
+
|
|
517
|
+
# append to __init__.py file if nested folders
|
|
518
|
+
next = folders[idx + 1] if idx + 2 < len(folders) else ""
|
|
519
|
+
if next:
|
|
520
|
+
init_the_init(full_path)
|
|
521
|
+
add_import_to_init(full_path, next)
|
|
522
|
+
|
|
523
|
+
except Exception as e:
|
|
524
|
+
# Clean up directories we created (in reverse order)
|
|
525
|
+
for dir_path in reversed(created_dirs):
|
|
526
|
+
try:
|
|
527
|
+
if os.path.exists(dir_path) and not os.listdir(dir_path): # Only remove if empty
|
|
528
|
+
os.rmdir(dir_path)
|
|
529
|
+
except:
|
|
530
|
+
pass # Best effort cleanup
|
|
531
|
+
|
|
532
|
+
# Re-raise the original exception
|
|
533
|
+
raise e
|
|
450
534
|
|
|
451
535
|
|
|
452
536
|
def generate_functions(functions: List[SpecificationDto]) -> None:
|
|
537
|
+
failed_functions = []
|
|
453
538
|
for func in functions:
|
|
454
|
-
|
|
539
|
+
try:
|
|
540
|
+
create_function(func)
|
|
541
|
+
except Exception as e:
|
|
542
|
+
function_path = f"{func.get('context', 'unknown')}.{func.get('name', 'unknown')}"
|
|
543
|
+
function_id = func.get('id', 'unknown')
|
|
544
|
+
failed_functions.append(f"{function_path} (id: {function_id})")
|
|
545
|
+
logging.warning(f"WARNING: Failed to generate function {function_path} (id: {function_id}): {str(e)}")
|
|
546
|
+
continue
|
|
547
|
+
|
|
548
|
+
if failed_functions:
|
|
549
|
+
logging.warning(f"WARNING: {len(failed_functions)} function(s) failed to generate:")
|
|
550
|
+
for failed_func in failed_functions:
|
|
551
|
+
logging.warning(f" - {failed_func}")
|
|
@@ -513,7 +513,13 @@ def parse_function_code(code: str, name: Optional[str] = "", context: Optional[s
|
|
|
513
513
|
deployable["context"] = context or deployable["config"].get("context", "")
|
|
514
514
|
deployable["name"] = name or deployable["config"].get("name", "")
|
|
515
515
|
deployable["disableAi"] = deployable["config"].get("disableAi", False)
|
|
516
|
-
deployable["description"] = deployable["
|
|
516
|
+
deployable["description"] = deployable["config"].get("description", "")
|
|
517
|
+
if deployable["description"]:
|
|
518
|
+
if deployable["description"] != deployable["types"].get("description", ""):
|
|
519
|
+
deployable["types"]["description"] = deployable["description"]
|
|
520
|
+
deployable["dirty"] = True
|
|
521
|
+
else:
|
|
522
|
+
deployable["description"] = deployable["types"].get("description", "")
|
|
517
523
|
if not deployable["name"]:
|
|
518
524
|
print_red("ERROR")
|
|
519
525
|
print("Function config is missing a name.")
|