poetry-plugin-ivcap 0.3.1__tar.gz → 0.4.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: poetry-plugin-ivcap
3
- Version: 0.3.1
3
+ Version: 0.4.1
4
4
  Summary: A custom Poetry command for IVCAP deployments
5
5
  License: MIT
6
6
  Author: Max Ott
@@ -10,6 +10,8 @@ import sys
10
10
  import tempfile
11
11
  import uuid
12
12
  import humanize
13
+ import subprocess
14
+ import requests
13
15
 
14
16
  from .constants import DEF_POLICY, PLUGIN_NAME, POLICY_OPT, SERVICE_FILE_OPT, SERVICE_ID_OPT
15
17
 
@@ -129,6 +131,131 @@ def get_policy(data, line):
129
131
  policy = DEF_POLICY
130
132
  return policy
131
133
 
134
+ def exec_job(data, args, is_silent, line):
135
+ """
136
+ Execute a job by posting a JSON request to the IVCAP API.
137
+
138
+ Returns:
139
+ requests.Response: The response object from the API call.
140
+ """
141
+ # Parse 'args' for run options
142
+ if not isinstance(args, list) or len(args) < 1:
143
+ raise Exception("args must be a list with at least one element")
144
+ file_name = args[0]
145
+ timeout = 20 # default timeout
146
+ if len(args) == 1:
147
+ pass # only file_name provided
148
+ elif len(args) == 3 and args[1] == '--timeout':
149
+ try:
150
+ timeout = int(args[2])
151
+ except ValueError:
152
+ raise Exception("Timeout value must be an integer")
153
+ else:
154
+ raise Exception("args must be [file_name] or [file_name, '--timeout', value]")
155
+
156
+ # Get access token using ivcap CLI
157
+ try:
158
+ token = subprocess.check_output(
159
+ ["ivcap", "--silent", "context", "get", "access-token", "--refresh-token"],
160
+ text=True
161
+ ).strip()
162
+ except Exception as e:
163
+ raise RuntimeError(f"Failed to get IVCAP access token: {e}")
164
+
165
+ # Get IVCAP deployment URL
166
+ try:
167
+ ivcap_url = subprocess.check_output(
168
+ ["ivcap", "--silent", "context", "get", "url"],
169
+ text=True
170
+ ).strip()
171
+ except Exception as e:
172
+ raise RuntimeError(f"Failed to get IVCAP deployment URL: {e}")
173
+
174
+ # Read the JSON request file
175
+ try:
176
+ with open(file_name, "r", encoding="utf-8") as f:
177
+ json_data = f.read()
178
+ except Exception as e:
179
+ raise RuntimeError(f"Failed to read request file '{file_name}': {e}")
180
+
181
+ # Prepare headers
182
+ headers = {
183
+ "content-type": "application/json",
184
+ "Timeout": f"{timeout}",
185
+ "Authorization": f"Bearer {token}"
186
+ }
187
+
188
+ # Build URL
189
+ service_id = get_service_id(data, is_silent, line)
190
+ url = f"{ivcap_url}/1/services2/{service_id}/jobs"
191
+ params = {"with-result-content": "true"}
192
+
193
+ # 5. POST request
194
+ if not is_silent:
195
+ line(f"<debug>Creating job '{url}'</debug>")
196
+ try:
197
+ response = requests.post(url, headers=headers, params=params, data=json_data)
198
+ except Exception as e:
199
+ raise RuntimeError(f"Job submission failed: {e}")
200
+
201
+ # Handle response according to requirements
202
+ import time
203
+ import json
204
+
205
+ def handle_response(resp):
206
+ content_type = resp.headers.get("content-type", "")
207
+ if resp.status_code >= 300:
208
+ line(f"<warning>WARNING: Received status code {resp.status_code}</warning>")
209
+ line(f"<info>Headers: {str(resp.headers)}</info>")
210
+ line(f"<info>Body: {str(resp.text)}</info>")
211
+ elif resp.status_code == 200:
212
+ if "application/json" in content_type:
213
+ try:
214
+ parsed = resp.json()
215
+ print(json.dumps(parsed, indent=2, sort_keys=True))
216
+ except Exception as e:
217
+ line(f"<warning>Failed to parse JSON response: {e}</warning>")
218
+ line(f"<warning>Headers: {str(resp.headers)}</warning>")
219
+ else:
220
+ line(f"<info>Headers: {str(resp.headers)}</info>")
221
+ else:
222
+ line(f"<warning>Received status code {resp.status_code}</warning>")
223
+ line(f"<warning>Headers: {str(resp.headers)}</warning>")
224
+
225
+ if response.status_code == 202:
226
+ try:
227
+ payload = response.json()
228
+ # use when ../output is fixed
229
+ # location = f"{payload.get('location')}/output"
230
+ location = f"{payload.get('location')}"
231
+ job_id = payload.get("job-id")
232
+ retry_later = payload.get("retry-later", 10)
233
+ if not is_silent:
234
+ line(f"<debug>Job '{job_id}' accepted, but no result yet. Polling in {retry_later} seconds.</debug>")
235
+ while True:
236
+ time.sleep(retry_later)
237
+ poll_headers = {
238
+ "Authorization": f"Bearer {token}"
239
+ }
240
+ poll_resp = requests.get(location, headers=poll_headers)
241
+ if poll_resp.status_code == 202:
242
+ try:
243
+ poll_payload = poll_resp.json()
244
+ location = poll_payload.get("location", location)
245
+ retry_later = poll_payload.get("retry-later", retry_later)
246
+ if not is_silent:
247
+ line(f"<debug>Still processing. Next poll in {retry_later} seconds.</debug>")
248
+ except Exception as e:
249
+ line(f"<error>Failed to parse polling response: {e}</error>")
250
+ break
251
+ else:
252
+ handle_response(poll_resp)
253
+ break
254
+ except Exception as e:
255
+ line(f"<error>Failed to handle 202 response: {e}</error>")
256
+ else:
257
+ handle_response(response)
258
+
132
259
  def get_account_id(data, line, is_silent=False):
