polyapi-python 0.3.1.dev1__tar.gz → 0.3.1.dev3__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 (47) hide show
  1. {polyapi_python-0.3.1.dev1/polyapi_python.egg-info → polyapi_python-0.3.1.dev3}/PKG-INFO +1 -1
  2. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/polyapi/__init__.py +4 -4
  3. polyapi_python-0.3.1.dev3/polyapi/cli.py +186 -0
  4. polyapi_python-0.3.1.dev3/polyapi/deployables.py +296 -0
  5. polyapi_python-0.3.1.dev3/polyapi/function_cli.py +123 -0
  6. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/polyapi/generate.py +1 -2
  7. polyapi_python-0.3.1.dev3/polyapi/parser.py +519 -0
  8. polyapi_python-0.3.1.dev3/polyapi/prepare.py +135 -0
  9. polyapi_python-0.3.1.dev3/polyapi/sync.py +122 -0
  10. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/polyapi/typedefs.py +21 -3
  11. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3/polyapi_python.egg-info}/PKG-INFO +1 -1
  12. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/polyapi_python.egg-info/SOURCES.txt +6 -1
  13. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/pyproject.toml +1 -1
  14. polyapi_python-0.3.1.dev3/tests/test_deployables.py +114 -0
  15. polyapi_python-0.3.1.dev3/tests/test_parser.py +243 -0
  16. polyapi_python-0.3.1.dev1/polyapi/cli.py +0 -73
  17. polyapi_python-0.3.1.dev1/polyapi/function_cli.py +0 -314
  18. polyapi_python-0.3.1.dev1/tests/test_function_cli.py +0 -92
  19. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/LICENSE +0 -0
  20. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/README.md +0 -0
  21. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/polyapi/__main__.py +0 -0
  22. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/polyapi/api.py +0 -0
  23. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/polyapi/auth.py +0 -0
  24. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/polyapi/client.py +0 -0
  25. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/polyapi/config.py +0 -0
  26. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/polyapi/constants.py +0 -0
  27. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/polyapi/error_handler.py +0 -0
  28. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/polyapi/exceptions.py +0 -0
  29. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/polyapi/execute.py +0 -0
  30. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/polyapi/py.typed +0 -0
  31. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/polyapi/rendered_spec.py +0 -0
  32. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/polyapi/schema.py +0 -0
  33. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/polyapi/server.py +0 -0
  34. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/polyapi/utils.py +0 -0
  35. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/polyapi/variables.py +0 -0
  36. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/polyapi/webhook.py +0 -0
  37. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/polyapi_python.egg-info/dependency_links.txt +0 -0
  38. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/polyapi_python.egg-info/requires.txt +0 -0
  39. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/polyapi_python.egg-info/top_level.txt +0 -0
  40. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/setup.cfg +0 -0
  41. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/tests/test_api.py +0 -0
  42. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/tests/test_auth.py +0 -0
  43. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/tests/test_rendered_spec.py +0 -0
  44. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/tests/test_schema.py +0 -0
  45. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/tests/test_server.py +0 -0
  46. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/tests/test_utils.py +0 -0
  47. {polyapi_python-0.3.1.dev1 → polyapi_python-0.3.1.dev3}/tests/test_variables.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: polyapi-python
3
- Version: 0.3.1.dev1
3
+ Version: 0.3.1.dev3
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
@@ -16,8 +16,8 @@ if len(sys.argv) > 1 and sys.argv[1] not in CLI_COMMANDS:
16
16
 
17
17
 
18
18
  polyCustom: Dict[str, Any] = {
19
- "executionId": None,
20
- "executionApiKey": None,
21
- "responseStatusCode": 200,
22
- "responseContentType": None,
19
+ "executionId": None,
20
+ "executionApiKey": None,
21
+ "responseStatusCode": 200,
22
+ "responseContentType": None,
23
23
  }
