polyapi-python 0.3.2.dev2__tar.gz → 0.3.3.dev2__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 (46) hide show
  1. {polyapi_python-0.3.2.dev2/polyapi_python.egg-info → polyapi_python-0.3.3.dev2}/PKG-INFO +15 -4
  2. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/README.md +10 -0
  3. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi/api.py +1 -0
  4. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi/cli.py +3 -2
  5. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi/deployables.py +4 -4
  6. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi/function_cli.py +6 -3
  7. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi/generate.py +93 -21
  8. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi/parser.py +6 -6
  9. polyapi_python-0.3.3.dev2/polyapi/poly_schemas.py +60 -0
  10. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi/schema.py +24 -2
  11. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi/typedefs.py +14 -0
  12. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi/utils.py +19 -10
  13. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2/polyapi_python.egg-info}/PKG-INFO +15 -4
  14. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi_python.egg-info/SOURCES.txt +2 -0
  15. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi_python.egg-info/requires.txt +2 -2
  16. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/pyproject.toml +3 -3
  17. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/tests/test_deployables.py +3 -4
  18. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/tests/test_parser.py +1 -1
  19. polyapi_python-0.3.3.dev2/tests/test_utils.py +14 -0
  20. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/LICENSE +0 -0
  21. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi/__init__.py +0 -0
  22. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi/__main__.py +0 -0
  23. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi/auth.py +0 -0
  24. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi/client.py +0 -0
  25. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi/config.py +0 -0
  26. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi/constants.py +0 -0
  27. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi/error_handler.py +0 -0
  28. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi/exceptions.py +0 -0
  29. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi/execute.py +0 -0
  30. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi/prepare.py +0 -0
  31. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi/py.typed +0 -0
  32. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi/rendered_spec.py +0 -0
  33. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi/server.py +0 -0
  34. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi/sync.py +0 -0
  35. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi/variables.py +0 -0
  36. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi/webhook.py +0 -0
  37. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi_python.egg-info/dependency_links.txt +0 -0
  38. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/polyapi_python.egg-info/top_level.txt +0 -0
  39. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/setup.cfg +0 -0
  40. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/tests/test_api.py +0 -0
  41. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/tests/test_auth.py +0 -0
  42. /polyapi_python-0.3.2.dev2/tests/test_utils.py → /polyapi_python-0.3.3.dev2/tests/test_generate.py +0 -0
  43. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/tests/test_rendered_spec.py +0 -0
  44. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/tests/test_schema.py +0 -0
  45. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/tests/test_server.py +0 -0
  46. {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev2}/tests/test_variables.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: polyapi-python
3
- Version: 0.3.2.dev2
3
+ Version: 0.3.3.dev2
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
@@ -28,14 +28,15 @@ Project-URL: Homepage, https://github.com/polyapi/polyapi-python
28
28
  Requires-Python: >=3.10
29
29
  Description-Content-Type: text/markdown
30
30
  License-File: LICENSE
31
- Requires-Dist: requests==2.31.0
32
- Requires-Dist: typing_extensions>=4.10.0
31
+ Requires-Dist: requests>=2.32.3
32
+ Requires-Dist: typing_extensions>=4.12.2
33
33
  Requires-Dist: jsonschema-gentypes==2.6.0
34
34
  Requires-Dist: pydantic==2.6.4
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
38
38
  Requires-Dist: truststore==0.8.0
39
+ Dynamic: license-file
39
40
 
40
41
  # PolyAPI Python Library
41
42
 
@@ -182,6 +183,16 @@ To run this library's unit tests, please clone the repo then run:
182
183
  python -m unittest discover
183
184
  ```
184
185
 
186
+ ## Linting
187
+
188
+ The flake8 config is at the root of this repo at `.flake8`.
189
+
190
+ When hacking on this library, please enable flake8 and add this line to your flake8 args (e.g., in your VSCode Workspace Settings):
191
+
192
+ ```
193
+ --config=.flake8
194
+ ```
195
+
185
196
  ## Support
186
197
 
187
198
  If you run into any issues or want help getting started with this project, please contact support@polyapi.io
@@ -143,6 +143,16 @@ To run this library's unit tests, please clone the repo then run:
143
143
  python -m unittest discover
144
144
  ```
145
145
 
146
+ ## Linting
147
+
148
+ The flake8 config is at the root of this repo at `.flake8`.
149
+
150
+ When hacking on this library, please enable flake8 and add this line to your flake8 args (e.g., in your VSCode Workspace Settings):
151
+
152
+ ```
153
+ --config=.flake8
154
+ ```
155
+
146
156
  ## Support