133
260
  check_ivcap_cmd(line)
134
261
  cmd = ["ivcap", "context", "get", "account-id"]
@@ -10,10 +10,11 @@ from cleo.helpers import argument, option
10
10
  import subprocess
11
11
  from importlib.metadata import version
12
12
 
13
- from poetry_plugin_ivcap.constants import DOCKER_BUILD_TEMPLATE_OPT, DOCKER_RUN_TEMPLATE_OPT, PLUGIN_CMD, PLUGIN_NAME, PORT_OPT, SERVICE_FILE_OPT, SERVICE_ID_OPT, SERVICE_TYPE_OPT
13
+ from poetry_plugin_ivcap.constants import DOCKER_BUILD_TEMPLATE_OPT, DOCKER_RUN_TEMPLATE_OPT, PLUGIN_CMD, PLUGIN_NAME
14
+ from poetry_plugin_ivcap.constants import PORT_OPT, SERVICE_FILE_OPT, SERVICE_ID_OPT, SERVICE_TYPE_OPT, POLICY_OPT
14
15
  from poetry_plugin_ivcap.util import get_version
15
16
 
16
- from .ivcap import create_service_id, get_service_id, service_register, tool_register
17
+ from .ivcap import create_service_id, exec_job, get_service_id, service_register, tool_register
17
18
  from .docker import docker_build, docker_run
18
19
  from .ivcap import docker_publish
19
20
 
@@ -31,6 +32,7 @@ Available subcommands:
31
32
  docker-build Build the docker image for this service
32
33
  docker-run Run the service's docker image locally for testing
33
34
  deploy Deploy the service to IVCAP (calls docker-publish, service-register and tool-register)
35
+ job-exec file_name Execute a job defined in 'file_name'
34
36
  docker-publish Publish the service's docker image to IVCAP
35
37
  service-register Register the service with IVCAP
36
38
  create-service-id Create a unique service ID for the service
@@ -40,15 +42,17 @@ Available subcommands:
40
42
 
41
43
  Example:
42
44
  poetry {PLUGIN_CMD} run -- --port 8080
45
+ poetry {PLUGIN_CMD} job-exec request.json -- --timeout 0 # don't wait for result
43
46
 
44
47
  Configurable options in pyproject.toml:
45
48
 
46
49
  [tool.{PLUGIN_NAME}]
47
50
  {SERVICE_FILE_OPT} = "service.py" # The Python file that implements the service
48
- {SERVICE_ID_OPT} = "urn:ivcap:service:ac158a1f-dfb4-5dac-bf2e-9bf15e0f2cc7" # A unique identifier for the service
51
+ {SERVICE_ID_OPT} = "urn:ivcap:service:ac158a1f-dfb4-5dac-bf2e-00000000000" # A unique identifier for the service
49
52
  {SERVICE_TYPE_OPT} = "lambda
50
53
 
51
54
  # Optional
55
+ {POLICY_OPT} = "urn:ivcap:policy:ivcap.open.metadata"
52
56
  {DOCKER_BUILD_TEMPLATE_OPT} = "docker buildx build -t #DOCKER_NAME# ."
53
57
  {DOCKER_RUN_TEMPLATE_OPT} = "docker run --rm -p #PORT#:#PORT# #DOCKER_NAME#"
54
58
  """
@@ -88,12 +92,14 @@ Configurable options in pyproject.toml:
88
92
  docker_publish(data, self.line)
89
93
  service_register(data, self.line)
90
94
  tool_register(data, self.line)
95
+ elif sub == "exec-job" or sub == "job-exec":
96
+ exec_job(data, args, is_silent, self.line)
91
97
  elif sub == "docker-publish":
92
98
  docker_publish(data, self.line)
93
99
  elif sub == "service-register":
94
100
  service_register(data, self.line)
95
101
  elif sub == "create-service-id":
96
- sid = create_service_id(data, self.line)
102
+ sid = create_service_id(data, is_silent, self.line)
97
103
  print(sid)
98
104
  elif sub == "get-service-id":
99
105
  sid = get_service_id(data, is_silent, self.line)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "poetry-plugin-ivcap"
3
- version = "0.3.1"
3
+ version = "0.4.1"
4
4
  description = "A custom Poetry command for IVCAP deployments"
5
5
  authors = ["Max Ott <max.ott@csiro.au>"]
6
6
  license = "MIT"