@vercel/python 6.14.1 → 6.15.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/python",
3
- "version": "6.14.1",
3
+ "version": "6.15.0",
4
4
  "main": "./dist/index.js",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://vercel.com/docs/runtimes#official-runtimes/python",
@@ -16,7 +16,7 @@
16
16
  "directory": "packages/python"
17
17
  },
18
18
  "dependencies": {
19
- "@vercel/python-analysis": "0.5.0"
19
+ "@vercel/python-analysis": "0.6.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@renovatebot/pep440": "4.2.1",
@@ -34,8 +34,9 @@
34
34
  "smol-toml": "1.5.2",
35
35
  "vitest": "2.1.4",
36
36
  "which": "3.0.0",
37
- "@vercel/build-utils": "13.4.2",
38
- "@vercel/error-utils": "2.0.3"
37
+ "@vercel/error-utils": "2.0.3",
38
+ "@vercel/build-utils": "13.4.3",
39
+ "@vercel/python-runtime": "0.5.0"
39
40
  },
40
41
  "scripts": {
41
42
  "build": "node ../../utils/build-builder.mjs",
@@ -45,6 +45,82 @@ USER_ASGI_APP = _CAND if callable(_CAND) else _app
45
45
 
46
46
  PUBLIC_DIR = 'public'
47
47
 
48
+
49
+ def _normalize_service_route_prefix(raw_prefix):
50
+ if not raw_prefix:
51
+ return ''
52
+
53
+ prefix = raw_prefix.strip()
54
+ if not prefix:
55
+ return ''
56
+
57
+ if not prefix.startswith('/'):
58
+ prefix = f'/{prefix}'
59
+
60
+ return '' if prefix == '/' else prefix.rstrip('/')
61
+
62
+
63
+ def _is_service_route_prefix_strip_enabled():
64
+ raw = os.environ.get('VERCEL_SERVICE_ROUTE_PREFIX_STRIP')
65
+ if not raw:
66
+ return False
67
+ return raw.lower() in ('1', 'true')
68
+
69
+
70
+ _SERVICE_ROUTE_PREFIX = (
71
+ _normalize_service_route_prefix(os.environ.get('VERCEL_SERVICE_ROUTE_PREFIX'))
72
+ if _is_service_route_prefix_strip_enabled()
73
+ else ''
74
+ )
75
+
76
+
77
+ def _strip_service_route_prefix(path_value):
78
+ if not path_value:
79
+ path_value = '/'
80
+ elif not path_value.startswith('/'):
81
+ path_value = f'/{path_value}'
82
+
83
+ prefix = _SERVICE_ROUTE_PREFIX
84
+ if not prefix:
85
+ return path_value, ''
86
+
87
+ if path_value == prefix:
88
+ return '/', prefix
89
+
90
+ if path_value.startswith(f'{prefix}/'):
91
+ stripped = path_value[len(prefix):]
92
+ return stripped if stripped else '/', prefix
93
+
94
+ return path_value, ''
95
+
96
+
97
+ def _apply_service_route_prefix_to_scope(scope):
98
+ path_value, matched_prefix = _strip_service_route_prefix(scope.get('path', '/'))
99
+ if not matched_prefix:
100
+ return scope
101
+
102
+ updated_scope = dict(scope)
103
+ updated_scope['path'] = path_value
104
+
105
+ raw_path = scope.get('raw_path')
106
+ if isinstance(raw_path, (bytes, bytearray)):
107
+ try:
108
+ decoded = bytes(raw_path).decode('utf-8', 'surrogateescape')
109
+ stripped_raw, _ = _strip_service_route_prefix(decoded)
110
+ updated_scope['raw_path'] = stripped_raw.encode(
111
+ 'utf-8', 'surrogateescape'
112
+ )
113
+ except Exception:
114
+ pass
115
+
116
+ existing_root = scope.get('root_path', '') or ''
117
+ if existing_root and existing_root != '/':
118
+ existing_root = existing_root.rstrip('/')
119
+ else:
120
+ existing_root = ''
121
+ updated_scope['root_path'] = f'{existing_root}{matched_prefix}'
122
+ return updated_scope
123
+
48
124
  # Prepare static files app (if starlette/fastapi installed)
49
125
  static_app = None
50
126
  if StaticFiles is not None:
@@ -59,19 +135,21 @@ if StaticFiles is not None:
59
135
 
60
136
 
61
137
  async def app(scope, receive, send):
62
- if static_app is not None and scope.get('type') == 'http':
63
- req_path = scope.get('path', '/') or '/'
138
+ effective_scope = _apply_service_route_prefix_to_scope(scope)
139
+
140
+ if static_app is not None and effective_scope.get('type') == 'http':
141
+ req_path = effective_scope.get('path', '/') or '/'
64
142
  safe = _p.normpath(req_path).lstrip('/')
65
143
  full = _p.join(PUBLIC_DIR, safe)
66
144
  try:
67
145
  base = _p.realpath(PUBLIC_DIR)
68
146
  target = _p.realpath(full)
69
147
  if (target == base or target.startswith(base + _p.sep)) and _p.isfile(target):
70
- await static_app(scope, receive, send)
148
+ await static_app(effective_scope, receive, send)
71
149
  return
72
150
  except Exception:
73
151
  pass
74
- await USER_ASGI_APP(scope, receive, send)
152
+ await USER_ASGI_APP(effective_scope, receive, send)
75
153
 
76
154
 
77
155
  if __name__ == '__main__':
@@ -24,6 +24,54 @@ def _color(text: str, code: str) -> str:
24
24
  USER_MODULE = "__VC_DEV_MODULE_PATH__"
25
25
  PUBLIC_DIR = "public"
26
26
 
27
+
28
+ def _normalize_service_route_prefix(raw_prefix):
29
+ if not raw_prefix:
30
+ return ''
31
+
32
+ prefix = raw_prefix.strip()
33
+ if not prefix:
34
+ return ''
35
+
36
+ if not prefix.startswith('/'):
37
+ prefix = f'/{prefix}'
38
+
39
+ return '' if prefix == '/' else prefix.rstrip('/')
40
+
41
+
42
+ def _is_service_route_prefix_strip_enabled():
43
+ raw = os.environ.get('VERCEL_SERVICE_ROUTE_PREFIX_STRIP')
44
+ if not raw:
45
+ return False
46
+ return raw.lower() in ('1', 'true')
47
+
48
+
49
+ _SERVICE_ROUTE_PREFIX = (
50
+ _normalize_service_route_prefix(os.environ.get('VERCEL_SERVICE_ROUTE_PREFIX'))
51
+ if _is_service_route_prefix_strip_enabled()
52
+ else ''
53
+ )
54
+
55
+
56
+ def _strip_service_route_prefix(path_info):
57
+ if not path_info:
58
+ path_info = '/'
59
+ elif not path_info.startswith('/'):
60
+ path_info = f'/{path_info}'
61
+
62
+ prefix = _SERVICE_ROUTE_PREFIX
63
+ if not prefix:
64
+ return path_info, ''
65
+
66
+ if path_info == prefix:
67
+ return '/', prefix
68
+
69
+ if path_info.startswith(f'{prefix}/'):
70
+ stripped = path_info[len(prefix):]
71
+ return stripped if stripped else '/', prefix
72
+
73
+ return path_info, ''
74
+
27
75
  _mod = import_module(USER_MODULE)
28
76
  _app = getattr(_mod, "app", None)
29
77
  if _app is None:
@@ -74,6 +122,18 @@ def _not_found(start_response):
74
122
 
75
123
 
76
124
  def _combined_app(environ, start_response):
125
+ path_info, matched_prefix = _strip_service_route_prefix(
126
+ environ.get("PATH_INFO", "/") or "/"
127
+ )
128
+ environ["PATH_INFO"] = path_info
129
+ if matched_prefix:
130
+ script_name = environ.get("SCRIPT_NAME", "") or ""
131
+ if script_name and script_name != "/":
132
+ script_name = script_name.rstrip("/")
133
+ else:
134
+ script_name = ""
135
+ environ["SCRIPT_NAME"] = f"{script_name}{matched_prefix}"
136
+
77
137
  # Try static first; if 404 then delegate to user app
78
138
  captured_status = ""
79
139
  captured_headers = tuple()