1
1
import argparse
2
- import logging
3
- import mimetypes
4
2
import os
5
- from pathlib import Path
6
3
import re
7
4
import subprocess
8
5
import sys
9
- import time
10
- import typing as t
6
+ from pathlib import Path
11
7
12
8
import yaml
13
9
from werkzeug ._internal import _logger # noqa
14
- from werkzeug .http import http_date , is_resource_modified
15
- from werkzeug .middleware .dispatcher import DispatcherMiddleware
16
- from werkzeug .middleware .http_proxy import ProxyMiddleware
17
- from werkzeug .middleware .shared_data import SharedDataMiddleware
18
- from werkzeug .serving import run_simple , WSGIRequestHandler , _ansi_style , \
19
- _log_add_style
20
- from werkzeug .urls import uri_to_iri
21
- from werkzeug .utils import get_content_type
22
- from werkzeug .wrappers import Request , Response
23
- from werkzeug .wsgi import get_path_info , wrap_file
24
-
25
- __version__ = "0.9.11"
26
-
27
- subprocesses = []
28
-
29
-
30
- class MainWSGIRequestHandler (WSGIRequestHandler ):
31
- def log_date_time_string (self ):
32
- """Return the current time formatted for logging."""
33
- now = time .time ()
34
- year , month , day , hh , mm , ss , x , y , z = time .localtime (now )
35
- s = "%04d-%02d-%02d %02d:%02d:%02d" % (
36
- year , month , day , hh , mm , ss )
37
- return s
38
-
39
- def log_request (
40
- self ,
41
- code : t .Union [int , str ] = "-" ,
42
- size : t .Union [int , str ] = "-" ,
43
- ) -> None :
44
- """coloring the status code"""
45
- try :
46
- path = uri_to_iri (self .path )
47
- msg = f"[{ self .command } ] { path } "
48
- except AttributeError :
49
- # path isn't set if the requestline was bad
50
- msg = self .requestline
51
-
52
- code = str (code )
53
-
54
- log_type = "info"
55
- if code != "200" : # possibility to filter 200 requests
56
- log_type = "warning"
57
-
58
- if _log_add_style :
59
- if code [0 ] == "1" : # 1xx - Informational
60
- code = _ansi_style (code , "bold" )
61
- elif code == "200" : # 2xx - Success
62
- pass
63
- elif code == "304" : # 304 - Resource Not Modified
64
- code = _ansi_style (code , "cyan" )
65
- elif code [0 ] == "3" : # 3xx - Redirection
66
- code = _ansi_style (code , "green" )
67
- elif code == "404" : # 404 - Resource Not Found
68
- code = _ansi_style (code , "yellow" )
69
- elif code [0 ] == "4" : # 4xx - Client Error
70
- code = _ansi_style (code , "bold" , "red" )
71
- else : # 5xx, or any other response
72
- code = _ansi_style (code , "bold" , "red" )
73
-
74
- self .log (log_type , '[%s] %s' , code , msg )
75
-
76
- def log (self , log_type : str , message : str , * args ) -> None :
77
- global _logger
78
-
79
- if _logger is None :
80
- _logger = logging .getLogger ("werkzeug" )
81
- _logger .setLevel (logging .INFO )
82
- _logger .addHandler (logging .StreamHandler ())
83
-
84
- getattr (_logger , log_type )(f"[{ self .log_date_time_string ()} ] { message % args } " )
85
-
86
-
87
- class WrappingApp :
88
- """simple wrapping app"""
89
-
90
- def __init__ (self , config ):
91
- pass
10
+ from werkzeug .serving import run_simple
92
11
93
- def wsgi_app (self , environ , start_response ):
94
- request = Request (environ )
95
- response = Response (f'Path not found or invalid: { request .path } ' ,
96
- status = 404 )
97
- return response (environ , start_response )
12
+ from . import utils
13
+ from .app_wrapper import AppWrapper
14
+ from .dispatcher import Dispatcher
15
+ from .proxy import Proxy
16
+ from .request_handler import CustomWSGIRequestHandler
17
+ from .shared_data import SharedData
98
18
99
- def __call__ (self , environ , start_response ):
100
- return self .wsgi_app (environ , start_response )
19
+ __version__ = "0.1.0"
101
20
102
-
103
- class Proxy (ProxyMiddleware ):
104
- """this addition allows to redirect all routes to given targets"""
105
-
106
- def __init__ (self , app , targets , chunk_size = 2 << 13 , timeout = 10 ):
107
- super ().__init__ (app , targets , chunk_size , timeout )
108
-
109
- def _set_defaults (opts ):
110
- opts .setdefault ("remove_prefix" , False )
111
- opts .setdefault ("host" , "<auto>" )
112
- opts .setdefault ("headers" , {})
113
- opts .setdefault ("ssl_context" , None )
114
- return opts
115
-
116
- self .targets = {
117
- f"{ k } " : _set_defaults (v ) for k , v in targets .items ()
118
- }
119
-
120
- def __call__ (self , environ : "WSGIEnvironment" ,
121
- start_response : "StartResponse" ) -> t .Iterable [bytes ]:
122
- path = get_path_info (environ , charset = 'utf-8' , errors = 'replace' )
123
- app = self .app
124
- for prefix , opts in self .targets .items ():
125
- if path .startswith (prefix ):
126
- app = self .proxy_to (opts , path , prefix )
127
- break
128
-
129
- return app (environ , start_response )
130
-
131
-
132
- class Dispatcher (DispatcherMiddleware ):
133
- """use regex to find a matching route"""
134
-
135
- def __call__ (self , environ , start_response ):
136
- app = self .mounts ["/" ]
137
-
138
- for route , _app in self .mounts .items ():
139
- if re .match (route , environ ["PATH_INFO" ]):
140
- app = _app
141
- break
142
- return app (environ , start_response )
143
-
144
-
145
- class SharedData (SharedDataMiddleware ):
146
- """use regex to find a matching files"""
147
-
148
- def __init__ (
149
- self ,
150
- app ,
151
- exports ,
152
- disallow : None = None ,
153
- cache : bool = True ,
154
- cache_timeout : int = 60 * 60 * 12 ,
155
- fallback_mimetype : str = "application/octet-stream" ,
156
- ) -> None :
157
- self .org_exports = exports .copy ()
158
- super ().__init__ (app , exports , disallow , cache , cache_timeout ,
159
- fallback_mimetype )
160
-
161
- def __call__ (self , environ , start_response ):
162
- path = get_path_info (environ )
163
- file_loader = None
164
-
165
- for search_path , loader in self .exports :
166
- # lets check for regex, and inject real_path
167
- if re .match (search_path , path ):
168
- real_path = re .sub (search_path , self .org_exports [search_path ],
169
- path , 1 )
170
- real_filename , file_loader = self .get_file_loader (real_path )(
171
- None )
172
-
173
- if file_loader is not None :
174
- break
175
-
176
- if search_path == path :
177
- real_filename , file_loader = loader (None )
178
-
179
- if file_loader is not None :
180
- break
181
-
182
- if not search_path .endswith ("/" ):
183
- search_path += "/"
184
-
185
- if path .startswith (search_path ):
186
- real_filename , file_loader = loader (path [len (search_path ):])
187
-
188
- if file_loader is not None :
189
- break
190
-
191
- if file_loader is None or not self .is_allowed (real_filename ): # noqa
192
- return self .app (environ , start_response )
193
-
194
- guessed_type = mimetypes .guess_type (real_filename ) # type: ignore
195
- mime_type = get_content_type (guessed_type [0 ] or self .fallback_mimetype ,
196
- "utf-8" )
197
-
198
- try :
199
- f , mtime , file_size = file_loader ()
200
- except :
201
- return self .app (environ , start_response ) # 404
202
-
203
- headers = [("Date" , http_date ())]
204
-
205
- if self .cache :
206
- timeout = self .cache_timeout
207
- etag = self .generate_etag (mtime , file_size ,
208
- real_filename ) # type: ignore
209
- headers += [
210
- ("Etag" , f'"{ etag } "' ),
211
- ("Cache-Control" , f"max-age={ timeout } , public" ),
212
- ]
213
-
214
- if not is_resource_modified (environ , etag , last_modified = mtime ):
215
- f .close ()
216
- start_response ("304 Not Modified" , headers )
217
- return []
218
-
219
- headers .append (("Expires" , http_date (time .time () + timeout )))
220
- else :
221
- headers .append (("Cache-Control" , "public" ))
222
-
223
- headers .extend (
224
- (
225
- ("Content-Type" , mime_type ),
226
- ("Content-Length" , str (file_size )),
227
- ("Last-Modified" , http_date (mtime )),
228
- )
229
- )
230
- start_response ("200 OK" , headers )
231
- return wrap_file (environ , f )
21
+ subprocesses = []
232
22
233
23
234
24
def start_server (
@@ -241,9 +31,8 @@ def start_server(
241
31
protocol : str = "http" ,
242
32
) -> None :
243
33
"""use the dispatcherMiddleware to connect SharedDataMiddleware and ProxyMiddleware with the wrapping app."""
244
- app = WrappingApp ({} )
34
+ app = AppWrapper ( )
245
35
apps = {}
246
-
247
36
# make shared middlewares for static files as configured in app.yaml
248
37
for route in app_yaml ["handlers" ]:
249
38
if path := route .get ("static_dir" ):
@@ -269,43 +58,7 @@ def start_server(
269
58
app .wsgi_app = Dispatcher (app .wsgi_app , apps )
270
59
271
60
run_simple (host , port , app , use_debugger = False , use_reloader = True ,
272
- threaded = True , request_handler = MainWSGIRequestHandler )
273
-
274
-
275
- def set_env_vars (application_id : str , args : argparse .Namespace , app_yaml : dict ):
276
- """set necessary environment variables"""
277
- # First, merge the app.yaml into the environment so that the variables
278
- # from the CLI can overwrite it.
279
- if env_vars := app_yaml .get ("env_variables" ):
280
- if not isinstance (env_vars , dict ):
281
- raise TypeError (
282
- f"env_variables section in app.yaml must be a dict. Got { type (env_vars )} " )
283
- os .environ |= {k : str (v ) for k , v in app_yaml ["env_variables" ].items ()}
284
-
285
- os .environ ["GAE_ENV" ] = "localdev"
286
- os .environ ["CLOUDSDK_CORE_PROJECT" ] = application_id
287
- os .environ ["GOOGLE_CLOUD_PROJECT" ] = application_id
288
- os .environ ["GAE_VERSION" ] = str (time .time ())
289
- os .environ ["GRPC_ENABLE_FORK_SUPPORT" ] = "0"
290
-
291
- # Merge environment variables from CLI parameter
292
- if args .env_var :
293
- os .environ |= dict (v .split ("=" , 1 ) for v in args .env_var )
294
-
295
-
296
- def patch_gunicorn ():
297
- import gunicorn .workers .base
298
- with open (gunicorn .workers .base .__file__ , 'r+' ) as file :
299
- content = file .read ()
300
-
301
- if "except (SyntaxError, NameError) as e:" in content :
302
- return 0
303
-
304
- file .seek (0 )
305
- file .write (content .replace (
306
- ' except SyntaxError as e:' ,
307
- ' except (SyntaxError, NameError) as e:'
308
- ))
61
+ threaded = True , request_handler = CustomWSGIRequestHandler )
309
62
310
63
311
64
def start_gunicorn (
@@ -363,7 +116,7 @@ def main():
363
116
help = 'The entrypoint is the basic gunicorn command. By default, it\' s taken from app.yaml. '
364
117
'This parameter can be used to set a different entrypoint. '
365
118
'To provide this parameter via ViUR-CLI, you have to double quote it: '
366
- ' --entrypoint "\' gunicorn -b :\ $PORT --disable-redirect-access-to-syslog main:app\' "' )
119
+ ' --entrypoint "\' gunicorn -b :$PORT --disable-redirect-access-to-syslog main:app\' "' )
367
120
ap .add_argument ('--port' , type = int , default = 8080 ,
368
121
help = 'port to which we bind the application' )
369
122
ap .add_argument ('--gunicorn_port' , type = int , default = 8090 ,
@@ -375,7 +128,7 @@ def main():
375
128
ap .add_argument ('--timeout' , type = int , default = 60 ,
376
129
help = 'Time is seconds before gunicorn abort a request' )
377
130
ap .add_argument ('-V' , '--version' , action = 'version' ,
378
- version = f'%(prog)s { __version__ } ' )
131
+ version = f'%(prog)s { __version__ } ' )
379
132
380
133
ap .add_argument (
381
134
'--env_var' , metavar = "KEY=VALUE" , nargs = "*" ,
@@ -392,18 +145,16 @@ def main():
392
145
with open (app_folder / args .appyaml , "r" ) as f :
393
146
app_yaml = yaml .load (f , Loader = yaml .Loader )
394
147
395
- set_env_vars (args .app_id , args , app_yaml )
396
- patch_gunicorn ()
397
-
148
+ utils .set_env_vars (args .app_id , args , app_yaml )
149
+ utils .patch_gunicorn ()
398
150
# Check for correct runtime
399
151
current_runtime = f"python{ sys .version_info .major } { sys .version_info .minor } "
400
152
app_runtime = app_yaml ["runtime" ]
401
153
assert app_runtime == current_runtime , f"app.yaml specifies { app_runtime } but you're on { current_runtime } , please correct this."
402
154
403
- if "WERKZEUG_RUN_MAIN" in os .environ and os . environ [ "WERKZEUG_RUN_MAIN" ] :
155
+ if os .environ . get ( "WERKZEUG_RUN_MAIN" ) :
404
156
# only start subprocesses wenn reloader starts
405
157
406
-
407
158
start_gunicorn (args , app_yaml , app_folder )
408
159
409
160
start_server (args .host , args .port , args .gunicorn_port , app_folder , app_yaml ,
0 commit comments