polyapi-python 0.3.7.dev7__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.
Files changed (48) hide show
  1. {polyapi_python-0.3.7.dev7/polyapi_python.egg-info → polyapi_python-0.3.8}/PKG-INFO +3 -2
  2. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/README.md +2 -1
  3. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi/cli.py +42 -1
  4. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi/client.py +4 -4
  5. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi/config.py +69 -1
  6. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi/deployables.py +10 -7
  7. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi/function_cli.py +4 -2
  8. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi/generate.py +141 -44
  9. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi/parser.py +7 -1
  10. polyapi_python-0.3.8/polyapi/poly_schemas.py +220 -0
  11. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi/prepare.py +13 -2
  12. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi/schema.py +16 -2
  13. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi/typedefs.py +1 -0
  14. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi/variables.py +89 -15
  15. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi/webhook.py +22 -16
  16. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8/polyapi_python.egg-info}/PKG-INFO +3 -2
  17. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi_python.egg-info/requires.txt +1 -1
  18. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/pyproject.toml +2 -2
  19. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/tests/test_deployables.py +3 -3
  20. polyapi_python-0.3.8/tests/test_generate.py +662 -0
  21. polyapi_python-0.3.7.dev7/polyapi/poly_schemas.py +0 -100
  22. polyapi_python-0.3.7.dev7/tests/test_generate.py +0 -289
  23. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/LICENSE +0 -0
  24. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi/__init__.py +0 -0
  25. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi/__main__.py +0 -0
  26. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi/api.py +0 -0
  27. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi/auth.py +0 -0
  28. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi/constants.py +0 -0
  29. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi/error_handler.py +0 -0
  30. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi/exceptions.py +0 -0
  31. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi/execute.py +0 -0
  32. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi/py.typed +0 -0
  33. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi/rendered_spec.py +0 -0
  34. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi/server.py +0 -0
  35. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi/sync.py +0 -0
  36. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi/utils.py +0 -0
  37. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi_python.egg-info/SOURCES.txt +0 -0
  38. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi_python.egg-info/dependency_links.txt +0 -0
  39. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/polyapi_python.egg-info/top_level.txt +0 -0
  40. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/setup.cfg +0 -0
  41. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/tests/test_api.py +0 -0
  42. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/tests/test_auth.py +0 -0
  43. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/tests/test_parser.py +0 -0
  44. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/tests/test_rendered_spec.py +0 -0
  45. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/tests/test_schema.py +0 -0
  46. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/tests/test_server.py +0 -0
  47. {polyapi_python-0.3.7.dev7 → polyapi_python-0.3.8}/tests/test_utils.py +0 -0
  48. {polyapi_python-0.3.7.dev7 → 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.7.dev7
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==2.6.4
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
- generate(contexts=contexts, no_types=args.no_types)
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.debug(e)"""
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}" if "." in f else f"*.{f}" for f in config["include_files_or_extensions"]) or "*"
116
- exclude_pattern = '|'.join(config["exclude_dirs"])
117
- pattern = '|'.join(f"polyConfig: {name}" for name in config["type_names"]) or 'polyConfig'
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:/ /C:\"{pattern}\""
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
- dir_command = f"dir /S /P /B {include_pattern} {dir_path}"
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('\r\n'))
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"] = ["poly", "node_modules", "dist", "build", "output", ".vscode", ".poly", ".github", ".husky", ".yarn"]
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
- from polyapi.generate import generate as generate_library
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
- generate_library()
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
- from typing import List, Optional, Tuple, cast
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 generate(contexts: Optional[List[str]] = None, no_types: bool = False) -> None:
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(no_types=no_types, contexts=contexts)
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
- # Only process variables if no_types is False
305
- if not no_types:
306
- variables = get_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
- # first lets add the import to the __init__
411
- init_the_init(full_path)
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
- func_str, func_type_defs = render_spec(spec)
441
+ func_str, func_type_defs = render_spec(spec)
414
442
 
415
- if func_str:
416
- # add function to init
417
- init_path = os.path.join(full_path, "__init__.py")
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
- # add type_defs to underscore file
422
- file_path = os.path.join(full_path, f"{to_func_namespace(function_name)}.py")
423
- with open(file_path, "w") as f:
424
- f.write(func_type_defs)
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 idx, folder in enumerate(folders):
433
- if idx + 1 == len(folders):
434
- # special handling for final level
435
- add_function_file(
436
- full_path,
437
- folder,
438
- spec,
439
- )
440
- else:
441
- full_path = os.path.join(full_path, folder)
442
- if not os.path.exists(full_path):
443
- os.makedirs(full_path)
444
-
445
- # append to __init__.py file if nested folders
446
- next = folders[idx + 1] if idx + 2 < len(folders) else ""
447
- if next:
448
- init_the_init(full_path)
449
- add_import_to_init(full_path, next)
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
- create_function(func)
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["types"].get("description", "")
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.")