147
157
 
148
158
  If you run into any issues or want help getting started with this project, please contact support@polyapi.io
@@ -8,6 +8,7 @@ API_DEFS_TEMPLATE = """
8
8
  from typing import List, Dict, Any, TypedDict
9
9
  {args_def}
10
10
  {return_type_def}
11
+
11
12
  class {api_response_type}(TypedDict):
12
13
  status: int
13
14
  headers: Dict
@@ -13,6 +13,7 @@ from .sync import sync_deployables
13
13
 
14
14
  CLI_COMMANDS = ["setup", "generate", "function", "clear", "help", "update_rendered_spec"]
15
15
 
16
+
16
17
  def execute_from_cli():
17
18
  # First we setup all our argument parsing logic
18
19
  # Then we parse the arguments (waaay at the bottom)
@@ -21,7 +22,7 @@ def execute_from_cli():
21
22
  description="Manage your Poly API configurations and functions",
22
23
  formatter_class=argparse.RawTextHelpFormatter
23
24
  )
24
-
25
+
25
26
  subparsers = parser.add_subparsers(help="Available commands")
26
27
 
27
28
  ###########################################################################
@@ -46,7 +47,7 @@ def execute_from_cli():
46
47
 
47
48
  def generate_command(args):
48
49
  initialize_config()
49
- print("Generating Poly functions...", end="")
50
+ print("Generating Poly Python SDK...", end="")
50
51
  generate()
51
52
  print_green("DONE")
52
53
 
@@ -261,7 +261,7 @@ def update_deployable_function_comments(file_content: str, deployable: dict, dis
261
261
  if deployable["docStartIndex"] == deployable["docEndIndex"]:
262
262
  # Function doesn't yet have any docstrings so we need to add additional whitespace
263
263
  docstring = " " + docstring + "\n"
264
-
264
+
265
265
  return f"{file_content[:deployable['docStartIndex']]}{docstring}{file_content[deployable['docEndIndex']:]}"
266
266
  return file_content
267
267
 
@@ -271,17 +271,17 @@ def write_updated_deployable(deployable: dict, disable_docs: bool = False) -> di
271
271
  """
272
272
  with open(deployable['file'], 'r', encoding='utf-8') as file:
273
273
  file_contents = file.read()
274
-
274
+
275
275
  if deployable['type'] in ['client-function', 'server-function']:
276
276
  file_contents = update_deployable_function_comments(file_contents, deployable, disable_docs)
277
277
  else:
278
278
  raise ValueError(f"Unsupported deployable type: '{deployable['type']}'")
279
279
 
280
280
  file_contents = update_deployment_comments(file_contents, deployable)
281
-
281
+
282
282
  with open(deployable['file'], 'w', encoding='utf-8') as file:
283
283
  file.write(file_contents)
284
-
284
+
285
285
  deployable['fileRevision'] = get_deployable_file_revision(file_contents)
286
286
  return deployable
287
287
 
@@ -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 get_functions_and_parse, generate_functions
4
+ from polyapi.generate import cache_specs, generate_functions, get_specs, parse_function_specs
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
@@ -55,7 +55,7 @@ def function_add_or_update(
55
55
  "code": code,
56
56
  "language": "python",
57
57
  "returnType": get_jsonschema_type(return_type),
58
- "arguments": [{**p, "key": p["name"], "type": get_jsonschema_type(p["type"]) } for p in parsed["types"]["params"]],
58
+ "arguments": [{**p, "key": p["name"], "type": get_jsonschema_type(p["type"])} for p in parsed["types"]["params"]],
59
59
  "logsEnabled": logs_enabled,
60
60
  }
61
61
 
