Skip to content

Commit e444bcc

Browse files
committed
Reformart files and edit readme
1 parent 6f4692c commit e444bcc

File tree

8 files changed

+275
-268
lines changed

8 files changed

+275
-268
lines changed

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ The static paths are stored in an app.yaml as handlers and are therefore compati
77
## Getting Started
88
Take a look at the example folder. Here you can find a start.sh which starts the App Server on port 8080.
99

10-
### External APIs
11-
App Server allows app code to connect to external APIs, eg [Google Cloud Datastore](https://cloud.google.com/datastore/docs/), like normal. To use the [local datastore emulator](https://cloud.google.com/datastore/docs/tools/datastore-emulator), first start it, then in a separate shell [set the appropriate environment variables](https://cloud.google.com/datastore/docs/tools/datastore-emulator#setting_environment_variables) (notably `DATASTORE_EMULATOR_HOST` and `DATASTORE_DATASET`) to point your app to it before you run App Server.
1210

1311
### Dependencies
1412
The app server dependents on the following packages

src/app_server/__init__.py

Lines changed: 17 additions & 266 deletions
Original file line numberDiff line numberDiff line change
@@ -1,234 +1,24 @@
11
import argparse
2-
import logging
3-
import mimetypes
42
import os
5-
from pathlib import Path
63
import re
74
import subprocess
85
import sys
9-
import time
10-
import typing as t
6+
from pathlib import Path
117

128
import yaml
139
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
9211

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
9818

99-
def __call__(self, environ, start_response):
100-
return self.wsgi_app(environ, start_response)
19+
__version__ = "0.1.0"
10120

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 = []
23222

23323

23424
def start_server(
@@ -241,9 +31,8 @@ def start_server(
24131
protocol: str = "http",
24232
) -> None:
24333
"""use the dispatcherMiddleware to connect SharedDataMiddleware and ProxyMiddleware with the wrapping app."""
244-
app = WrappingApp({})
34+
app = AppWrapper()
24535
apps = {}
246-
24736
# make shared middlewares for static files as configured in app.yaml
24837
for route in app_yaml["handlers"]:
24938
if path := route.get("static_dir"):
@@ -269,43 +58,7 @@ def start_server(
26958
app.wsgi_app = Dispatcher(app.wsgi_app, apps)
27059

27160
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)
30962

31063

31164
def start_gunicorn(
@@ -363,7 +116,7 @@ def main():
363116
help='The entrypoint is the basic gunicorn command. By default, it\'s taken from app.yaml. '
364117
'This parameter can be used to set a different entrypoint. '
365118
'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\'"')
367120
ap.add_argument('--port', type=int, default=8080,
368121
help='port to which we bind the application')
369122
ap.add_argument('--gunicorn_port', type=int, default=8090,
@@ -375,7 +128,7 @@ def main():
375128
ap.add_argument('--timeout', type=int, default=60,
376129
help='Time is seconds before gunicorn abort a request')
377130
ap.add_argument('-V', '--version', action='version',
378-
version=f'%(prog)s {__version__}' )
131+
version=f'%(prog)s {__version__}')
379132

380133
ap.add_argument(
381134
'--env_var', metavar="KEY=VALUE", nargs="*",
@@ -392,18 +145,16 @@ def main():
392145
with open(app_folder / args.appyaml, "r") as f:
393146
app_yaml = yaml.load(f, Loader=yaml.Loader)
394147

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()
398150
# Check for correct runtime
399151
current_runtime = f"python{sys.version_info.major}{sys.version_info.minor}"
400152
app_runtime = app_yaml["runtime"]
401153
assert app_runtime == current_runtime, f"app.yaml specifies {app_runtime} but you're on {current_runtime}, please correct this."
402154

403-
if "WERKZEUG_RUN_MAIN" in os.environ and os.environ["WERKZEUG_RUN_MAIN"]:
155+
if os.environ.get("WERKZEUG_RUN_MAIN"):
404156
# only start subprocesses wenn reloader starts
405157

406-
407158
start_gunicorn(args, app_yaml, app_folder)
408159

409160
start_server(args.host, args.port, args.gunicorn_port, app_folder, app_yaml,

src/app_server/app_wrapper.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from werkzeug.wrappers import Request, Response
2+
class AppWrapper:
3+
"""simple wrapping app"""
4+
@staticmethod
5+
def wsgi_app(environ, start_response):
6+
request = Request(environ)
7+
response = Response(f'Path not found or invalid: {request.path}',
8+
status=404)
9+
return response(environ, start_response)
10+
11+
def __call__(self, environ, start_response):
12+
return self.wsgi_app(environ, start_response)

src/app_server/dispatcher.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import re
2+
from werkzeug.middleware.dispatcher import DispatcherMiddleware
3+
4+
5+
class Dispatcher(DispatcherMiddleware):
6+
"""use regex to find a matching route"""
7+
8+
def __call__(self, environ, start_response):
9+
app = self.mounts["/"]
10+
11+
for route, _app in self.mounts.items():
12+
if re.match(route, environ["PATH_INFO"]):
13+
app = _app
14+
break
15+
return app(environ, start_response)

0 commit comments

Comments
 (0)