@@ -0,0 +1,186 @@
1
+ import os
2
+ import argparse
3
+
4
+ from polyapi.utils import print_green, print_red
5
+
6
+ from .config import initialize_config, set_api_key_and_url
7
+ from .generate import generate, clear
8
+ from .function_cli import function_add_or_update, function_execute
9
+ from .rendered_spec import get_and_update_rendered_spec
10
+ from .prepare import prepare_deployables
11
+ from .sync import sync_deployables
12
+
13
+
14
+ CLI_COMMANDS = ["setup", "generate", "function", "clear", "help", "update_rendered_spec"]
15
+
16
+ def execute_from_cli():
17
+ # First we setup all our argument parsing logic
18
+ # Then we parse the arguments (waaay at the bottom)
19
+ parser = argparse.ArgumentParser(
20
+ prog="python -m polyapi",
21
+ description="Manage your Poly API configurations and functions",
22
+ formatter_class=argparse.RawTextHelpFormatter
23
+ )
24
+
25
+ subparsers = parser.add_subparsers(help="Available commands")
26
+
27
+ ###########################################################################
28
+ # Setup command
29
+ setup_parser = subparsers.add_parser("setup", help="Setup your Poly connection")
30
+ setup_parser.add_argument("api_key", nargs="?", help="API key for Poly API")
31
+ setup_parser.add_argument("url", nargs="?", help="URL for the Poly API")
32
+
33
+ def setup(args):
34
+ if args.api_key and args.url:
35
+ set_api_key_and_url(args.url, args.api_key)
36
+ else:
37
+ initialize_config(force=True)
38
+ generate()
39
+
40
+ setup_parser.set_defaults(command=setup)
41
+
42
+
43
+ ###########################################################################
44
+ # Generate command
45
+ generate_parser = subparsers.add_parser("generate", help="Generates Poly library")
46
+
47
+ def generate_command(args):
48
+ initialize_config()
49
+ print("Generating Poly functions...", end="")
50
+ generate()
51
+ print_green("DONE")
52
+
53
+ generate_parser.set_defaults(command=generate_command)
54
+
55
+
56
+ ###########################################################################
57
+ # Function commands
58
+ fn_parser = subparsers.add_parser("function", help="Manage and execute functions")
59
+ fn_subparsers = fn_parser.add_subparsers(help="Available commands")
60
+
61
+ # Function - Add command
62
+ fn_add_parser = fn_subparsers.add_parser("add", help="Add or update the function")
63
+ fn_add_parser.add_argument("name", help="Name of the function")
64
+ fn_add_parser.add_argument("file", help="Path to the function file")
65
+ fn_add_parser.add_argument("--context", required=False, default="", help="Context of the function")
66
+ fn_add_parser.add_argument("--description", required=False, default="", help="Description of the function")
67
+ fn_add_parser.add_argument("--server", action="store_true", help="Marks the function as a server function")
68
+ fn_add_parser.add_argument("--client", action="store_true", help="Marks the function as a client function")
69
+ fn_add_parser.add_argument("--logs", choices=["enabled", "disabled"], default="disabled", help="Enable or disable logs for the function.")
70
+ fn_add_parser.add_argument("--execution-api-key", required=False, default="", help="API key for execution (for server functions only).")
71
+ fn_add_parser.add_argument("--disable-ai", "--skip-generate", action="store_true", help="Pass --disable-ai skip AI generation of missing descriptions")
72
+
73
+ def add_function(args):
74
+ initialize_config()
75
+ logs_enabled = args.logs == "enabled"
76
+ err = ""
77
+ if args.server and args.client:
78
+ err = "Specify either `--server` or `--client`. Found both."
79
+ elif not args.server and not args.client:
80
+ err = "You must specify `--server` or `--client`."
81
+ elif logs_enabled and not args.server:
82
+ err = "Option `logs` is only for server functions (--server)."
83
+
84
+ if err:
85
+ print_red("ERROR")
86
+ print(err)
87
+ exit(1)
88
+
89
+ function_add_or_update(
90
+ context=args.context,
91
+ description=args.description,
92
+ client=args.client,
93
+ server=args.server,
94
+ logs_enabled=logs_enabled,
95
+ generate=not args.disable_ai,
96
+ execution_api_key=args.execution_api_key
97
+ )
98
+
99
+ fn_add_parser.set_defaults(command=add_function)
100
+
101
+
102
+ # Function - Execute command
103
+ fn_exec_parser = fn_subparsers.add_parser("execute", help="Execute a function with the provided arguments")
104
+ fn_exec_parser.add_argument("name", help="Name of the function")
105
+ fn_exec_parser.add_argument("args", nargs="*", help="Arguments for the function")
106
+ fn_exec_parser.add_argument("--context", required=False, default="", help="Context of the function")
107
+
108
+ def execute_function(args):
109
+ initialize_config()
110
+ print(function_execute(args.context, args.name, args.args))
111
+
112
+ fn_exec_parser.set_defaults(command=execute_function)
113
+
114
+
115
+ ###########################################################################
116
+ # Clear command
117
+ clear_parser = subparsers.add_parser("clear", help="Clear current generated Poly library")
118
+
119
+ def clear_command(_):
120
+ print("Clearing the generated library...")
121
+ clear()
122
+
123
+ clear_parser.set_defaults(command=clear_command)
124
+
125
+
126
+ ###########################################################################
127
+ # Update rendered spec command
128
+ update_spec_parser = subparsers.add_parser("update_rendered_spec", help="Update the rendered spec file")
129
+ update_spec_parser.add_argument("spec", help="Specification file to update")
130
+
131
+ def update_rendered_spec(args):
132
+ updated = get_and_update_rendered_spec(args.spec)
133
+ if updated:
134
+ print("Updated rendered spec!")
135
+ else:
136
+ print("Failed to update rendered spec!")
137
+ exit(1)
138
+
139
+ update_spec_parser.set_defaults(command=update_rendered_spec)
140
+
141
+
142
+ ###########################################################################
143
+ # Prepare command
144
+ prepare_parser = subparsers.add_parser('prepare', help="Find and prepare all Poly deployables")
145
+ prepare_parser.add_argument("--lazy", action="store_true", help="Skip prepare work if the cache is up to date. (Relies on `git`)")
146
+ prepare_parser.add_argument("--disable-docs", action="store_true", help="Don't write any docstrings into the deployable files.")
147
+ prepare_parser.add_argument("--disable-ai", action="store_true", help="Don't use AI to fill in any missing descriptions.")
148
+
149
+ def prepare(args):
150
+ initialize_config()
151
+ disable_ai = args.disable_ai or bool(os.getenv("DISABLE_AI"))
152
+ prepare_deployables(lazy=args.lazy, disable_docs=args.disable_docs, disable_ai=disable_ai)
153
+
154
+ prepare_parser.set_defaults(command=prepare)
155
+
156
+
157
+ ###########################################################################
158
+ # Sync command
159
+ sync_parser = subparsers.add_parser("sync", help="Find and sync all Poly deployables")
160
+ sync_parser.add_argument("--dry-run", action="store_true", help="Run through sync steps with logging but don't make any changes.")
161
+
162
+ def sync(args):
163
+ initialize_config()
164
+ prepare_deployables(lazy=True, disable_docs=True, disable_ai=True)
165
+ if args.dry_run:
166
+ print("Running dry-run of sync...")
167
+ sync_deployables(dry_run=args.dry_run)
168
+ print("Poly deployments synced.")
169
+
170
+ sync_parser.set_defaults(command=sync)
171
+
172
+ ###########################################################################
173
+ # _------. #
174
+ # / , \_ __ __ _ ________ #
175
+ # / / /{}\ |o\_ / / ___ / /( )_____ / ____/ /_ __ #
176
+ # / \ `--' /-' \ / / / _ \/ __/// ___/ / /_ / / / / / #
177
+ # | \ \ | / /___/ __/ /_ (__ ) / __/ / / /_/ / #
178
+ # | |`-, | /_____/\___/\__/ /____/ /_/ /_/\__, / #
179
+ # / /__/)/ /____/ #
180
+ # / | #
181
+ ###########################################################################
182
+ parsed_args = parser.parse_args()
183
+ if hasattr(parsed_args, "command"):
184
+ parsed_args.command(parsed_args)
185
+ else:
186
+ parser.print_help()
@@ -0,0 +1,296 @@
1
+ import os
2
+ import subprocess
3
+ import json
4
+ import hashlib
5
+ from pathlib import Path
6
+ from typing import TypedDict, List, Dict, Tuple, Optional, Any, Union
7
+ from subprocess import check_output, CalledProcessError
8
+
9
+
10
+ # Constants
11
+ CACHE_VERSION_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "poly/deployments_revision")
12
+ CACHE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "poly/deployables")
13
+
14
+
15
+ class DeployableTypes(str):
16
+ pass
17
+
18
+ class DeployableTypeNames(str):
19
+ pass
20
+
21
+ class Deployment(TypedDict):
22
+ context: str
23
+ name: str
24
+ type: DeployableTypes
25
+ instance: str
26
+ id: str
27
+ deployed: str
28
+ fileRevision: str
29
+
30
+ class ParsedDeployableConfig(TypedDict):
31
+ context: str
32
+ name: str
33
+ type: DeployableTypes
34
+ disableAi: Optional[bool]
35
+ config: Dict[str, Any]
36
+
37
+ class DeployableFunctionParamBase(TypedDict):
38
+ type: str
39
+ typeSchema: Optional[Dict[str, Any]]
40
+ description: str
41
+
42
+ class DeployableFunctionParam(DeployableFunctionParamBase):
43
+ name: str
44
+
45
+ class DeployableFunctionTypes(TypedDict):
46
+ description: str
47
+ params: List[DeployableFunctionParam]
48
+ returns: DeployableFunctionParamBase
49
+
50
+ class DeployableRecord(ParsedDeployableConfig, total=False):
51
+ gitRevision: str
52
+ fileRevision: str
53
+ file: str
54
+ types: DeployableFunctionTypes
55
+ typeSchemas: Dict[str, Any]
56
+ dependencies: List[str]
57
+ deployments: List[Deployment]
58
+ deploymentCommentRanges: List[Tuple[int, int]]
59
+ docStartIndex: int
60
+ docEndIndex: int
61
+ dirty: Optional[bool]
62
+
63
+ class SyncDeployment(TypedDict, total=False):
64
+ context: str
65
+ name: str
66
+ description: str
67
+ type: str # This should be an enumeration or a predefined set of strings if you have known types.
68
+ fileRevision: str
69
+ file: str
70
+ typeSchemas: Dict[str, any]
71
+ dependencies: List[str]
72
+ config: Dict[str, any]
73
+ instance: str
74
+ id: Optional[str] = None
75
+ deployed: Optional[str] = None
76
+
77
+ DeployableTypeEntries: List[Tuple[DeployableTypeNames, DeployableTypes]] = [
78
+ ("PolyServerFunction", "server-function"),
79
+ ("PolyClientFunction", "client-function"),
80
+ ]
81
+
82
+ DeployableTypeToName: Dict[DeployableTypeNames, DeployableTypes] = {name: type for name, type in DeployableTypeEntries}
83
+
84
+ def prepare_deployable_directory() -> None:
85
+ Path(CACHE_DIR).mkdir(parents=True, exist_ok=True)
86
+
87
+ def load_deployable_records() -> List[DeployableRecord]:
88
+ return [read_json_file(os.path.join(CACHE_DIR, name)) for name in os.listdir(CACHE_DIR) if name.endswith(".json")]
89
+
90
+ def save_deployable_records(records: List[DeployableRecord]) -> None:
91
+ for record in records:
92
+ write_json_file(os.path.join(CACHE_DIR, f'{record["context"]}.{record["name"]}.json'), record)
93
+
94
+ def remove_deployable_records(records: List[DeployableRecord]) -> None:
95
+ for record in records:
96
+ os.remove(os.path.join(CACHE_DIR, f'{record["context"]}.{record["name"]}.json'))
97
+
98
+ def read_json_file(path: Union[str, Path]) -> Any:
99
+ with open(path, "r", encoding="utf-8") as file:
100
+ return json.load(file)
101
+
102
+ def write_json_file(path: Union[str, Path], contents: Any) -> None:
103
+ with open(path, "w", encoding="utf-8") as file:
104
+ json.dump(contents, file, indent=2)
105
+
106
+ class PolyDeployConfig(TypedDict):
107
+ type_names: List[str]
108
+ include_dirs: List[str]
109
+ include_files_or_extensions: List[str]
110
+ exclude_dirs: List[str]
111
+
112
+ def get_all_deployable_files_windows(config: PolyDeployConfig) -> List[str]:
113
+ # Constructing the Windows command using dir and findstr
114
+ include_pattern = " ".join(f"*.{f}" if "." in f else f"*.{f}" for f in config["include_files_or_extensions"]) or "*"
115
+ exclude_pattern = '|'.join(config["exclude_dirs"])
116
+ pattern = '|'.join(f"polyConfig: {name}" for name in config["type_names"]) or 'polyConfig'
117
+
118
+ exclude_command = f" | findstr /V /I \"{exclude_pattern}\"" if exclude_pattern else ''
119
+ search_command = f" | findstr /M /I /F:/ /C:\"{pattern}\""
120
+
121
+ result = []
122
+ for dir_path in config["include_dirs"]:
123
+ dir_command = f"dir /S /P /B {include_pattern} {dir_path}"
124
+ full_command = f"{dir_command}{exclude_command}{search_command}"
125
+ try:
126
+ output = subprocess.check_output(full_command, shell=True, text=True)
127
+ result.extend(output.strip().split('\r\n'))
128
+ except subprocess.CalledProcessError:
129
+ pass
130
+ return result
131
+
132
+ def get_all_deployable_files_linux(config: PolyDeployConfig) -> List[str]:
133
+ # Constructing the Linux grep command
134
+ include = " ".join(f'--include={f if "." in f else f"*.{f}"}' for f in config["include_files_or_extensions"])
135
+ exclude_dir = " ".join(f"--exclude-dir={dir}" for dir in config["exclude_dirs"])
136
+
137
+ search_path = " ".join(config["include_dirs"]) or "."
138
+ patterns = " ".join(f"-e 'polyConfig: {name}'" for name in config["type_names"]) or "-e 'polyConfig'"
139
+ grep_command = f'grep {include} {exclude_dir} -Rl {patterns} {search_path}'
140
+
141
+ try:
142
+ output = subprocess.check_output(grep_command, shell=True, text=True)
143
+ return output.strip().split('\n')
144
+ except subprocess.CalledProcessError:
145
+ return []
146
+
147
+ def get_all_deployable_files(config: PolyDeployConfig) -> List[str]:
148
+ # Setting default values if not provided
149
+ if not config.get("type_names"):
150
+ config["type_names"] = [entry[0] for entry in DeployableTypeEntries] # Assuming DeployableTypeEntries is defined elsewhere
151
+ if not config.get("include_dirs"):
152
+ config["include_dirs"] = ["."]
153
+ if not config.get("include_files_or_extensions"):
154
+ config["include_files_or_extensions"] = ["py"]
155
+ if not config.get("exclude_dirs"):
156
+ config["exclude_dirs"] = ["poly", "node_modules", "dist", "build", "output", ".vscode", ".poly", ".github", ".husky", ".yarn"]
157
+
158
+ is_windows = os.name == "nt"
159
+ if is_windows:
160
+ return get_all_deployable_files_windows(config)
161
+ else:
162
+ return get_all_deployable_files_linux(config)
163
+
164
+ def get_deployable_file_revision(file_contents: str) -> str:
165
+ # Remove leading single-line comments and hash the remaining contents
166
+ file_contents = "\n".join(line for line in file_contents.split("\n") if not line.strip().startswith("#"))
167
+ return hashlib.sha256(file_contents.encode('utf-8')).hexdigest()[:7]
168
+
169
+ def get_git_revision(branch_or_tag: str = "HEAD") -> str:
170
+ try:
171
+ return check_output(["git", "rev-parse", "--short", branch_or_tag], text=True).strip()
172
+ except CalledProcessError:
173
+ # Return a random 7-character hash as a fallback
174
+ return "".join(format(ord(c), 'x') for c in os.urandom(4))[:7]
175
+
176
+ def get_cache_deployments_revision() -> str:
177
+ """Retrieve the cache deployments revision from a file."""
178
+ try:
179
+ with open(CACHE_VERSION_FILE, 'r', encoding='utf-8') as file:
180
+ return file.read().strip()
181
+ except FileNotFoundError:
182
+ return ''
183
+
184
+ def write_cache_revision(git_revision: Optional[str] = None) -> None:
185
+ if git_revision is None:
186
+ git_revision = get_git_revision()
187
+ with open(CACHE_VERSION_FILE, 'w', encoding='utf-8') as file:
188
+ file.write(git_revision)
189
+
190
+ def is_cache_up_to_date() -> bool:
191
+ if not Path(CACHE_VERSION_FILE).exists():
192
+ return False
193
+ with open(CACHE_VERSION_FILE, 'r', encoding='utf-8') as file:
194
+ cached_revision = file.read().strip()
195
+ git_revision = get_git_revision()
196
+ return cached_revision == git_revision
197
+
198
+ def is_cache_up_to_date() -> bool:
199
+ """Check if the cached revision matches the current Git revision."""
200
+ cached_revision = get_cache_deployments_revision()
201
+ git_revision = get_git_revision() # This function needs to be defined or imported
202
+ return cached_revision == git_revision
203
+
204
+ def write_deploy_comments(deployments: List[Dict]) -> str:
205
+ """Generate a string of deployment comments for each deployment."""
206
+ canopy_path = 'polyui/collections' if 'localhost' in os.getenv('POLY_API_BASE_URL', '') else 'canopy/polyui/collections'
207
+ comments = []
208
+ for d in deployments:
209
+ instance_url = d['instance'].replace(':8000', ':3000') if d['instance'].endswith(':8000') else d['instance']
210
+ comment = f"# Poly deployed @ {d['deployed']} - {d['context']}.{d['name']} - {instance_url}/{canopy_path}/{d['type']}s/{d['id']} - {d['fileRevision']}"
211
+ comments.append(comment)
212
+ return '\n'.join(comments)
213
+
214
+ def print_docstring_function_comment(description: str, args: list, returns: dict) -> str:
215
+ docstring = f'"""{description}\n\n'
216
+ if args:
217
+ docstring += ' Args:\n'
218
+ for arg in args:
219
+ name = arg.get('name')
220
+ arg_type = arg.get('type', '')
221
+ desc = arg.get('description', '')
222
+ if arg_type:
223
+ docstring += f' {name} ({arg_type}): {desc}\n'
224
+ else:
225
+ docstring += f' {name}: {desc}\n'
226
+
227
+ return_type = returns.get('type', '')
228
+ return_description = returns.get('description', '')
229
+ if return_type:
230
+ docstring += f'\n Returns:\n {return_type}: {return_description}\n'
231
+ else:
232
+ docstring += f'\n Returns:\n {return_description}\n'
233
+
234
+ docstring += ' """'
235
+ return docstring
236
+
237
+
238
+ def update_deployment_comments(file_content: str, deployable: dict) -> str:
239
+ """
240
+ Remove old deployment comments based on the provided ranges and add new ones.
241
+ """
242
+ for range in reversed(deployable['deploymentCommentRanges']):
243
+ file_content = file_content[:range[0]] + file_content[range[1]:]
244
+ if deployable['deployments']:
245
+ deployment_comments = write_deploy_comments(deployable['deployments'])
246
+ deployable['deploymentCommentRanges'] = [(0, len(deployment_comments) + 1)]
247
+ file_content = f"{deployment_comments}\n{file_content}"
248
+ return file_content
249
+
250
+ def update_deployable_function_comments(file_content: str, deployable: dict, disable_docs: bool = False) -> str:
251
+ """
252
+ Update the docstring in the file content based on the deployable's documentation data.
253
+ """
254
+ if not disable_docs:
255
+ docstring = print_docstring_function_comment(
256
+ deployable['types']['description'],
257
+ deployable['types']['params'],
258
+ deployable['types']['returns']
259
+ )
260
+ if deployable["docStartIndex"] == deployable["docEndIndex"]:
261
+ # Function doesn't yet have any docstrings so we need to add additional whitespace
262
+ docstring = " " + docstring + "\n"
263
+
264
+ return f"{file_content[:deployable['docStartIndex']]}{docstring}{file_content[deployable['docEndIndex']:]}"
265
+ return file_content
266
+
267
+ def write_updated_deployable(deployable: dict, disable_docs: bool = False) -> dict:
268
+ """
269
+ Read the deployable's file, update its comments and docstring, and write back to the file.
270
+ """
271
+ with open(deployable['file'], 'r', encoding='utf-8') as file:
272
+ file_contents = file.read()
273
+
274
+ if deployable['type'] in ['client-function', 'server-function']:
275
+ file_contents = update_deployable_function_comments(file_contents, deployable, disable_docs)
276
+ else:
277
+ raise ValueError(f"Unsupported deployable type: '{deployable['type']}'")
278
+
279
+ file_contents = update_deployment_comments(file_contents, deployable)
280
+
281
+ with open(deployable['file'], 'w', encoding='utf-8') as file:
282
+ file.write(file_contents)
283
+
284
+ deployable['fileRevision'] = get_deployable_file_revision(file_contents)
285
+ return deployable
286
+
287
+ def write_deploy_comments(deployments: list) -> str:
288
+ """
289
+ Generate deployment comments for each deployment record.
290
+ """
291
+ canopy_path = 'polyui/collections' if 'localhost' in os.getenv('POLY_API_BASE_URL', '') else 'canopy/polyui/collections'
292
+ comments = []
293
+ for d in deployments:
294
+ instance_url = d['instance'].replace(':8000', ':3000') if d['instance'].endswith(':8000') else d['instance']
295
+ comments.append(f"# Poly deployed @ {d['deployed']} - {d['context']}.{d['name']} - {instance_url}/{canopy_path}/{d['type']}s/{d['id']} - {d['fileRevision']}")
296
+ return "\n".join(comments)
@@ -0,0 +1,123 @@
1
+ import sys
2
+ from typing import Any, List
3
+ import requests
4
+ from polyapi.generate import get_functions_and_parse, generate_functions
5
+ from polyapi.config import get_api_key_and_url
6
+ from polyapi.utils import get_auth_headers, print_green, print_red, print_yellow
7
+ from polyapi.parser import parse_function_code, get_jsonschema_type
8
+ import importlib
9
+
10
+
11
+ def _func_already_exists(context: str, function_name: str) -> bool:
12
+ try:
13
+ module = importlib.import_module(f"polyapi.poly.{context}")
14
+ return bool(getattr(module, function_name, False))
15
+ except ModuleNotFoundError:
16
+ return False
17
+
18
+
19
+ def function_add_or_update(
20
+ name: str,
21
+ file: str,
22
+ context: str,
23
+ description: str,
24
+ client: bool,
25
+ server: bool,
26
+ logs_enabled: bool,
27
+ generate: bool = True,
28
+ execution_api_key: str = ""
29
+ ):
30
+ verb = "Updating" if _func_already_exists(context, name) else "Adding"
31
+ ftype = "server" if server else "client"
32
+ print(f"{verb} custom {ftype} function...", end="")
33
+
34
+ with open(file, "r") as f:
35
+ code = f.read()
36
+
37
+ # OK! let's parse the code and generate the arguments
38
+ parsed = parse_function_code(code, name, context)
39
+ return_type = parsed["types"]["returns"]["type"]
40
+
41
+ if not return_type:
42
+ print_red("ERROR")
43
+ print(
44
+ f"Function {name} not found as top-level function in {name}"
45
+ )
46
+ sys.exit(1)
47
+
48
+ data = {
49
+ "context": context or parsed["context"],
50
+ "name": name,
51
+ "description": description or parsed["types"]["description"],
52
+ "code": code,
53
+ "language": "python",
54
+ "returnType": return_type,
55
+ "returnTypeSchema": parsed["types"]["returns"]["typeSchema"],
56
+ "arguments": [{**p, "type": get_jsonschema_type(p["type"]) } for p in parsed["types"]["params"]],
57
+ "logsEnabled": logs_enabled or parsed["config"].get("logs_enabled", False),
58
+ }
59
+
60
+ if server and parsed["dependencies"]:
61
+ print_yellow(
62
+ "\nPlease note that deploying your functions will take a few minutes because it makes use of libraries other than polyapi."
63
+ )
64
+ data["requirements"] = parsed["dependencies"]
65
+
66
+ api_key, api_url = get_api_key_and_url()
67
+ assert api_key
68
+ if server:
69
+ url = f"{api_url}/functions/server"
70
+
71
+ if execution_api_key:
72
+ data["executionApiKey"] = execution_api_key
73
+
74
+ elif client:
75
+ url = f"{api_url}/functions/client"
76
+ else:
77
+ print_red("ERROR")
78
+ print("Please specify type of function with --client or --server")
79
+ sys.exit(1)
80
+
81
+ headers = get_auth_headers(api_key)
82
+ resp = requests.post(url, headers=headers, json=data)
83
+ if resp.status_code == 201:
84
+ print_green("DEPLOYED")
85
+ function_id = resp.json()["id"]
86
+ print(f"Function ID: {function_id}")
87
+ if generate:
88
+ print("Generating new custom function...", end="")
89
+ functions = get_functions_and_parse(limit_ids=[function_id])
90
+ generate_functions(functions)
91
+ print_green("DONE")
92
+ else:
93
+ print("Error adding function.")
94
+ print(resp.status_code)
95
+ print(resp.content)
96
+ sys.exit(1)
97
+
98
+
99
+ def function_execute(context: str, name: str, args: List) -> Any:
100
+ context_code = importlib.import_module(f"polyapi.poly.{context}")
101
+ print(f"Executing poly.{context}.{name}... ")
102
+ fn = getattr(context_code, name)
103
+ return fn(*args)
104
+
105
+
106
+ def spec_delete(function_type: str, function_id: str):
107
+ api_key, api_url = get_api_key_and_url()
108
+ assert api_key
109
+ if function_type == "api":
110
+ url = f"{api_url}/functions/api/{function_id}"
111
+ elif function_type == "serverFunction":
112
+ url = f"{api_url}/functions/server/{function_id}"
113
+ elif function_type == "customFunction":
114
+ url = f"{api_url}/functions/client/{function_id}"
115
+ elif function_type == "webhookHandle":
116
+ url = f"{api_url}/webhooks/{function_id}"
117
+ else:
118
+ print_red("ERROR")
119
+ print(f"Unknown function type: {function_type}")
120
+ sys.exit(1)
121
+ headers = get_auth_headers(api_key)
122
+ resp = requests.delete(url, headers=headers)
123
+ return resp
@@ -13,7 +13,7 @@ from .api import render_api_function
13
13
  from .server import render_server_function
14
14
  from .utils import add_import_to_init, get_auth_headers, init_the_init, to_func_namespace
15
15
  from .variables import generate_variables
16
- from .config import get_api_key_and_url, initialize_config
16
+ from .config import get_api_key_and_url
17
17
 
18
18
  SUPPORTED_FUNCTION_TYPES = {
19
19
  "apiFunction",
@@ -122,7 +122,6 @@ def remove_old_library():
122
122
 
123
123
 
124
124
  def generate() -> None:
125
- initialize_config()
126
125
 
127
126
  remove_old_library()
128
127