@@ -88,7 +88,10 @@ def function_add_or_update(
88
88
  print(f"Function ID: {function_id}")
89
89
  if generate:
90
90
  print("Generating new custom function...", end="")
91
- functions = get_functions_and_parse(limit_ids=[function_id])
91
+ # TODO do something more efficient here rather than regetting ALL the specs again
92
+ specs = get_specs()
93
+ cache_specs(specs)
94
+ functions = parse_function_specs(specs)
92
95
  generate_functions(functions)
93
96
  print_green("DONE")
94
97
  else:
@@ -2,13 +2,15 @@ import json
2
2
  import requests
3
3
  import os
4
4
  import shutil
5
- from typing import List
5
+ from typing import List, cast
6
6
 
7
+ from polyapi import schema
7
8
  from polyapi.auth import render_auth_function
8
9
  from polyapi.client import render_client_function
10
+ from polyapi.poly_schemas import generate_schemas
9
11
  from polyapi.webhook import render_webhook_handle
10
12
 
11
- from .typedefs import PropertySpecification, SpecificationDto, VariableSpecDto
13
+ from .typedefs import PropertySpecification, SchemaSpecDto, SpecificationDto, VariableSpecDto
12
14
  from .api import render_api_function
13
15
  from .server import render_server_function
14
16
  from .utils import add_import_to_init, get_auth_headers, init_the_init, to_func_namespace
@@ -18,12 +20,21 @@ from .config import get_api_key_and_url
18
20
  SUPPORTED_FUNCTION_TYPES = {
19
21
  "apiFunction",
20
22
  "authFunction",
21
- "customFunction",
23
+ "customFunction", # client function - this is badly named in /specs atm
22
24
  "serverFunction",
23
25
  "webhookHandle",
24
26
  }
25
27
 
26
- SUPPORTED_TYPES = SUPPORTED_FUNCTION_TYPES | {"serverVariable"}
28
+ SUPPORTED_TYPES = SUPPORTED_FUNCTION_TYPES | {"serverVariable", "schema", "snippet"}
29
+
30
+
31
+ X_POLY_REF_WARNING = '''"""
32
+ x-poly-ref:
33
+ path:'''
34
+
35
+ X_POLY_REF_BETTER_WARNING = '''"""
36
+ Unresolved schema, please add the following schema to complete it:
37
+ path:'''
27
38
 
28
39
 
29
40
  def get_specs() -> List:
@@ -38,9 +49,56 @@ def get_specs() -> List:
38
49
  raise NotImplementedError(resp.content)
39
50
 
40
51
 
52
+ def build_schema_index(items):
53
+ index = {}
54
+ for item in items:
55
+ if item.get("type") == "schema" and "contextName" in item:
56
+ index[item["contextName"]] = {**item.get("definition", {}), "name": item.get("name")}
57
+ return index
58
+
59
+
60
+ def resolve_poly_refs(obj, schema_index):
61
+ if isinstance(obj, dict):
62
+ if "x-poly-ref" in obj:
63
+ ref = obj["x-poly-ref"]
64
+ if isinstance(ref, dict) and "path" in ref:
65
+ path = ref["path"]
66
+ if path in schema_index:
67
+ return resolve_poly_refs(schema_index[path], schema_index)
68
+ else:
69
+ return obj
70
+ return {k: resolve_poly_refs(v, schema_index) for k, v in obj.items()}
71
+ elif isinstance(obj, list):
72
+ return [resolve_poly_refs(item, schema_index) for item in obj]
73
+ else:
74
+ return obj
75
+
76
+
77
+ def replace_poly_refs_in_functions(specs: List[SpecificationDto], schema_index):
78
+ spec_idxs_to_remove = []
79
+ for idx, spec in enumerate(specs):
80
+ if spec.get("type") in ("apiFunction", "customFunction", "serverFunction"):
81
+ func = spec.get("function")
82
+ if func:
83
+ try:
84
+ spec["function"] = resolve_poly_refs(func, schema_index)
85
+ except Exception:
86
+ print()
87
+ print(f"{spec['context']}.{spec['name']} (id: {spec['id']}) failed to resolve poly refs, skipping!")
88
+ spec_idxs_to_remove.append(idx)
89
+
90
+ # reverse the list so we pop off later indexes first
91
+ spec_idxs_to_remove.reverse()
92
+
93
+ for idx in spec_idxs_to_remove:
94
+ specs.pop(idx)
95
+
96
+ return specs
97
+
98
+
41
99
  def parse_function_specs(
42
100
  specs: List[SpecificationDto],
43
- limit_ids: List[str] | None, # optional list of ids to limit to
101
+ limit_ids: List[str] | None = None, # optional list of ids to limit to
44
102
  ) -> List[SpecificationDto]:
45
103
  functions = []
46
104
  for spec in specs:
@@ -91,23 +149,14 @@ def read_cached_specs() -> List[SpecificationDto]:
91
149
  return json.loads(f.read())
92
150
 
93
151
 
94
- def get_functions_and_parse(limit_ids: List[str] | None = None) -> List[SpecificationDto]:
95
- specs = get_specs()
96
- cache_specs(specs)
97
- return parse_function_specs(specs, limit_ids=limit_ids)
152
+ def get_variables() -> List[VariableSpecDto]:
153
+ specs = read_cached_specs()
154
+ return [cast(VariableSpecDto, spec) for spec in specs if spec["type"] == "serverVariable"]
98
155
 
99
156
 
100
- def get_variables() -> List[VariableSpecDto]:
101
- api_key, api_url = get_api_key_and_url()
102
- headers = {"Authorization": f"Bearer {api_key}"}
103
- # TODO do some caching so this and get_functions just do 1 function call
104
- url = f"{api_url}/specs"
105
- resp = requests.get(url, headers=headers)
106
- if resp.status_code == 200:
107
- specs = resp.json()
108
- return [spec for spec in specs if spec["type"] == "serverVariable"]
109
- else:
110
- raise NotImplementedError(resp.content)
157
+ def get_schemas() -> List[SchemaSpecDto]:
158
+ specs = read_cached_specs()
159
+ return [cast(SchemaSpecDto, spec) for spec in specs if spec["type"] == "schema"]
111
160
 
112
161
 
113
162
  def remove_old_library():
@@ -120,12 +169,28 @@ def remove_old_library():
120
169
  if os.path.exists(path):
121
170
  shutil.rmtree(path)
122
171
 
172
+ path = os.path.join(currdir, "schemas")
173
+ if os.path.exists(path):
174
+ shutil.rmtree(path)
175
+
123
176
 
124
177
  def generate() -> None:
125
178
 
126
179
  remove_old_library()
127
180
 
128
- functions = get_functions_and_parse()
181
+ limit_ids: List[str] = [] # useful for narrowing down generation to a single function to debug
182
+
183
+ specs = get_specs()
184
+ cache_specs(specs)
185
+ functions = parse_function_specs(specs, limit_ids=limit_ids)
186
+
187
+ schemas = get_schemas()
188
+ if schemas:
189
+ generate_schemas(schemas)
190
+
191
+ schema_index = build_schema_index(schemas)
192
+ functions = replace_poly_refs_in_functions(functions, schema_index)
193
+
129
194
  if functions:
130
195
  generate_functions(functions)
131
196
  else:
@@ -138,6 +203,7 @@ def generate() -> None:
138
203
  if variables:
139
204
  generate_variables(variables)
140
205
 
206
+
141
207
  # indicator to vscode extension that this is a polyapi-python project
142
208
  file_path = os.path.join(os.getcwd(), ".polyapi-python")
143
209
  open(file_path, "w").close()
@@ -214,6 +280,12 @@ def render_spec(spec: SpecificationDto):
214
280
  arguments,
215
281
  return_type,
216
282
  )
