@vercel/python 3.0.5 → 3.1.0

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.
package/dist/index.js CHANGED
@@ -3819,7 +3819,6 @@ const allOptions = [
3819
3819
  discontinueDate: new Date('2022-07-18'),
3820
3820
  },
3821
3821
  ];
3822
- const upstreamProvider = 'This change is the result of a decision made by an upstream infrastructure provider (AWS)';
3823
3822
  function getDevPythonVersion() {
3824
3823
  // Use the system-installed version of `python3` when running `vercel dev`
3825
3824
  return {
@@ -3854,12 +3853,12 @@ function getSupportedPythonVersion({ isDev, pipLockPythonVersion, }) {
3854
3853
  throw new build_utils_1.NowBuildError({
3855
3854
  code: 'BUILD_UTILS_PYTHON_VERSION_DISCONTINUED',
3856
3855
  link: 'http://vercel.link/python-version',
3857
- message: `Python version "${selection.version}" detected in Pipfile.lock is discontinued and must be upgraded. ${upstreamProvider}.`,
3856
+ message: `Python version "${selection.version}" detected in Pipfile.lock is discontinued and must be upgraded.`,
3858
3857
  });
3859
3858
  }
3860
3859
  if (selection.discontinueDate) {
3861
3860
  const d = selection.discontinueDate.toISOString().split('T')[0];
3862
- console.warn(`Error: Python version "${selection.version}" detected in Pipfile.lock is deprecated. Deployments created on or after ${d} will fail to build. ${upstreamProvider}. http://vercel.link/python-version`);
3861
+ console.warn(`Error: Python version "${selection.version}" detected in Pipfile.lock has reached End-of-Life. Deployments created on or after ${d} will fail to build. http://vercel.link/python-version`);
3863
3862
  }
3864
3863
  return selection;
3865
3864
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/python",
3
- "version": "3.0.5",
3
+ "version": "3.1.0",
4
4
  "main": "./dist/index.js",
5
5
  "license": "MIT",
6
6
  "homepage": "https://vercel.com/docs/runtimes#official-runtimes/python",
@@ -23,10 +23,10 @@
23
23
  "devDependencies": {
24
24
  "@types/execa": "^0.9.0",
25
25
  "@types/jest": "27.4.1",
26
- "@vercel/build-utils": "5.0.1",
26
+ "@vercel/build-utils": "5.0.4",
27
27
  "@vercel/ncc": "0.24.0",
28
28
  "execa": "^1.0.0",
29
29
  "typescript": "4.3.4"
30
30
  },
31
- "gitHead": "0a2af4fb94d71a7a23ffc72fb68748be1177f345"
31
+ "gitHead": "3d3774ee7e3d344b3292d2166d485bdf41a68d4c"
32
32
  }
package/vc_init.py CHANGED
@@ -167,13 +167,23 @@ elif 'app' in __vc_variables:
167
167
  else:
168
168
  print('using Asynchronous Server Gateway Interface (ASGI)')
169
169
  # Originally authored by Jordan Eremieff and included under MIT license:
170
- # https://github.com/erm/mangum/blob/b4d21c8f5e304a3e17b88bc9fa345106acc50ad7/mangum/__init__.py
171
- # https://github.com/erm/mangum/blob/b4d21c8f5e304a3e17b88bc9fa345106acc50ad7/LICENSE
170
+ # https://github.com/erm/mangum/blob/07ce20a0e2f67c5c2593258a92c03fdc66d9edda/mangum/__init__.py
171
+ # https://github.com/erm/mangum/blob/07ce20a0e2f67c5c2593258a92c03fdc66d9edda/LICENSE
172
172
  import asyncio
173
173
  import enum
174
+ import logging
175
+ from contextlib import ExitStack
174
176
  from urllib.parse import urlparse
175
177
  from werkzeug.datastructures import Headers
176
178
 
179
+ def get_event_loop():
180
+ try:
181
+ return asyncio.get_running_loop()
182
+ except:
183
+ if sys.version_info < (3, 10):
184
+ return asyncio.get_event_loop()
185
+ else:
186
+ return asyncio.get_event_loop_policy().get_event_loop()
177
187
 
178
188
  class ASGICycleState(enum.Enum):
179
189
  REQUEST = enum.auto()
@@ -194,8 +204,8 @@ elif 'app' in __vc_variables:
194
204
  ASGI instance using the connection scope.
195
205
  Runs until the response is completely read from the application.
196
206
  """
197
- loop = asyncio.new_event_loop()
198
- self.app_queue = asyncio.Queue(loop=loop)
207
+ loop = get_event_loop()
208
+ self.app_queue = asyncio.Queue()
199
209
  self.put_message({'type': 'http.request', 'body': body, 'more_body': False})
200
210
 
201
211
  asgi_instance = app(self.scope, self.receive, self.send)
@@ -257,6 +267,156 @@ elif 'app' in __vc_variables:
257
267
  self.response['body'] = base64.b64encode(self.body).decode('utf-8')
258
268
  self.response['encoding'] = 'base64'
259
269
 
270
+ class LifespanFailure(Exception):
271
+ """Raise when a lifespan failure event is sent by an application."""
272
+
273
+ class LifespanUnsupported(Exception):
274
+ """Raise when lifespan events are not supported by an application."""
275
+
276
+ class UnexpectedMessage(Exception):
277
+ """Raise when an unexpected message type is received during an ASGI cycle."""
278
+
279
+ class LifespanCycleState(enum.Enum):
280
+ """
281
+ The state of the ASGI `lifespan` connection.
282
+ * **CONNECTING** - Initial state. The ASGI application instance will be run with
283
+ the connection scope containing the `lifespan` type.
284
+ * **STARTUP** - The lifespan startup event has been pushed to the queue to be
285
+ received by the application.
286
+ * **SHUTDOWN** - The lifespan shutdown event has been pushed to the queue to be
287
+ received by the application.
288
+ * **FAILED** - A lifespan failure has been detected, and the connection will be
289
+ closed with an error.
290
+ * **UNSUPPORTED** - An application attempted to send a message before receiving
291
+ the lifepan startup event. If the lifespan argument is "on", then the connection
292
+ will be closed with an error.
293
+ """
294
+
295
+ CONNECTING = enum.auto()
296
+ STARTUP = enum.auto()
297
+ SHUTDOWN = enum.auto()
298
+ FAILED = enum.auto()
299
+ UNSUPPORTED = enum.auto()
300
+
301
+ class Lifespan:
302
+
303
+ def __init__(self, app):
304
+ self.app = app
305
+ self.state = LifespanCycleState.CONNECTING
306
+ self.exception = None
307
+ self.logger = logging.getLogger('lifespan')
308
+ self.loop = get_event_loop()
309
+ self.app_queue = asyncio.Queue()
310
+ self.startup_event = asyncio.Event()
311
+ self.shutdown_event = asyncio.Event()
312
+
313
+
314
+ def __enter__(self) -> None:
315
+ """Runs the event loop for application startup."""
316
+ self.loop.create_task(self.run())
317
+ self.loop.run_until_complete(self.startup())
318
+
319
+ def __exit__(
320
+ self,
321
+ exc_type,
322
+ exc_value,
323
+ traceback,
324
+ ) -> None:
325
+ """Runs the event loop for application shutdown."""
326
+ self.loop.run_until_complete(self.shutdown())
327
+
328
+ async def run(self):
329
+ """Calls the application with the `lifespan` connection scope."""
330
+ try:
331
+ await self.app(
332
+ {"type": "lifespan", "asgi": {"spec_version": "2.0", "version": "3.0"}},
333
+ self.receive,
334
+ self.send,
335
+ )
336
+ except LifespanUnsupported:
337
+ self.logger.info("ASGI 'lifespan' protocol appears unsupported.")
338
+ except (LifespanFailure, UnexpectedMessage) as exc:
339
+ self.exception = exc
340
+ except BaseException as exc:
341
+ self.logger.error("Exception in 'lifespan' protocol.", exc_info=exc)
342
+ finally:
343
+ self.startup_event.set()
344
+ self.shutdown_event.set()
345
+
346
+ async def send(self, message):
347
+ """Awaited by the application to send ASGI `lifespan` events."""
348
+ message_type = message["type"]
349
+
350
+ if self.state is LifespanCycleState.CONNECTING:
351
+ # If a message is sent before the startup event is received by the
352
+ # application, then assume that lifespan is unsupported.
353
+ self.state = LifespanCycleState.UNSUPPORTED
354
+ raise LifespanUnsupported("Lifespan protocol appears unsupported.")
355
+
356
+ if message_type not in (
357
+ "lifespan.startup.complete",
358
+ "lifespan.shutdown.complete",
359
+ "lifespan.startup.failed",
360
+ "lifespan.shutdown.failed",
361
+ ):
362
+ self.state = LifespanCycleState.FAILED
363
+ raise UnexpectedMessage(f"Unexpected '{message_type}' event received.")
364
+
365
+ if self.state is LifespanCycleState.STARTUP:
366
+ if message_type == "lifespan.startup.complete":
367
+ self.startup_event.set()
368
+ elif message_type == "lifespan.startup.failed":
369
+ self.state = LifespanCycleState.FAILED
370
+ self.startup_event.set()
371
+ message_value = message.get("message", "")
372
+ raise LifespanFailure(f"Lifespan startup failure. {message_value}")
373
+
374
+ elif self.state is LifespanCycleState.SHUTDOWN:
375
+ if message_type == "lifespan.shutdown.complete":
376
+ self.shutdown_event.set()
377
+ elif message_type == "lifespan.shutdown.failed":
378
+ self.state = LifespanCycleState.FAILED
379
+ self.shutdown_event.set()
380
+ message_value = message.get("message", "")
381
+ raise LifespanFailure(f"Lifespan shutdown failure. {message_value}")
382
+
383
+ async def receive(self):
384
+ """Awaited by the application to receive ASGI `lifespan` events."""
385
+ if self.state is LifespanCycleState.CONNECTING:
386
+
387
+ # Connection established. The next event returned by the queue will be
388
+ # `lifespan.startup` to inform the application that the connection is
389
+ # ready to receive lfiespan messages.
390
+ self.state = LifespanCycleState.STARTUP
391
+
392
+ elif self.state is LifespanCycleState.STARTUP:
393
+
394
+ # Connection shutting down. The next event returned by the queue will be
395
+ # `lifespan.shutdown` to inform the application that the connection is now
396
+ # closing so that it may perform cleanup.
397
+ self.state = LifespanCycleState.SHUTDOWN
398
+
399
+ return await self.app_queue.get()
400
+
401
+ async def startup(self) -> None:
402
+ """Pushes the `lifespan` startup event to the queue and handles errors."""
403
+ await self.app_queue.put({"type": "lifespan.startup"})
404
+ await self.startup_event.wait()
405
+ if self.state is LifespanCycleState.FAILED:
406
+ raise LifespanFailure(self.exception)
407
+
408
+ if not self.exception:
409
+ self.logger.info("Application startup complete.")
410
+ else:
411
+ self.logger.info("Application startup failed.")
412
+
413
+ async def shutdown(self) -> None:
414
+ """Pushes the `lifespan` shutdown event to the queue and handles errors."""
415
+ await self.app_queue.put({"type": "lifespan.shutdown"})
416
+ await self.shutdown_event.wait()
417
+ if self.state is LifespanCycleState.FAILED:
418
+ raise LifespanFailure(self.exception)
419
+
260
420
  def vc_handler(event, context):
261
421
  payload = json.loads(event['body'])
262
422
 
@@ -289,9 +449,13 @@ elif 'app' in __vc_variables:
289
449
  'raw_path': path.encode(),
290
450
  }
291
451
 
292
- asgi_cycle = ASGICycle(scope)
293
- response = asgi_cycle(__vc_module.app, body)
294
- return response
452
+ with ExitStack() as stack:
453
+ lifespan = Lifespan(__vc_module.app)
454
+ stack.enter_context(lifespan)
455
+
456
+ asgi_cycle = ASGICycle(scope)
457
+ response = asgi_cycle(__vc_module.app, body)
458
+ return response
295
459
 
296
460
  else:
297
461
  print('Missing variable `handler` or `app` in file "__VC_HANDLER_ENTRYPOINT".')