283
+
284
+ if X_POLY_REF_WARNING in func_type_defs:
285
+ # this indicates that jsonschema_gentypes has detected an x-poly-ref
286
+ # let's add a more user friendly error explaining what is going on
287
+ func_type_defs = func_type_defs.replace(X_POLY_REF_WARNING, X_POLY_REF_BETTER_WARNING)
288
+
217
289
  return func_str, func_type_defs
218
290
 
219
291
 
@@ -47,7 +47,7 @@ def _parse_sphinx_docstring(docstring: str) -> Dict[str, Any]:
47
47
  "type": "Any"
48
48
  }
49
49
  current_section = None
50
-
50
+
51
51
  for line in lines:
52
52
  stripped_line = line.strip()
53
53
  if stripped_line.startswith(":param "):
@@ -56,7 +56,7 @@ def _parse_sphinx_docstring(docstring: str) -> Dict[str, Any]:
56
56
  param_name = param_name.strip()
57
57
  if param_name in params:
58
58
  params[param_name]["description"] = param_desc.strip()
59
- else:
59
+ else:
60
60
  params[param_name] = { "name": param_name, "type": "", "description": param_desc.strip() }
61
61
  current_section = param_name
62
62
 
@@ -118,7 +118,7 @@ def _parse_google_docstring(docstring: str) -> Dict[str, Any]:
118
118
  for line in lines:
119
119
  line = line.rstrip()
120
120
  section_match = section_pattern.match(line)
121
-
121
+
122
122
  if section_match:
123
123
  mode = section_match.group(1).lower()
124
124
  continue
@@ -365,7 +365,7 @@ def parse_function_code(code: str, name: Optional[str] = "", context: Optional[s
365
365
  self._line_offsets.append(
366
366
  self._line_offsets[i-1] + len(self._lines[i-1])
367
367
  )
368
-
368
+
369
369
  self._extract_deploy_comments()
370
370
 
371
371
  def visit_AnnAssign(self, node):
@@ -483,7 +483,7 @@ def parse_function_code(code: str, name: Optional[str] = "", context: Optional[s
483
483
  if docstring_params[type_index]["type"] != python_type:
484
484
  deployable["dirty"] = True
485
485
  except:
486
- pass
486
+ pass
487
487
  else:
488
488
  deployable["dirty"] = True
489
489
 
@@ -497,7 +497,7 @@ def parse_function_code(code: str, name: Optional[str] = "", context: Optional[s
497
497
  deployable["types"]["returns"]["typeSchema"] = return_type_schema
498
498
  else:
499
499
  deployable["types"]["returns"]["type"] = "Any"
500
-
500
+
501
501
  def generic_visit(self, node):
502
502
  if hasattr(node, 'lineno') and hasattr(node, 'col_offset'):
503
503
  self._current_offset = self._line_offsets[node.lineno - 1] + node.col_offset
@@ -0,0 +1,60 @@
1
+ import os
2
+ from typing import Any, Dict, List, Tuple
3
+
4
+ from polyapi.schema import wrapped_generate_schema_types
5
+ from polyapi.utils import add_import_to_init, init_the_init
6
+
7
+ from .typedefs import SchemaSpecDto
8
+
9
+ SCHEMA_CODE_IMPORTS = """from typing_extensions import TypedDict, NotRequired
10
+
11
+
12
+ """
13
+
14
+
15
+ FALLBACK_SPEC_TEMPLATE = """class {name}(TypedDict, total=False):
16
+ ''' unable to generate schema for {name}, defaulting to permissive type '''
17
+ pass
18
+ """
19
+
20
+
21
+ def generate_schemas(specs: List[SchemaSpecDto]):
22
+ for spec in specs:
23
+ create_schema(spec)
24
+
25
+
26
+ def create_schema(spec: SchemaSpecDto) -> None:
27
+ folders = ["schemas"]
28
+ if spec["context"]:
29
+ folders += [s for s in spec["context"].split(".")]
30
+
31
+ # build up the full_path by adding all the folders
32
+ full_path = os.path.join(os.path.dirname(os.path.abspath(__file__)))
33
+
34
+ for idx, folder in enumerate(folders):
35
+ full_path = os.path.join(full_path, folder)
36
+ if not os.path.exists(full_path):
37
+ os.makedirs(full_path)
38
+ next = folders[idx + 1] if idx + 1 < len(folders) else None
39
+ if next:
40
+ add_import_to_init(full_path, next, code_imports=SCHEMA_CODE_IMPORTS)
41
+
42
+ add_schema_to_init(full_path, spec)
43
+
44
+
45
+ def add_schema_to_init(full_path: str, spec: SchemaSpecDto):
46
+ init_the_init(full_path, code_imports="")
47
+ init_path = os.path.join(full_path, "__init__.py")
48
+ with open(init_path, "a") as f:
49
+ f.write(render_poly_schema(spec) + "\n\n")
50
+
51
+
52
+ def render_poly_schema(spec: SchemaSpecDto) -> str:
53
+ definition = spec["definition"]
54
+ if not definition.get("type"):
55
+ definition["type"] = "object"
56
+ root, schema_types = wrapped_generate_schema_types(
57
+ definition, root=spec["name"], fallback_type=Dict
58
+ )
59
+ return schema_types
60
+ # return FALLBACK_SPEC_TEMPLATE.format(name=spec["name"])
@@ -1,11 +1,18 @@
1
+ """ NOTE: this file represents the schema parsing logic for jsonschema_gentypes
2
+ """
3
+ import random
4
+ import string
1
5
  import logging
2
6
  import contextlib
3
7
  from typing import Dict
4
8
  from jsonschema_gentypes.cli import process_config
5
9
  from jsonschema_gentypes import configuration
10
+ import referencing
6
11
  import tempfile
7
12
  import json
8
13
 
14
+ import referencing.exceptions
15
+
9
16
  from polyapi.constants import JSONSCHEMA_TO_PYTHON_TYPE_MAP
10
17
 
11
18
 
@@ -33,8 +40,18 @@ def _temp_store_input_data(input_data: Dict) -> str:
33
40
 
34
41
 
35
42
  def wrapped_generate_schema_types(type_spec: dict, root, fallback_type):
43
+ from polyapi.utils import pascalCase
36
44
  if not root:
37
- root = "MyList" if fallback_type == "List" else "MyDict"
45
+ root = "List" if fallback_type == "List" else "Dict"
46
+ if type_spec.get("x-poly-ref") and type_spec["x-poly-ref"].get("path"):
47
+ # x-poly-ref occurs when we have an unresolved reference
48
+ # lets name the root after the reference for some level of visibility
49
+ root += pascalCase(type_spec["x-poly-ref"]["path"].replace(".", " "))
50
+ else:
51
+ # add three random letters for uniqueness
52
+ root += random.choice(string.ascii_letters).upper()
53
+ root += random.choice(string.ascii_letters).upper()
54
+ root += random.choice(string.ascii_letters).upper()
38
55
 
39
56
  root = clean_title(root)
40
57
 
@@ -44,8 +61,13 @@ def wrapped_generate_schema_types(type_spec: dict, root, fallback_type):
44
61
  # some schemas are so huge, our library cant handle it
45
62
  # TODO identify critical recursion penalty and maybe switch underlying logic to iterative?
46
63
  return fallback_type, ""
64
+ except referencing.exceptions.CannotDetermineSpecification:
65
+ # just go with fallback_type here
66
+ # we couldn't match the right $ref earlier in resolve_poly_refs
67
+ # {'$ref': '#/definitions/FinanceAccountListModel'}
68
+ return fallback_type, ""
47
69
  except:
48
- logging.exception(f"Error when generating schema type: {type_spec}")
70
+ logging.error(f"Error when generating schema type: {type_spec}\nusing fallback type '{fallback_type}'")
49
71
  return fallback_type, ""
50
72
 
51
73
 
@@ -55,6 +55,19 @@ class VariableSpecDto(TypedDict):
55
55
  variable: VariableSpecification
56
56
  type: Literal['serverVariable']
57
57
 
58
+
59
+ class SchemaSpecDto(TypedDict):
60
+ id: str
61
+ context: str
62
+ name: str
63
+ contextName: str
64
+ type: Literal['schema']
65
+ definition: Dict[Any, Any]
66
+ visibilityMetadata: object
67
+ unresolvedPolySchemaRefs: List
68
+ # TODO add more
69
+
70
+
58
71
  Visibility = Union[Literal['PUBLIC'], Literal['TENANT'], Literal['ENVIRONMENT']]
59
72
 
60
73
 
@@ -69,6 +82,7 @@ class PolyServerFunction(PolyDeployable):
69
82
  always_on: NotRequired[bool]
70
83
  visibility: NotRequired[Visibility]
71
84
 
85
+
72
86
  class PolyClientFunction(PolyDeployable):
73
87
  logs_enabled: NotRequired[bool]
74
88
  visibility: NotRequired[Visibility]
@@ -11,19 +11,19 @@ from polyapi.schema import wrapped_generate_schema_types, clean_title, map_primi
11
11
 
12
12
  # this string should be in every __init__ file.
13
13
  # it contains all the imports needed for the function or variable code to run
14
- CODE_IMPORTS = "from typing import List, Dict, Any, TypedDict, Optional, Callable\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"
15
- FALLBACK_TYPES = {"Dict", "List"}
14
+ 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"
16
15
 
17
16
 
18
- def init_the_init(full_path: str) -> None:
17
+ def init_the_init(full_path: str, code_imports="") -> None:
19
18
  init_path = os.path.join(full_path, "__init__.py")
20
19
  if not os.path.exists(init_path):
20
+ code_imports = code_imports or CODE_IMPORTS
21
21
  with open(init_path, "w") as f:
22
- f.write(CODE_IMPORTS)
22
+ f.write(code_imports)
23
23
 
24
24
 
25
- def add_import_to_init(full_path: str, next: str) -> None:
26
- init_the_init(full_path)
25
+ def add_import_to_init(full_path: str, next: str, code_imports="") -> None:
26
+ init_the_init(full_path, code_imports=code_imports)
27
27
 
28
28
  init_path = os.path.join(full_path, "__init__.py")
29
29
  with open(init_path, "a+") as f:
@@ -38,7 +38,7 @@ def get_auth_headers(api_key: str):
38
38
  return {"Authorization": f"Bearer {api_key}"}
39
39
 
40
40
 
41
- def camelCase(s):
41
+ def camelCase(s: str) -> str:
42
42
  s = s.strip()
43
43
  if " " in s or "-" in s:
44
44
  s = re.sub(r"(_|-)+", " ", s).title().replace(" ", "")
@@ -48,6 +48,10 @@ def camelCase(s):
48
48
  return s
49
49
 
50
50
 
51
+ def pascalCase(s) -> str:
52
+ return re.sub(r"(^|_)([a-z])", lambda match: match.group(2).upper(), s)
53
+
54
+
51
55
  def print_green(s: str):
52
56
  print(Fore.GREEN + s + Style.RESET_ALL)
53
57
 
@@ -111,15 +115,20 @@ def get_type_and_def(type_spec: PropertyType) -> Tuple[str, str]:
111
115
  elif type_spec["kind"] == "object":
112
116
  if type_spec.get("schema"):
113
117
  schema = type_spec["schema"]
114
- title = schema.get("title", "")
118
+ title = schema.get("title", schema.get("name", ""))
115
119
  if title:
116
120
  assert isinstance(title, str)
117
121
  return wrapped_generate_schema_types(schema, title, "Dict") # type: ignore
118
-
122
+ elif schema.get("allOf") and len(schema['allOf']):
123
+ # we are in a case of a single allOf, lets strip off the allOf and move on!
124
+ # our library doesn't handle allOf well yet
125
+ allOf = schema['allOf'][0]
126
+ title = allOf.get("title", allOf.get("name", ""))
127
+ return wrapped_generate_schema_types(allOf, title, "Dict")
119
128
  elif schema.get("items"):
120
129
  # fallback to schema $ref name if no explicit title
121
130
  items = schema.get("items") # type: ignore
122
- title = items.get("title", "") # type: ignore
131
+ title = items.get("title") # type: ignore
123
132
  if not title:
124
133
  # title is actually a reference to another schema
125
134
  title = items.get("$ref", "") # type: ignore
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: polyapi-python
3
- Version: 0.3.2.dev2
3
+ Version: 0.3.3.dev2
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
@@ -28,14 +28,15 @@ Project-URL: Homepage, https://github.com/polyapi/polyapi-python
28
28
  Requires-Python: >=3.10
29
29
  Description-Content-Type: text/markdown
30
30
  License-File: LICENSE
31
- Requires-Dist: requests==2.31.0
32
- Requires-Dist: typing_extensions>=4.10.0
31
+ Requires-Dist: requests>=2.32.3
32
+ Requires-Dist: typing_extensions>=4.12.2
33
33
  Requires-Dist: jsonschema-gentypes==2.6.0
34
34
  Requires-Dist: pydantic==2.6.4
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
38
38
  Requires-Dist: truststore==0.8.0
39
+ Dynamic: license-file
39
40
 
40
41
  # PolyAPI Python Library
41
42
 
@@ -182,6 +183,16 @@ To run this library's unit tests, please clone the repo then run:
182
183
  python -m unittest discover
183
184
  ```
184
185
 
186
+ ## Linting
187
+
188
+ The flake8 config is at the root of this repo at `.flake8`.
189
+
190
+ When hacking on this library, please enable flake8 and add this line to your flake8 args (e.g., in your VSCode Workspace Settings):
191
+
192
+ ```
193
+ --config=.flake8
194
+ ```
195
+
185
196
  ## Support
186
197
 
187
198
  If you run into any issues or want help getting started with this project, please contact support@polyapi.io
@@ -16,6 +16,7 @@ polyapi/execute.py
16
16
  polyapi/function_cli.py
17
17
  polyapi/generate.py
18
18
  polyapi/parser.py
19
+ polyapi/poly_schemas.py
19
20
  polyapi/prepare.py
20
21
  polyapi/py.typed
21
22
  polyapi/rendered_spec.py
@@ -34,6 +35,7 @@ polyapi_python.egg-info/top_level.txt
34
35
  tests/test_api.py
35
36
  tests/test_auth.py
36
37
  tests/test_deployables.py
38
+ tests/test_generate.py
37
39
  tests/test_parser.py
38
40
  tests/test_rendered_spec.py
39
41
  tests/test_schema.py
@@ -1,5 +1,5 @@
1
- requests==2.31.0
2
- typing_extensions>=4.10.0
1
+ requests>=2.32.3
2
+ typing_extensions>=4.12.2
3
3
  jsonschema-gentypes==2.6.0
4
4
  pydantic==2.6.4
5
5
  stdlib_list==0.10.0
@@ -3,12 +3,12 @@ requires = ["setuptools>=61.2", "wheel"]
3
3
 
4
4
  [project]
5
5
  name = "polyapi-python"
6
- version = "0.3.2.dev2"
6
+ version = "0.3.3.dev2"
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 = [
10
- "requests==2.31.0",
11
- "typing_extensions>=4.10.0",
10
+ "requests>=2.32.3",
11
+ "typing_extensions>=4.12.2",
12
12
  "jsonschema-gentypes==2.6.0",
13
13
  "pydantic==2.6.4",
14
14
  "stdlib_list==0.10.0",
@@ -21,7 +21,6 @@ def foobar() -> int:
21
21
 
22
22
  EXPECTED_SERVER_FN_DEPLOYMENTS = '''# Poly deployed @ 2024-11-12T14:43:22.631113 - testing.foobar - https://na1.polyapi.io/canopy/polyui/collections/server-functions/jh23h5g3h5b24jh5b2j3h45v2jhg43v52j3h - 086aedd
23
23
  # Poly deployed @ 2024-11-11T14:43:22.631113 - testing.foobar - https://dev.polyapi.io/canopy/polyui/collections/server-functions/jh23h5g3h5b24jh5b2j3h45v2jhg43v52j3h - 086aedd
24
-
25
24
  from polyapi.typedefs import PolyServerFunction
26
25
 
27
26
  polyConfig: PolyServerFunction = {
@@ -66,11 +65,11 @@ def foobar(foo: str, bar: Dict[str, str]) -> int:
66
65
  """A function that does something really import.
67
66
 
68
67
  Args:
69
- foo (str):
70
- bar (Dict[str, str]):
68
+ foo (str):
69
+ bar (Dict[str, str]):
71
70
 
72
71
  Returns:
73
- int:
72
+ int:
74
73
  """
75
74
  print("Okay then!")
76
75
  return 7
@@ -186,7 +186,7 @@ class T(unittest.TestCase):
186
186
  code = "import requests\n\n\ndef foobar(n: int) -> int:\n return 9\n"
187
187
  deployable = parse_function_code(code, "foobar")
188
188
  self.assertEqual(deployable["dependencies"], [])
189
-
189
+
190
190
  def test_parse_glide_server_function_no_docstring(self):
191
191
  code = GLIDE_SIMPLE_SERVER_FN
192
192
  deployable = parse_function_code(code, "foobar")
@@ -0,0 +1,14 @@
1
+ import unittest
2
+ from polyapi.utils import get_type_and_def, rewrite_reserved
3
+
4
+ OPENAPI_FUNCTION = {'kind': 'function', 'spec': {'arguments': [{'name': 'event', 'required': False, 'type': {'kind': 'object', 'schema': {'$schema': 'http://json-schema.org/draft-06/schema#', 'type': 'array', 'items': {'$ref': '#/definitions/WebhookEventTypeElement'}, 'definitions': {'WebhookEventTypeElement': {'type': 'object', 'additionalProperties': False, 'properties': {'title': {'type': 'string'}, 'manufacturerName': {'type': 'string'}, 'carType': {'type': 'string'}, 'id': {'type': 'integer'}}, 'required': ['carType', 'id', 'manufacturerName', 'title'], 'title': 'WebhookEventTypeElement'}}}}}, {'name': 'headers', 'required': False, 'type': {'kind': 'object', 'typeName': 'Record<string, any>'}}, {'name': 'params', 'required': False, 'type': {'kind': 'object', 'typeName': 'Record<string, any>'}}, {'name': 'polyCustom', 'required': False, 'type': {'kind': 'object', 'properties': [{'name': 'responseStatusCode', 'type': {'type': 'number', 'kind': 'primitive'}, 'required': True}, {'name': 'responseContentType', 'type': {'type': 'string', 'kind': 'primitive'}, 'required': True, 'nullable': True}]}}], 'returnType': {'kind': 'void'}, 'synchronous': True}}
5
+
6
+
7
+ class T(unittest.TestCase):
8
+ def test_get_type_and_def(self):
9
+ arg_type, arg_def = get_type_and_def(OPENAPI_FUNCTION)
10
+ self.assertEqual(arg_type, "Callable[[List[WebhookEventTypeElement], Dict, Dict, Dict], None]")
11
+
12
+ def test_rewrite_reserved(self):
13
+ rv = rewrite_reserved("from")
14
+ self.assertEqual(rv, "_from")