cavisson-pythonagent 0.0.1__py3-none-any.whl
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.
- cavisson_pythonagent-0.0.1.dist-info/METADATA +32 -0
- cavisson_pythonagent-0.0.1.dist-info/RECORD +180 -0
- cavisson_pythonagent-0.0.1.dist-info/WHEEL +5 -0
- cavisson_pythonagent-0.0.1.dist-info/licenses/LICENSE +19 -0
- cavisson_pythonagent-0.0.1.dist-info/top_level.txt +1 -0
- pythonagent/__init__.py +22 -0
- pythonagent/agent/__init__.py +219 -0
- pythonagent/agent/internal/__init__.py +1 -0
- pythonagent/agent/internal/agent.py +1651 -0
- pythonagent/agent/internal/framesinfo.py +152 -0
- pythonagent/agent/internal/heap_dump.py +39 -0
- pythonagent/agent/internal/intercept_module.py +70 -0
- pythonagent/agent/internal/logs.py +122 -0
- pythonagent/agent/internal/metadata/__init__.py +0 -0
- pythonagent/agent/internal/metadata/agent_meta_data.py +276 -0
- pythonagent/agent/internal/proc_compat.py +75 -0
- pythonagent/agent/internal/profile.py +47 -0
- pythonagent/agent/internal/provider.py +83 -0
- pythonagent/agent/internal/thread_dump.py +85 -0
- pythonagent/agent/internal/udp.py +245 -0
- pythonagent/agent/internal/udp_message.py +800 -0
- pythonagent/agent/probes/Instrumentation/__init__.py +340 -0
- pythonagent/agent/probes/Instrumentation/find.py +243 -0
- pythonagent/agent/probes/Instrumentation/module_version_resolver.py +155 -0
- pythonagent/agent/probes/Instrumentation/new_parser.py +64 -0
- pythonagent/agent/probes/Instrumentation/parser.py +129 -0
- pythonagent/agent/probes/__init__.py +219 -0
- pythonagent/agent/probes/base.py +303 -0
- pythonagent/agent/probes/cache/__init__.py +51 -0
- pythonagent/agent/probes/cache/redis.py +119 -0
- pythonagent/agent/probes/cache/redis_asyncio.py +83 -0
- pythonagent/agent/probes/coroutines/__init__.py +1 -0
- pythonagent/agent/probes/coroutines/asyncio.py +63 -0
- pythonagent/agent/probes/elasticdb/__init__.py +7 -0
- pythonagent/agent/probes/elasticdb/aelastic.py +54 -0
- pythonagent/agent/probes/frameworks/__init__.py +27 -0
- pythonagent/agent/probes/frameworks/agentprofiler.py +105 -0
- pythonagent/agent/probes/frameworks/aiohttp_web.py +155 -0
- pythonagent/agent/probes/frameworks/aisess.py +151 -0
- pythonagent/agent/probes/frameworks/asgi.py +340 -0
- pythonagent/agent/probes/frameworks/bottle.py +27 -0
- pythonagent/agent/probes/frameworks/cherry.py +25 -0
- pythonagent/agent/probes/frameworks/django.py +128 -0
- pythonagent/agent/probes/frameworks/falcon.py +21 -0
- pythonagent/agent/probes/frameworks/fastapi.py +35 -0
- pythonagent/agent/probes/frameworks/flask.py +30 -0
- pythonagent/agent/probes/frameworks/pyramid.py +56 -0
- pythonagent/agent/probes/frameworks/test.py +108 -0
- pythonagent/agent/probes/frameworks/tornado_async_web.py +117 -0
- pythonagent/agent/probes/frameworks/tornado_web.py +133 -0
- pythonagent/agent/probes/frameworks/wsgi.py +353 -0
- pythonagent/agent/probes/grpc/__init__.py +76 -0
- pythonagent/agent/probes/grpc/client_interceptor.py +132 -0
- pythonagent/agent/probes/grpc/server_interceptor.py +129 -0
- pythonagent/agent/probes/havoc/__init__.py +0 -0
- pythonagent/agent/probes/havoc/custom_memory_stress.py +186 -0
- pythonagent/agent/probes/havoc/custom_thread_stress.py +187 -0
- pythonagent/agent/probes/havoc/havoc_constants.py +218 -0
- pythonagent/agent/probes/havoc/havoc_manager.py +981 -0
- pythonagent/agent/probes/http/__init__.py +49 -0
- pythonagent/agent/probes/http/aiohttp_client.py +59 -0
- pythonagent/agent/probes/http/boto.py +12 -0
- pythonagent/agent/probes/http/httplib.py +110 -0
- pythonagent/agent/probes/http/httpx_client.py +116 -0
- pythonagent/agent/probes/http/requests.py +15 -0
- pythonagent/agent/probes/http/tornado_httpclient.py +85 -0
- pythonagent/agent/probes/http/urllib3.py +16 -0
- pythonagent/agent/probes/langchain/__init__.py +21 -0
- pythonagent/agent/probes/langchain/base_tool.py +95 -0
- pythonagent/agent/probes/langchain/langchain_community.py +136 -0
- pythonagent/agent/probes/langchain/langchain_core.py +32 -0
- pythonagent/agent/probes/langchain/langchain_openai.py +110 -0
- pythonagent/agent/probes/logging/__init__.py +106 -0
- pythonagent/agent/probes/message_brokers/__init__.py +4 -0
- pythonagent/agent/probes/message_brokers/pika.py +126 -0
- pythonagent/agent/probes/mongodb/__init__.py +6 -0
- pythonagent/agent/probes/mongodb/pymongo.py +286 -0
- pythonagent/agent/probes/openai/__init__.py +3 -0
- pythonagent/agent/probes/openai/openai.py +797 -0
- pythonagent/agent/probes/span.py +101 -0
- pythonagent/agent/probes/sql/__init__.py +13 -0
- pythonagent/agent/probes/sql/botocores3.py +51 -0
- pythonagent/agent/probes/sql/dbapi.py +285 -0
- pythonagent/agent/probes/sql/dynamodb.py +90 -0
- pythonagent/agent/probes/sql/mysql_connector.py +24 -0
- pythonagent/agent/probes/sql/mysql_connector_cext.py +24 -0
- pythonagent/agent/probes/sql/mysqldb.py +43 -0
- pythonagent/agent/probes/sql/psycopg2.py +174 -0
- pythonagent/agent/probes/sql/pymysql.py +25 -0
- pythonagent/bootstrap/__init__.py +0 -0
- pythonagent/bootstrap/cav_gunicorn.py +26 -0
- pythonagent/bootstrap/cavagent_lambda_wrapper.py +291 -0
- pythonagent/bootstrap/run.py +47 -0
- pythonagent/bootstrap/sitecustomize.py +287 -0
- pythonagent/cavisson/netdiagnostics/CavAgent/instrumentationprofile.json +26 -0
- pythonagent/cavisson/netdiagnostics/CavAgent/interceptor_points.txt +29 -0
- pythonagent/cavisson/netdiagnostics/python/CavAgent/instrumentationprofile.json +42 -0
- pythonagent/cavisson/netdiagnostics/python/CavAgent/interceptor_points.txt +29 -0
- pythonagent/cavisson/netdiagnostics/python/config/ndsettings.conf +6 -0
- pythonagent/config.py +279 -0
- pythonagent/find.py +72 -0
- pythonagent/find_mod_cls_name.py +54 -0
- pythonagent/lang.py +131 -0
- pythonagent/lib.py +91 -0
- pythonagent/main/__init__.py +0 -0
- pythonagent/main/pytrace/__init__.py +79 -0
- pythonagent/main/pytrace/commands/__init__.py +0 -0
- pythonagent/main/pytrace/commands/auto_discovery.py +25 -0
- pythonagent/main/pytrace/commands/run.py +401 -0
- pythonagent/main/pytrace/pytrace.py +133 -0
- pythonagent/main/wsgi.py +6 -0
- pythonagent/main.py +27 -0
- pythonagent/run.py +46 -0
- pythonagent/sqins.py +8 -0
- pythonagent/test.py +73 -0
- pythonagent/utils.py +168 -0
- pythonagent/vendor/__init__.py +0 -0
- pythonagent/vendor/pympler/__init__.py +1 -0
- pythonagent/vendor/pympler/asizeof.py +2810 -0
- pythonagent/vendor/pympler/charts.py +62 -0
- pythonagent/vendor/pympler/classtracker.py +590 -0
- pythonagent/vendor/pympler/classtracker_stats.py +780 -0
- pythonagent/vendor/pympler/garbagegraph.py +80 -0
- pythonagent/vendor/pympler/mprofile.py +97 -0
- pythonagent/vendor/pympler/muppy.py +275 -0
- pythonagent/vendor/pympler/panels.py +115 -0
- pythonagent/vendor/pympler/process.py +238 -0
- pythonagent/vendor/pympler/py.typed +0 -0
- pythonagent/vendor/pympler/refbrowser.py +451 -0
- pythonagent/vendor/pympler/refgraph.py +350 -0
- pythonagent/vendor/pympler/summary.py +321 -0
- pythonagent/vendor/pympler/tracker.py +267 -0
- pythonagent/vendor/pympler/util/__init__.py +0 -0
- pythonagent/vendor/pympler/util/bottle.py +3809 -0
- pythonagent/vendor/pympler/util/compat.py +23 -0
- pythonagent/vendor/pympler/util/stringutils.py +77 -0
- pythonagent/vendor/pympler/web.py +346 -0
- pythonagent/vendor/werkzeug/__init__.py +20 -0
- pythonagent/vendor/werkzeug/_compat.py +228 -0
- pythonagent/vendor/werkzeug/_internal.py +473 -0
- pythonagent/vendor/werkzeug/_reloader.py +341 -0
- pythonagent/vendor/werkzeug/datastructures.py +3120 -0
- pythonagent/vendor/werkzeug/debug/__init__.py +498 -0
- pythonagent/vendor/werkzeug/debug/console.py +218 -0
- pythonagent/vendor/werkzeug/debug/repr.py +297 -0
- pythonagent/vendor/werkzeug/debug/tbtools.py +628 -0
- pythonagent/vendor/werkzeug/exceptions.py +829 -0
- pythonagent/vendor/werkzeug/filesystem.py +64 -0
- pythonagent/vendor/werkzeug/formparser.py +584 -0
- pythonagent/vendor/werkzeug/http.py +1307 -0
- pythonagent/vendor/werkzeug/local.py +420 -0
- pythonagent/vendor/werkzeug/middleware/__init__.py +25 -0
- pythonagent/vendor/werkzeug/middleware/dispatcher.py +66 -0
- pythonagent/vendor/werkzeug/middleware/http_proxy.py +219 -0
- pythonagent/vendor/werkzeug/middleware/lint.py +408 -0
- pythonagent/vendor/werkzeug/middleware/profiler.py +132 -0
- pythonagent/vendor/werkzeug/middleware/proxy_fix.py +169 -0
- pythonagent/vendor/werkzeug/middleware/shared_data.py +293 -0
- pythonagent/vendor/werkzeug/posixemulation.py +117 -0
- pythonagent/vendor/werkzeug/routing.py +2210 -0
- pythonagent/vendor/werkzeug/security.py +249 -0
- pythonagent/vendor/werkzeug/serving.py +1117 -0
- pythonagent/vendor/werkzeug/test.py +1123 -0
- pythonagent/vendor/werkzeug/testapp.py +241 -0
- pythonagent/vendor/werkzeug/urls.py +1138 -0
- pythonagent/vendor/werkzeug/useragents.py +202 -0
- pythonagent/vendor/werkzeug/utils.py +778 -0
- pythonagent/vendor/werkzeug/wrappers/__init__.py +36 -0
- pythonagent/vendor/werkzeug/wrappers/accept.py +50 -0
- pythonagent/vendor/werkzeug/wrappers/auth.py +33 -0
- pythonagent/vendor/werkzeug/wrappers/base_request.py +673 -0
- pythonagent/vendor/werkzeug/wrappers/base_response.py +700 -0
- pythonagent/vendor/werkzeug/wrappers/common_descriptors.py +341 -0
- pythonagent/vendor/werkzeug/wrappers/cors.py +100 -0
- pythonagent/vendor/werkzeug/wrappers/etag.py +304 -0
- pythonagent/vendor/werkzeug/wrappers/json.py +145 -0
- pythonagent/vendor/werkzeug/wrappers/request.py +49 -0
- pythonagent/vendor/werkzeug/wrappers/response.py +84 -0
- pythonagent/vendor/werkzeug/wrappers/user_agent.py +14 -0
- pythonagent/vendor/werkzeug/wsgi.py +1000 -0
|
@@ -0,0 +1,1123 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
werkzeug.test
|
|
4
|
+
~~~~~~~~~~~~~
|
|
5
|
+
|
|
6
|
+
This module implements a client to WSGI applications for testing.
|
|
7
|
+
|
|
8
|
+
:copyright: 2007 Pallets
|
|
9
|
+
:license: BSD-3-Clause
|
|
10
|
+
"""
|
|
11
|
+
import mimetypes
|
|
12
|
+
import sys
|
|
13
|
+
from io import BytesIO
|
|
14
|
+
from itertools import chain
|
|
15
|
+
from random import random
|
|
16
|
+
from tempfile import TemporaryFile
|
|
17
|
+
from time import time
|
|
18
|
+
|
|
19
|
+
from ._compat import iteritems
|
|
20
|
+
from ._compat import iterlists
|
|
21
|
+
from ._compat import itervalues
|
|
22
|
+
from ._compat import make_literal_wrapper
|
|
23
|
+
from ._compat import reraise
|
|
24
|
+
from ._compat import string_types
|
|
25
|
+
from ._compat import text_type
|
|
26
|
+
from ._compat import to_bytes
|
|
27
|
+
from ._compat import wsgi_encoding_dance
|
|
28
|
+
from ._internal import _get_environ
|
|
29
|
+
from .datastructures import CallbackDict
|
|
30
|
+
from .datastructures import CombinedMultiDict
|
|
31
|
+
from .datastructures import EnvironHeaders
|
|
32
|
+
from .datastructures import FileMultiDict
|
|
33
|
+
from .datastructures import Headers
|
|
34
|
+
from .datastructures import MultiDict
|
|
35
|
+
from .http import dump_cookie
|
|
36
|
+
from .http import dump_options_header
|
|
37
|
+
from .http import parse_options_header
|
|
38
|
+
from .urls import iri_to_uri
|
|
39
|
+
from .urls import url_encode
|
|
40
|
+
from .urls import url_fix
|
|
41
|
+
from .urls import url_parse
|
|
42
|
+
from .urls import url_unparse
|
|
43
|
+
from .urls import url_unquote
|
|
44
|
+
from .utils import get_content_type
|
|
45
|
+
from .wrappers import BaseRequest
|
|
46
|
+
from .wsgi import ClosingIterator
|
|
47
|
+
from .wsgi import get_current_url
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
from urllib.request import Request as U2Request
|
|
51
|
+
except ImportError:
|
|
52
|
+
from urllib2 import Request as U2Request
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
from http.cookiejar import CookieJar
|
|
56
|
+
except ImportError:
|
|
57
|
+
from cookielib import CookieJar
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def stream_encode_multipart(
|
|
61
|
+
values, use_tempfile=True, threshold=1024 * 500, boundary=None, charset="utf-8"
|
|
62
|
+
):
|
|
63
|
+
"""Encode a dict of values (either strings or file descriptors or
|
|
64
|
+
:class:`FileStorage` objects.) into a multipart encoded string stored
|
|
65
|
+
in a file descriptor.
|
|
66
|
+
"""
|
|
67
|
+
if boundary is None:
|
|
68
|
+
boundary = "---------------WerkzeugFormPart_%s%s" % (time(), random())
|
|
69
|
+
_closure = [BytesIO(), 0, False]
|
|
70
|
+
|
|
71
|
+
if use_tempfile:
|
|
72
|
+
|
|
73
|
+
def write_binary(string):
|
|
74
|
+
stream, total_length, on_disk = _closure
|
|
75
|
+
if on_disk:
|
|
76
|
+
stream.write(string)
|
|
77
|
+
else:
|
|
78
|
+
length = len(string)
|
|
79
|
+
if length + _closure[1] <= threshold:
|
|
80
|
+
stream.write(string)
|
|
81
|
+
else:
|
|
82
|
+
new_stream = TemporaryFile("wb+")
|
|
83
|
+
new_stream.write(stream.getvalue())
|
|
84
|
+
new_stream.write(string)
|
|
85
|
+
_closure[0] = new_stream
|
|
86
|
+
_closure[2] = True
|
|
87
|
+
_closure[1] = total_length + length
|
|
88
|
+
|
|
89
|
+
else:
|
|
90
|
+
write_binary = _closure[0].write
|
|
91
|
+
|
|
92
|
+
def write(string):
|
|
93
|
+
write_binary(string.encode(charset))
|
|
94
|
+
|
|
95
|
+
if not isinstance(values, MultiDict):
|
|
96
|
+
values = MultiDict(values)
|
|
97
|
+
|
|
98
|
+
for key, values in iterlists(values):
|
|
99
|
+
for value in values:
|
|
100
|
+
write('--%s\r\nContent-Disposition: form-data; name="%s"' % (boundary, key))
|
|
101
|
+
reader = getattr(value, "read", None)
|
|
102
|
+
if reader is not None:
|
|
103
|
+
filename = getattr(value, "filename", getattr(value, "name", None))
|
|
104
|
+
content_type = getattr(value, "content_type", None)
|
|
105
|
+
if content_type is None:
|
|
106
|
+
content_type = (
|
|
107
|
+
filename
|
|
108
|
+
and mimetypes.guess_type(filename)[0]
|
|
109
|
+
or "application/octet-stream"
|
|
110
|
+
)
|
|
111
|
+
if filename is not None:
|
|
112
|
+
write('; filename="%s"\r\n' % filename)
|
|
113
|
+
else:
|
|
114
|
+
write("\r\n")
|
|
115
|
+
write("Content-Type: %s\r\n\r\n" % content_type)
|
|
116
|
+
while 1:
|
|
117
|
+
chunk = reader(16384)
|
|
118
|
+
if not chunk:
|
|
119
|
+
break
|
|
120
|
+
write_binary(chunk)
|
|
121
|
+
else:
|
|
122
|
+
if not isinstance(value, string_types):
|
|
123
|
+
value = str(value)
|
|
124
|
+
|
|
125
|
+
value = to_bytes(value, charset)
|
|
126
|
+
write("\r\n\r\n")
|
|
127
|
+
write_binary(value)
|
|
128
|
+
write("\r\n")
|
|
129
|
+
write("--%s--\r\n" % boundary)
|
|
130
|
+
|
|
131
|
+
length = int(_closure[0].tell())
|
|
132
|
+
_closure[0].seek(0)
|
|
133
|
+
return _closure[0], length, boundary
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def encode_multipart(values, boundary=None, charset="utf-8"):
|
|
137
|
+
"""Like `stream_encode_multipart` but returns a tuple in the form
|
|
138
|
+
(``boundary``, ``data``) where data is a bytestring.
|
|
139
|
+
"""
|
|
140
|
+
stream, length, boundary = stream_encode_multipart(
|
|
141
|
+
values, use_tempfile=False, boundary=boundary, charset=charset
|
|
142
|
+
)
|
|
143
|
+
return boundary, stream.read()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class _TestCookieHeaders(object):
|
|
147
|
+
|
|
148
|
+
"""A headers adapter for cookielib
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
def __init__(self, headers):
|
|
152
|
+
self.headers = headers
|
|
153
|
+
|
|
154
|
+
def getheaders(self, name):
|
|
155
|
+
headers = []
|
|
156
|
+
name = name.lower()
|
|
157
|
+
for k, v in self.headers:
|
|
158
|
+
if k.lower() == name:
|
|
159
|
+
headers.append(v)
|
|
160
|
+
return headers
|
|
161
|
+
|
|
162
|
+
def get_all(self, name, default=None):
|
|
163
|
+
rv = []
|
|
164
|
+
for k, v in self.headers:
|
|
165
|
+
if k.lower() == name.lower():
|
|
166
|
+
rv.append(v)
|
|
167
|
+
return rv or default or []
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class _TestCookieResponse(object):
|
|
171
|
+
|
|
172
|
+
"""Something that looks like a httplib.HTTPResponse, but is actually just an
|
|
173
|
+
adapter for our test responses to make them available for cookielib.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
def __init__(self, headers):
|
|
177
|
+
self.headers = _TestCookieHeaders(headers)
|
|
178
|
+
|
|
179
|
+
def info(self):
|
|
180
|
+
return self.headers
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class _TestCookieJar(CookieJar):
|
|
184
|
+
|
|
185
|
+
"""A cookielib.CookieJar modified to inject and read cookie headers from
|
|
186
|
+
and to wsgi environments, and wsgi application responses.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
def inject_wsgi(self, environ):
|
|
190
|
+
"""Inject the cookies as client headers into the server's wsgi
|
|
191
|
+
environment.
|
|
192
|
+
"""
|
|
193
|
+
cvals = ["%s=%s" % (c.name, c.value) for c in self]
|
|
194
|
+
|
|
195
|
+
if cvals:
|
|
196
|
+
environ["HTTP_COOKIE"] = "; ".join(cvals)
|
|
197
|
+
else:
|
|
198
|
+
environ.pop("HTTP_COOKIE", None)
|
|
199
|
+
|
|
200
|
+
def extract_wsgi(self, environ, headers):
|
|
201
|
+
"""Extract the server's set-cookie headers as cookies into the
|
|
202
|
+
cookie jar.
|
|
203
|
+
"""
|
|
204
|
+
self.extract_cookies(
|
|
205
|
+
_TestCookieResponse(headers), U2Request(get_current_url(environ))
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _iter_data(data):
|
|
210
|
+
"""Iterates over a `dict` or :class:`MultiDict` yielding all keys and
|
|
211
|
+
values.
|
|
212
|
+
This is used to iterate over the data passed to the
|
|
213
|
+
:class:`EnvironBuilder`.
|
|
214
|
+
"""
|
|
215
|
+
if isinstance(data, MultiDict):
|
|
216
|
+
for key, values in iterlists(data):
|
|
217
|
+
for value in values:
|
|
218
|
+
yield key, value
|
|
219
|
+
else:
|
|
220
|
+
for key, values in iteritems(data):
|
|
221
|
+
if isinstance(values, list):
|
|
222
|
+
for value in values:
|
|
223
|
+
yield key, value
|
|
224
|
+
else:
|
|
225
|
+
yield key, values
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class EnvironBuilder(object):
|
|
229
|
+
"""This class can be used to conveniently create a WSGI environment
|
|
230
|
+
for testing purposes. It can be used to quickly create WSGI environments
|
|
231
|
+
or request objects from arbitrary data.
|
|
232
|
+
|
|
233
|
+
The signature of this class is also used in some other places as of
|
|
234
|
+
Werkzeug 0.5 (:func:`create_environ`, :meth:`BaseResponse.from_values`,
|
|
235
|
+
:meth:`Client.open`). Because of this most of the functionality is
|
|
236
|
+
available through the constructor alone.
|
|
237
|
+
|
|
238
|
+
Files and regular form data can be manipulated independently of each
|
|
239
|
+
other with the :attr:`form` and :attr:`files` attributes, but are
|
|
240
|
+
passed with the same argument to the constructor: `data`.
|
|
241
|
+
|
|
242
|
+
`data` can be any of these values:
|
|
243
|
+
|
|
244
|
+
- a `str` or `bytes` object: The object is converted into an
|
|
245
|
+
:attr:`input_stream`, the :attr:`content_length` is set and you have to
|
|
246
|
+
provide a :attr:`content_type`.
|
|
247
|
+
- a `dict` or :class:`MultiDict`: The keys have to be strings. The values
|
|
248
|
+
have to be either any of the following objects, or a list of any of the
|
|
249
|
+
following objects:
|
|
250
|
+
|
|
251
|
+
- a :class:`file`-like object: These are converted into
|
|
252
|
+
:class:`FileStorage` objects automatically.
|
|
253
|
+
- a `tuple`: The :meth:`~FileMultiDict.add_file` method is called
|
|
254
|
+
with the key and the unpacked `tuple` items as positional
|
|
255
|
+
arguments.
|
|
256
|
+
- a `str`: The string is set as form data for the associated key.
|
|
257
|
+
- a file-like object: The object content is loaded in memory and then
|
|
258
|
+
handled like a regular `str` or a `bytes`.
|
|
259
|
+
|
|
260
|
+
:param path: the path of the request. In the WSGI environment this will
|
|
261
|
+
end up as `PATH_INFO`. If the `query_string` is not defined
|
|
262
|
+
and there is a question mark in the `path` everything after
|
|
263
|
+
it is used as query string.
|
|
264
|
+
:param base_url: the base URL is a URL that is used to extract the WSGI
|
|
265
|
+
URL scheme, host (server name + server port) and the
|
|
266
|
+
script root (`SCRIPT_NAME`).
|
|
267
|
+
:param query_string: an optional string or dict with URL parameters.
|
|
268
|
+
:param method: the HTTP method to use, defaults to `GET`.
|
|
269
|
+
:param input_stream: an optional input stream. Do not specify this and
|
|
270
|
+
`data`. As soon as an input stream is set you can't
|
|
271
|
+
modify :attr:`args` and :attr:`files` unless you
|
|
272
|
+
set the :attr:`input_stream` to `None` again.
|
|
273
|
+
:param content_type: The content type for the request. As of 0.5 you
|
|
274
|
+
don't have to provide this when specifying files
|
|
275
|
+
and form data via `data`.
|
|
276
|
+
:param content_length: The content length for the request. You don't
|
|
277
|
+
have to specify this when providing data via
|
|
278
|
+
`data`.
|
|
279
|
+
:param errors_stream: an optional error stream that is used for
|
|
280
|
+
`wsgi.errors`. Defaults to :data:`stderr`.
|
|
281
|
+
:param multithread: controls `wsgi.multithread`. Defaults to `False`.
|
|
282
|
+
:param multiprocess: controls `wsgi.multiprocess`. Defaults to `False`.
|
|
283
|
+
:param run_once: controls `wsgi.run_once`. Defaults to `False`.
|
|
284
|
+
:param headers: an optional list or :class:`Headers` object of headers.
|
|
285
|
+
:param data: a string or dict of form data or a file-object.
|
|
286
|
+
See explanation above.
|
|
287
|
+
:param json: An object to be serialized and assigned to ``data``.
|
|
288
|
+
Defaults the content type to ``"application/json"``.
|
|
289
|
+
Serialized with the function assigned to :attr:`json_dumps`.
|
|
290
|
+
:param environ_base: an optional dict of environment defaults.
|
|
291
|
+
:param environ_overrides: an optional dict of environment overrides.
|
|
292
|
+
:param charset: the charset used to encode unicode data.
|
|
293
|
+
|
|
294
|
+
.. versionadded:: 0.15
|
|
295
|
+
The ``json`` param and :meth:`json_dumps` method.
|
|
296
|
+
|
|
297
|
+
.. versionadded:: 0.15
|
|
298
|
+
The environ has keys ``REQUEST_URI`` and ``RAW_URI`` containing
|
|
299
|
+
the path before perecent-decoding. This is not part of the WSGI
|
|
300
|
+
PEP, but many WSGI servers include it.
|
|
301
|
+
|
|
302
|
+
.. versionchanged:: 0.6
|
|
303
|
+
``path`` and ``base_url`` can now be unicode strings that are
|
|
304
|
+
encoded with :func:`iri_to_uri`.
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
#: the server protocol to use. defaults to HTTP/1.1
|
|
308
|
+
server_protocol = "HTTP/1.1"
|
|
309
|
+
|
|
310
|
+
#: the wsgi version to use. defaults to (1, 0)
|
|
311
|
+
wsgi_version = (1, 0)
|
|
312
|
+
|
|
313
|
+
#: the default request class for :meth:`get_request`
|
|
314
|
+
request_class = BaseRequest
|
|
315
|
+
|
|
316
|
+
import json
|
|
317
|
+
|
|
318
|
+
#: The serialization function used when ``json`` is passed.
|
|
319
|
+
json_dumps = staticmethod(json.dumps)
|
|
320
|
+
del json
|
|
321
|
+
|
|
322
|
+
def __init__(
|
|
323
|
+
self,
|
|
324
|
+
path="/",
|
|
325
|
+
base_url=None,
|
|
326
|
+
query_string=None,
|
|
327
|
+
method="GET",
|
|
328
|
+
input_stream=None,
|
|
329
|
+
content_type=None,
|
|
330
|
+
content_length=None,
|
|
331
|
+
errors_stream=None,
|
|
332
|
+
multithread=False,
|
|
333
|
+
multiprocess=False,
|
|
334
|
+
run_once=False,
|
|
335
|
+
headers=None,
|
|
336
|
+
data=None,
|
|
337
|
+
environ_base=None,
|
|
338
|
+
environ_overrides=None,
|
|
339
|
+
charset="utf-8",
|
|
340
|
+
mimetype=None,
|
|
341
|
+
json=None,
|
|
342
|
+
):
|
|
343
|
+
path_s = make_literal_wrapper(path)
|
|
344
|
+
if query_string is not None and path_s("?") in path:
|
|
345
|
+
raise ValueError("Query string is defined in the path and as an argument")
|
|
346
|
+
if query_string is None and path_s("?") in path:
|
|
347
|
+
path, query_string = path.split(path_s("?"), 1)
|
|
348
|
+
self.charset = charset
|
|
349
|
+
self.path = iri_to_uri(path)
|
|
350
|
+
if base_url is not None:
|
|
351
|
+
base_url = url_fix(iri_to_uri(base_url, charset), charset)
|
|
352
|
+
self.base_url = base_url
|
|
353
|
+
if isinstance(query_string, (bytes, text_type)):
|
|
354
|
+
self.query_string = query_string
|
|
355
|
+
else:
|
|
356
|
+
if query_string is None:
|
|
357
|
+
query_string = MultiDict()
|
|
358
|
+
elif not isinstance(query_string, MultiDict):
|
|
359
|
+
query_string = MultiDict(query_string)
|
|
360
|
+
self.args = query_string
|
|
361
|
+
self.method = method
|
|
362
|
+
if headers is None:
|
|
363
|
+
headers = Headers()
|
|
364
|
+
elif not isinstance(headers, Headers):
|
|
365
|
+
headers = Headers(headers)
|
|
366
|
+
self.headers = headers
|
|
367
|
+
if content_type is not None:
|
|
368
|
+
self.content_type = content_type
|
|
369
|
+
if errors_stream is None:
|
|
370
|
+
errors_stream = sys.stderr
|
|
371
|
+
self.errors_stream = errors_stream
|
|
372
|
+
self.multithread = multithread
|
|
373
|
+
self.multiprocess = multiprocess
|
|
374
|
+
self.run_once = run_once
|
|
375
|
+
self.environ_base = environ_base
|
|
376
|
+
self.environ_overrides = environ_overrides
|
|
377
|
+
self.input_stream = input_stream
|
|
378
|
+
self.content_length = content_length
|
|
379
|
+
self.closed = False
|
|
380
|
+
|
|
381
|
+
if json is not None:
|
|
382
|
+
if data is not None:
|
|
383
|
+
raise TypeError("can't provide both json and data")
|
|
384
|
+
|
|
385
|
+
data = self.json_dumps(json)
|
|
386
|
+
|
|
387
|
+
if self.content_type is None:
|
|
388
|
+
self.content_type = "application/json"
|
|
389
|
+
|
|
390
|
+
if data:
|
|
391
|
+
if input_stream is not None:
|
|
392
|
+
raise TypeError("can't provide input stream and data")
|
|
393
|
+
if hasattr(data, "read"):
|
|
394
|
+
data = data.read()
|
|
395
|
+
if isinstance(data, text_type):
|
|
396
|
+
data = data.encode(self.charset)
|
|
397
|
+
if isinstance(data, bytes):
|
|
398
|
+
self.input_stream = BytesIO(data)
|
|
399
|
+
if self.content_length is None:
|
|
400
|
+
self.content_length = len(data)
|
|
401
|
+
else:
|
|
402
|
+
for key, value in _iter_data(data):
|
|
403
|
+
if isinstance(value, (tuple, dict)) or hasattr(value, "read"):
|
|
404
|
+
self._add_file_from_data(key, value)
|
|
405
|
+
else:
|
|
406
|
+
self.form.setlistdefault(key).append(value)
|
|
407
|
+
|
|
408
|
+
if mimetype is not None:
|
|
409
|
+
self.mimetype = mimetype
|
|
410
|
+
|
|
411
|
+
@classmethod
|
|
412
|
+
def from_environ(cls, environ, **kwargs):
|
|
413
|
+
"""Turn an environ dict back into a builder. Any extra kwargs
|
|
414
|
+
override the args extracted from the environ.
|
|
415
|
+
|
|
416
|
+
.. versionadded:: 0.15
|
|
417
|
+
"""
|
|
418
|
+
headers = Headers(EnvironHeaders(environ))
|
|
419
|
+
out = {
|
|
420
|
+
"path": environ["PATH_INFO"],
|
|
421
|
+
"base_url": cls._make_base_url(
|
|
422
|
+
environ["wsgi.url_scheme"], headers.pop("Host"), environ["SCRIPT_NAME"]
|
|
423
|
+
),
|
|
424
|
+
"query_string": environ["QUERY_STRING"],
|
|
425
|
+
"method": environ["REQUEST_METHOD"],
|
|
426
|
+
"input_stream": environ["wsgi.input"],
|
|
427
|
+
"content_type": headers.pop("Content-Type", None),
|
|
428
|
+
"content_length": headers.pop("Content-Length", None),
|
|
429
|
+
"errors_stream": environ["wsgi.errors"],
|
|
430
|
+
"multithread": environ["wsgi.multithread"],
|
|
431
|
+
"multiprocess": environ["wsgi.multiprocess"],
|
|
432
|
+
"run_once": environ["wsgi.run_once"],
|
|
433
|
+
"headers": headers,
|
|
434
|
+
}
|
|
435
|
+
out.update(kwargs)
|
|
436
|
+
return cls(**out)
|
|
437
|
+
|
|
438
|
+
def _add_file_from_data(self, key, value):
|
|
439
|
+
"""Called in the EnvironBuilder to add files from the data dict."""
|
|
440
|
+
if isinstance(value, tuple):
|
|
441
|
+
self.files.add_file(key, *value)
|
|
442
|
+
else:
|
|
443
|
+
self.files.add_file(key, value)
|
|
444
|
+
|
|
445
|
+
@staticmethod
|
|
446
|
+
def _make_base_url(scheme, host, script_root):
|
|
447
|
+
return url_unparse((scheme, host, script_root, "", "")).rstrip("/") + "/"
|
|
448
|
+
|
|
449
|
+
@property
|
|
450
|
+
def base_url(self):
|
|
451
|
+
"""The base URL is used to extract the URL scheme, host name,
|
|
452
|
+
port, and root path.
|
|
453
|
+
"""
|
|
454
|
+
return self._make_base_url(self.url_scheme, self.host, self.script_root)
|
|
455
|
+
|
|
456
|
+
@base_url.setter
|
|
457
|
+
def base_url(self, value):
|
|
458
|
+
if value is None:
|
|
459
|
+
scheme = "http"
|
|
460
|
+
netloc = "localhost"
|
|
461
|
+
script_root = ""
|
|
462
|
+
else:
|
|
463
|
+
scheme, netloc, script_root, qs, anchor = url_parse(value)
|
|
464
|
+
if qs or anchor:
|
|
465
|
+
raise ValueError("base url must not contain a query string or fragment")
|
|
466
|
+
self.script_root = script_root.rstrip("/")
|
|
467
|
+
self.host = netloc
|
|
468
|
+
self.url_scheme = scheme
|
|
469
|
+
|
|
470
|
+
@property
|
|
471
|
+
def content_type(self):
|
|
472
|
+
"""The content type for the request. Reflected from and to
|
|
473
|
+
the :attr:`headers`. Do not set if you set :attr:`files` or
|
|
474
|
+
:attr:`form` for auto detection.
|
|
475
|
+
"""
|
|
476
|
+
ct = self.headers.get("Content-Type")
|
|
477
|
+
if ct is None and not self._input_stream:
|
|
478
|
+
if self._files:
|
|
479
|
+
return "multipart/form-data"
|
|
480
|
+
if self._form:
|
|
481
|
+
return "application/x-www-form-urlencoded"
|
|
482
|
+
return None
|
|
483
|
+
return ct
|
|
484
|
+
|
|
485
|
+
@content_type.setter
|
|
486
|
+
def content_type(self, value):
|
|
487
|
+
if value is None:
|
|
488
|
+
self.headers.pop("Content-Type", None)
|
|
489
|
+
else:
|
|
490
|
+
self.headers["Content-Type"] = value
|
|
491
|
+
|
|
492
|
+
@property
|
|
493
|
+
def mimetype(self):
|
|
494
|
+
"""The mimetype (content type without charset etc.)
|
|
495
|
+
|
|
496
|
+
.. versionadded:: 0.14
|
|
497
|
+
"""
|
|
498
|
+
ct = self.content_type
|
|
499
|
+
return ct.split(";")[0].strip() if ct else None
|
|
500
|
+
|
|
501
|
+
@mimetype.setter
|
|
502
|
+
def mimetype(self, value):
|
|
503
|
+
self.content_type = get_content_type(value, self.charset)
|
|
504
|
+
|
|
505
|
+
@property
|
|
506
|
+
def mimetype_params(self):
|
|
507
|
+
""" The mimetype parameters as dict. For example if the
|
|
508
|
+
content type is ``text/html; charset=utf-8`` the params would be
|
|
509
|
+
``{'charset': 'utf-8'}``.
|
|
510
|
+
|
|
511
|
+
.. versionadded:: 0.14
|
|
512
|
+
"""
|
|
513
|
+
|
|
514
|
+
def on_update(d):
|
|
515
|
+
self.headers["Content-Type"] = dump_options_header(self.mimetype, d)
|
|
516
|
+
|
|
517
|
+
d = parse_options_header(self.headers.get("content-type", ""))[1]
|
|
518
|
+
return CallbackDict(d, on_update)
|
|
519
|
+
|
|
520
|
+
@property
|
|
521
|
+
def content_length(self):
|
|
522
|
+
"""The content length as integer. Reflected from and to the
|
|
523
|
+
:attr:`headers`. Do not set if you set :attr:`files` or
|
|
524
|
+
:attr:`form` for auto detection.
|
|
525
|
+
"""
|
|
526
|
+
return self.headers.get("Content-Length", type=int)
|
|
527
|
+
|
|
528
|
+
@content_length.setter
|
|
529
|
+
def content_length(self, value):
|
|
530
|
+
if value is None:
|
|
531
|
+
self.headers.pop("Content-Length", None)
|
|
532
|
+
else:
|
|
533
|
+
self.headers["Content-Length"] = str(value)
|
|
534
|
+
|
|
535
|
+
def _get_form(self, name, storage):
|
|
536
|
+
"""Common behavior for getting the :attr:`form` and
|
|
537
|
+
:attr:`files` properties.
|
|
538
|
+
|
|
539
|
+
:param name: Name of the internal cached attribute.
|
|
540
|
+
:param storage: Storage class used for the data.
|
|
541
|
+
"""
|
|
542
|
+
if self.input_stream is not None:
|
|
543
|
+
raise AttributeError("an input stream is defined")
|
|
544
|
+
|
|
545
|
+
rv = getattr(self, name)
|
|
546
|
+
|
|
547
|
+
if rv is None:
|
|
548
|
+
rv = storage()
|
|
549
|
+
setattr(self, name, rv)
|
|
550
|
+
|
|
551
|
+
return rv
|
|
552
|
+
|
|
553
|
+
def _set_form(self, name, value):
|
|
554
|
+
"""Common behavior for setting the :attr:`form` and
|
|
555
|
+
:attr:`files` properties.
|
|
556
|
+
|
|
557
|
+
:param name: Name of the internal cached attribute.
|
|
558
|
+
:param value: Value to assign to the attribute.
|
|
559
|
+
"""
|
|
560
|
+
self._input_stream = None
|
|
561
|
+
setattr(self, name, value)
|
|
562
|
+
|
|
563
|
+
@property
|
|
564
|
+
def form(self):
|
|
565
|
+
"""A :class:`MultiDict` of form values."""
|
|
566
|
+
return self._get_form("_form", MultiDict)
|
|
567
|
+
|
|
568
|
+
@form.setter
|
|
569
|
+
def form(self, value):
|
|
570
|
+
self._set_form("_form", value)
|
|
571
|
+
|
|
572
|
+
@property
|
|
573
|
+
def files(self):
|
|
574
|
+
"""A :class:`FileMultiDict` of uploaded files. Use
|
|
575
|
+
:meth:`~FileMultiDict.add_file` to add new files.
|
|
576
|
+
"""
|
|
577
|
+
return self._get_form("_files", FileMultiDict)
|
|
578
|
+
|
|
579
|
+
@files.setter
|
|
580
|
+
def files(self, value):
|
|
581
|
+
self._set_form("_files", value)
|
|
582
|
+
|
|
583
|
+
@property
|
|
584
|
+
def input_stream(self):
|
|
585
|
+
"""An optional input stream. If you set this it will clear
|
|
586
|
+
:attr:`form` and :attr:`files`.
|
|
587
|
+
"""
|
|
588
|
+
return self._input_stream
|
|
589
|
+
|
|
590
|
+
@input_stream.setter
|
|
591
|
+
def input_stream(self, value):
|
|
592
|
+
self._input_stream = value
|
|
593
|
+
self._form = None
|
|
594
|
+
self._files = None
|
|
595
|
+
|
|
596
|
+
@property
|
|
597
|
+
def query_string(self):
|
|
598
|
+
"""The query string. If you set this to a string
|
|
599
|
+
:attr:`args` will no longer be available.
|
|
600
|
+
"""
|
|
601
|
+
if self._query_string is None:
|
|
602
|
+
if self._args is not None:
|
|
603
|
+
return url_encode(self._args, charset=self.charset)
|
|
604
|
+
return ""
|
|
605
|
+
return self._query_string
|
|
606
|
+
|
|
607
|
+
@query_string.setter
|
|
608
|
+
def query_string(self, value):
|
|
609
|
+
self._query_string = value
|
|
610
|
+
self._args = None
|
|
611
|
+
|
|
612
|
+
@property
|
|
613
|
+
def args(self):
|
|
614
|
+
"""The URL arguments as :class:`MultiDict`."""
|
|
615
|
+
if self._query_string is not None:
|
|
616
|
+
raise AttributeError("a query string is defined")
|
|
617
|
+
if self._args is None:
|
|
618
|
+
self._args = MultiDict()
|
|
619
|
+
return self._args
|
|
620
|
+
|
|
621
|
+
@args.setter
|
|
622
|
+
def args(self, value):
|
|
623
|
+
self._query_string = None
|
|
624
|
+
self._args = value
|
|
625
|
+
|
|
626
|
+
@property
|
|
627
|
+
def server_name(self):
|
|
628
|
+
"""The server name (read-only, use :attr:`host` to set)"""
|
|
629
|
+
return self.host.split(":", 1)[0]
|
|
630
|
+
|
|
631
|
+
@property
|
|
632
|
+
def server_port(self):
|
|
633
|
+
"""The server port as integer (read-only, use :attr:`host` to set)"""
|
|
634
|
+
pieces = self.host.split(":", 1)
|
|
635
|
+
if len(pieces) == 2 and pieces[1].isdigit():
|
|
636
|
+
return int(pieces[1])
|
|
637
|
+
if self.url_scheme == "https":
|
|
638
|
+
return 443
|
|
639
|
+
return 80
|
|
640
|
+
|
|
641
|
+
def __del__(self):
|
|
642
|
+
try:
|
|
643
|
+
self.close()
|
|
644
|
+
except Exception:
|
|
645
|
+
pass
|
|
646
|
+
|
|
647
|
+
def close(self):
|
|
648
|
+
"""Closes all files. If you put real :class:`file` objects into the
|
|
649
|
+
:attr:`files` dict you can call this method to automatically close
|
|
650
|
+
them all in one go.
|
|
651
|
+
"""
|
|
652
|
+
if self.closed:
|
|
653
|
+
return
|
|
654
|
+
try:
|
|
655
|
+
files = itervalues(self.files)
|
|
656
|
+
except AttributeError:
|
|
657
|
+
files = ()
|
|
658
|
+
for f in files:
|
|
659
|
+
try:
|
|
660
|
+
f.close()
|
|
661
|
+
except Exception:
|
|
662
|
+
pass
|
|
663
|
+
self.closed = True
|
|
664
|
+
|
|
665
|
+
def get_environ(self):
|
|
666
|
+
"""Return the built environ.
|
|
667
|
+
|
|
668
|
+
.. versionchanged:: 0.15
|
|
669
|
+
The content type and length headers are set based on
|
|
670
|
+
input stream detection. Previously this only set the WSGI
|
|
671
|
+
keys.
|
|
672
|
+
"""
|
|
673
|
+
input_stream = self.input_stream
|
|
674
|
+
content_length = self.content_length
|
|
675
|
+
|
|
676
|
+
mimetype = self.mimetype
|
|
677
|
+
content_type = self.content_type
|
|
678
|
+
|
|
679
|
+
if input_stream is not None:
|
|
680
|
+
start_pos = input_stream.tell()
|
|
681
|
+
input_stream.seek(0, 2)
|
|
682
|
+
end_pos = input_stream.tell()
|
|
683
|
+
input_stream.seek(start_pos)
|
|
684
|
+
content_length = end_pos - start_pos
|
|
685
|
+
elif mimetype == "multipart/form-data":
|
|
686
|
+
values = CombinedMultiDict([self.form, self.files])
|
|
687
|
+
input_stream, content_length, boundary = stream_encode_multipart(
|
|
688
|
+
values, charset=self.charset
|
|
689
|
+
)
|
|
690
|
+
content_type = mimetype + '; boundary="%s"' % boundary
|
|
691
|
+
elif mimetype == "application/x-www-form-urlencoded":
|
|
692
|
+
# XXX: py2v3 review
|
|
693
|
+
values = url_encode(self.form, charset=self.charset)
|
|
694
|
+
values = values.encode("ascii")
|
|
695
|
+
content_length = len(values)
|
|
696
|
+
input_stream = BytesIO(values)
|
|
697
|
+
else:
|
|
698
|
+
input_stream = BytesIO()
|
|
699
|
+
|
|
700
|
+
result = {}
|
|
701
|
+
if self.environ_base:
|
|
702
|
+
result.update(self.environ_base)
|
|
703
|
+
|
|
704
|
+
def _path_encode(x):
|
|
705
|
+
return wsgi_encoding_dance(url_unquote(x, self.charset), self.charset)
|
|
706
|
+
|
|
707
|
+
qs = wsgi_encoding_dance(self.query_string)
|
|
708
|
+
|
|
709
|
+
result.update(
|
|
710
|
+
{
|
|
711
|
+
"REQUEST_METHOD": self.method,
|
|
712
|
+
"SCRIPT_NAME": _path_encode(self.script_root),
|
|
713
|
+
"PATH_INFO": _path_encode(self.path),
|
|
714
|
+
"QUERY_STRING": qs,
|
|
715
|
+
# Non-standard, added by mod_wsgi, uWSGI
|
|
716
|
+
"REQUEST_URI": wsgi_encoding_dance(self.path),
|
|
717
|
+
# Non-standard, added by gunicorn
|
|
718
|
+
"RAW_URI": wsgi_encoding_dance(self.path),
|
|
719
|
+
"SERVER_NAME": self.server_name,
|
|
720
|
+
"SERVER_PORT": str(self.server_port),
|
|
721
|
+
"HTTP_HOST": self.host,
|
|
722
|
+
"SERVER_PROTOCOL": self.server_protocol,
|
|
723
|
+
"wsgi.version": self.wsgi_version,
|
|
724
|
+
"wsgi.url_scheme": self.url_scheme,
|
|
725
|
+
"wsgi.input": input_stream,
|
|
726
|
+
"wsgi.errors": self.errors_stream,
|
|
727
|
+
"wsgi.multithread": self.multithread,
|
|
728
|
+
"wsgi.multiprocess": self.multiprocess,
|
|
729
|
+
"wsgi.run_once": self.run_once,
|
|
730
|
+
}
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
headers = self.headers.copy()
|
|
734
|
+
|
|
735
|
+
if content_type is not None:
|
|
736
|
+
result["CONTENT_TYPE"] = content_type
|
|
737
|
+
headers.set("Content-Type", content_type)
|
|
738
|
+
|
|
739
|
+
if content_length is not None:
|
|
740
|
+
result["CONTENT_LENGTH"] = str(content_length)
|
|
741
|
+
headers.set("Content-Length", content_length)
|
|
742
|
+
|
|
743
|
+
for key, value in headers.to_wsgi_list():
|
|
744
|
+
result["HTTP_%s" % key.upper().replace("-", "_")] = value
|
|
745
|
+
|
|
746
|
+
if self.environ_overrides:
|
|
747
|
+
result.update(self.environ_overrides)
|
|
748
|
+
|
|
749
|
+
return result
|
|
750
|
+
|
|
751
|
+
def get_request(self, cls=None):
|
|
752
|
+
"""Returns a request with the data. If the request class is not
|
|
753
|
+
specified :attr:`request_class` is used.
|
|
754
|
+
|
|
755
|
+
:param cls: The request wrapper to use.
|
|
756
|
+
"""
|
|
757
|
+
if cls is None:
|
|
758
|
+
cls = self.request_class
|
|
759
|
+
return cls(self.get_environ())
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
class ClientRedirectError(Exception):
|
|
763
|
+
"""If a redirect loop is detected when using follow_redirects=True with
|
|
764
|
+
the :cls:`Client`, then this exception is raised.
|
|
765
|
+
"""
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
class Client(object):
|
|
769
|
+
"""This class allows you to send requests to a wrapped application.
|
|
770
|
+
|
|
771
|
+
The response wrapper can be a class or factory function that takes
|
|
772
|
+
three arguments: app_iter, status and headers. The default response
|
|
773
|
+
wrapper just returns a tuple.
|
|
774
|
+
|
|
775
|
+
Example::
|
|
776
|
+
|
|
777
|
+
class ClientResponse(BaseResponse):
|
|
778
|
+
...
|
|
779
|
+
|
|
780
|
+
client = Client(MyApplication(), response_wrapper=ClientResponse)
|
|
781
|
+
|
|
782
|
+
The use_cookies parameter indicates whether cookies should be stored and
|
|
783
|
+
sent for subsequent requests. This is True by default, but passing False
|
|
784
|
+
will disable this behaviour.
|
|
785
|
+
|
|
786
|
+
If you want to request some subdomain of your application you may set
|
|
787
|
+
`allow_subdomain_redirects` to `True` as if not no external redirects
|
|
788
|
+
are allowed.
|
|
789
|
+
|
|
790
|
+
.. versionadded:: 0.5
|
|
791
|
+
`use_cookies` is new in this version. Older versions did not provide
|
|
792
|
+
builtin cookie support.
|
|
793
|
+
|
|
794
|
+
.. versionadded:: 0.14
|
|
795
|
+
The `mimetype` parameter was added.
|
|
796
|
+
|
|
797
|
+
.. versionadded:: 0.15
|
|
798
|
+
The ``json`` parameter.
|
|
799
|
+
"""
|
|
800
|
+
|
|
801
|
+
def __init__(
|
|
802
|
+
self,
|
|
803
|
+
application,
|
|
804
|
+
response_wrapper=None,
|
|
805
|
+
use_cookies=True,
|
|
806
|
+
allow_subdomain_redirects=False,
|
|
807
|
+
):
|
|
808
|
+
self.application = application
|
|
809
|
+
self.response_wrapper = response_wrapper
|
|
810
|
+
if use_cookies:
|
|
811
|
+
self.cookie_jar = _TestCookieJar()
|
|
812
|
+
else:
|
|
813
|
+
self.cookie_jar = None
|
|
814
|
+
self.allow_subdomain_redirects = allow_subdomain_redirects
|
|
815
|
+
|
|
816
|
+
def set_cookie(
|
|
817
|
+
self,
|
|
818
|
+
server_name,
|
|
819
|
+
key,
|
|
820
|
+
value="",
|
|
821
|
+
max_age=None,
|
|
822
|
+
expires=None,
|
|
823
|
+
path="/",
|
|
824
|
+
domain=None,
|
|
825
|
+
secure=None,
|
|
826
|
+
httponly=False,
|
|
827
|
+
samesite=None,
|
|
828
|
+
charset="utf-8",
|
|
829
|
+
):
|
|
830
|
+
"""Sets a cookie in the client's cookie jar. The server name
|
|
831
|
+
is required and has to match the one that is also passed to
|
|
832
|
+
the open call.
|
|
833
|
+
"""
|
|
834
|
+
assert self.cookie_jar is not None, "cookies disabled"
|
|
835
|
+
header = dump_cookie(
|
|
836
|
+
key,
|
|
837
|
+
value,
|
|
838
|
+
max_age,
|
|
839
|
+
expires,
|
|
840
|
+
path,
|
|
841
|
+
domain,
|
|
842
|
+
secure,
|
|
843
|
+
httponly,
|
|
844
|
+
charset,
|
|
845
|
+
samesite=samesite,
|
|
846
|
+
)
|
|
847
|
+
environ = create_environ(path, base_url="http://" + server_name)
|
|
848
|
+
headers = [("Set-Cookie", header)]
|
|
849
|
+
self.cookie_jar.extract_wsgi(environ, headers)
|
|
850
|
+
|
|
851
|
+
def delete_cookie(self, server_name, key, path="/", domain=None):
|
|
852
|
+
"""Deletes a cookie in the test client."""
|
|
853
|
+
self.set_cookie(
|
|
854
|
+
server_name, key, expires=0, max_age=0, path=path, domain=domain
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
def run_wsgi_app(self, environ, buffered=False):
|
|
858
|
+
"""Runs the wrapped WSGI app with the given environment."""
|
|
859
|
+
if self.cookie_jar is not None:
|
|
860
|
+
self.cookie_jar.inject_wsgi(environ)
|
|
861
|
+
rv = run_wsgi_app(self.application, environ, buffered=buffered)
|
|
862
|
+
if self.cookie_jar is not None:
|
|
863
|
+
self.cookie_jar.extract_wsgi(environ, rv[2])
|
|
864
|
+
return rv
|
|
865
|
+
|
|
866
|
+
def resolve_redirect(self, response, new_location, environ, buffered=False):
|
|
867
|
+
"""Perform a new request to the location given by the redirect
|
|
868
|
+
response to the previous request.
|
|
869
|
+
"""
|
|
870
|
+
scheme, netloc, path, qs, anchor = url_parse(new_location)
|
|
871
|
+
builder = EnvironBuilder.from_environ(environ, query_string=qs)
|
|
872
|
+
|
|
873
|
+
to_name_parts = netloc.split(":", 1)[0].split(".")
|
|
874
|
+
from_name_parts = builder.server_name.split(".")
|
|
875
|
+
|
|
876
|
+
if to_name_parts != [""]:
|
|
877
|
+
# The new location has a host, use it for the base URL.
|
|
878
|
+
builder.url_scheme = scheme
|
|
879
|
+
builder.host = netloc
|
|
880
|
+
else:
|
|
881
|
+
# A local redirect with autocorrect_location_header=False
|
|
882
|
+
# doesn't have a host, so use the request's host.
|
|
883
|
+
to_name_parts = from_name_parts
|
|
884
|
+
|
|
885
|
+
# Explain why a redirect to a different server name won't be followed.
|
|
886
|
+
if to_name_parts != from_name_parts:
|
|
887
|
+
if to_name_parts[-len(from_name_parts) :] == from_name_parts:
|
|
888
|
+
if not self.allow_subdomain_redirects:
|
|
889
|
+
raise RuntimeError("Following subdomain redirects is not enabled.")
|
|
890
|
+
else:
|
|
891
|
+
raise RuntimeError("Following external redirects is not supported.")
|
|
892
|
+
|
|
893
|
+
path_parts = path.split("/")
|
|
894
|
+
root_parts = builder.script_root.split("/")
|
|
895
|
+
|
|
896
|
+
if path_parts[: len(root_parts)] == root_parts:
|
|
897
|
+
# Strip the script root from the path.
|
|
898
|
+
builder.path = path[len(builder.script_root) :]
|
|
899
|
+
else:
|
|
900
|
+
# The new location is not under the script root, so use the
|
|
901
|
+
# whole path and clear the previous root.
|
|
902
|
+
builder.path = path
|
|
903
|
+
builder.script_root = ""
|
|
904
|
+
|
|
905
|
+
status_code = int(response[1].split(None, 1)[0])
|
|
906
|
+
|
|
907
|
+
# Only 307 and 308 preserve all of the original request.
|
|
908
|
+
if status_code not in {307, 308}:
|
|
909
|
+
# HEAD is preserved, everything else becomes GET.
|
|
910
|
+
if builder.method != "HEAD":
|
|
911
|
+
builder.method = "GET"
|
|
912
|
+
|
|
913
|
+
# Clear the body and the headers that describe it.
|
|
914
|
+
builder.input_stream = None
|
|
915
|
+
builder.content_type = None
|
|
916
|
+
builder.content_length = None
|
|
917
|
+
builder.headers.pop("Transfer-Encoding", None)
|
|
918
|
+
|
|
919
|
+
# Disable the response wrapper while handling redirects. Not
|
|
920
|
+
# thread safe, but the client should not be shared anyway.
|
|
921
|
+
old_response_wrapper = self.response_wrapper
|
|
922
|
+
self.response_wrapper = None
|
|
923
|
+
|
|
924
|
+
try:
|
|
925
|
+
return self.open(builder, as_tuple=True, buffered=buffered)
|
|
926
|
+
finally:
|
|
927
|
+
self.response_wrapper = old_response_wrapper
|
|
928
|
+
|
|
929
|
+
def open(self, *args, **kwargs):
|
|
930
|
+
"""Takes the same arguments as the :class:`EnvironBuilder` class with
|
|
931
|
+
some additions: You can provide a :class:`EnvironBuilder` or a WSGI
|
|
932
|
+
environment as only argument instead of the :class:`EnvironBuilder`
|
|
933
|
+
arguments and two optional keyword arguments (`as_tuple`, `buffered`)
|
|
934
|
+
that change the type of the return value or the way the application is
|
|
935
|
+
executed.
|
|
936
|
+
|
|
937
|
+
.. versionchanged:: 0.5
|
|
938
|
+
If a dict is provided as file in the dict for the `data` parameter
|
|
939
|
+
the content type has to be called `content_type` now instead of
|
|
940
|
+
`mimetype`. This change was made for consistency with
|
|
941
|
+
:class:`werkzeug.FileWrapper`.
|
|
942
|
+
|
|
943
|
+
The `follow_redirects` parameter was added to :func:`open`.
|
|
944
|
+
|
|
945
|
+
Additional parameters:
|
|
946
|
+
|
|
947
|
+
:param as_tuple: Returns a tuple in the form ``(environ, result)``
|
|
948
|
+
:param buffered: Set this to True to buffer the application run.
|
|
949
|
+
This will automatically close the application for
|
|
950
|
+
you as well.
|
|
951
|
+
:param follow_redirects: Set this to True if the `Client` should
|
|
952
|
+
follow HTTP redirects.
|
|
953
|
+
"""
|
|
954
|
+
as_tuple = kwargs.pop("as_tuple", False)
|
|
955
|
+
buffered = kwargs.pop("buffered", False)
|
|
956
|
+
follow_redirects = kwargs.pop("follow_redirects", False)
|
|
957
|
+
environ = None
|
|
958
|
+
if not kwargs and len(args) == 1:
|
|
959
|
+
if isinstance(args[0], EnvironBuilder):
|
|
960
|
+
environ = args[0].get_environ()
|
|
961
|
+
elif isinstance(args[0], dict):
|
|
962
|
+
environ = args[0]
|
|
963
|
+
if environ is None:
|
|
964
|
+
builder = EnvironBuilder(*args, **kwargs)
|
|
965
|
+
try:
|
|
966
|
+
environ = builder.get_environ()
|
|
967
|
+
finally:
|
|
968
|
+
builder.close()
|
|
969
|
+
|
|
970
|
+
response = self.run_wsgi_app(environ.copy(), buffered=buffered)
|
|
971
|
+
|
|
972
|
+
# handle redirects
|
|
973
|
+
redirect_chain = []
|
|
974
|
+
while 1:
|
|
975
|
+
status_code = int(response[1].split(None, 1)[0])
|
|
976
|
+
if (
|
|
977
|
+
status_code not in {301, 302, 303, 305, 307, 308}
|
|
978
|
+
or not follow_redirects
|
|
979
|
+
):
|
|
980
|
+
break
|
|
981
|
+
|
|
982
|
+
# Exhaust intermediate response bodies to ensure middleware
|
|
983
|
+
# that returns an iterator runs any cleanup code.
|
|
984
|
+
if not buffered:
|
|
985
|
+
for _ in response[0]:
|
|
986
|
+
pass
|
|
987
|
+
|
|
988
|
+
new_location = response[2]["location"]
|
|
989
|
+
new_redirect_entry = (new_location, status_code)
|
|
990
|
+
if new_redirect_entry in redirect_chain:
|
|
991
|
+
raise ClientRedirectError("loop detected")
|
|
992
|
+
redirect_chain.append(new_redirect_entry)
|
|
993
|
+
environ, response = self.resolve_redirect(
|
|
994
|
+
response, new_location, environ, buffered=buffered
|
|
995
|
+
)
|
|
996
|
+
|
|
997
|
+
if self.response_wrapper is not None:
|
|
998
|
+
response = self.response_wrapper(*response)
|
|
999
|
+
if as_tuple:
|
|
1000
|
+
return environ, response
|
|
1001
|
+
return response
|
|
1002
|
+
|
|
1003
|
+
def get(self, *args, **kw):
|
|
1004
|
+
"""Like open but method is enforced to GET."""
|
|
1005
|
+
kw["method"] = "GET"
|
|
1006
|
+
return self.open(*args, **kw)
|
|
1007
|
+
|
|
1008
|
+
def patch(self, *args, **kw):
|
|
1009
|
+
"""Like open but method is enforced to PATCH."""
|
|
1010
|
+
kw["method"] = "PATCH"
|
|
1011
|
+
return self.open(*args, **kw)
|
|
1012
|
+
|
|
1013
|
+
def post(self, *args, **kw):
|
|
1014
|
+
"""Like open but method is enforced to POST."""
|
|
1015
|
+
kw["method"] = "POST"
|
|
1016
|
+
return self.open(*args, **kw)
|
|
1017
|
+
|
|
1018
|
+
def head(self, *args, **kw):
|
|
1019
|
+
"""Like open but method is enforced to HEAD."""
|
|
1020
|
+
kw["method"] = "HEAD"
|
|
1021
|
+
return self.open(*args, **kw)
|
|
1022
|
+
|
|
1023
|
+
def put(self, *args, **kw):
|
|
1024
|
+
"""Like open but method is enforced to PUT."""
|
|
1025
|
+
kw["method"] = "PUT"
|
|
1026
|
+
return self.open(*args, **kw)
|
|
1027
|
+
|
|
1028
|
+
def delete(self, *args, **kw):
|
|
1029
|
+
"""Like open but method is enforced to DELETE."""
|
|
1030
|
+
kw["method"] = "DELETE"
|
|
1031
|
+
return self.open(*args, **kw)
|
|
1032
|
+
|
|
1033
|
+
def options(self, *args, **kw):
|
|
1034
|
+
"""Like open but method is enforced to OPTIONS."""
|
|
1035
|
+
kw["method"] = "OPTIONS"
|
|
1036
|
+
return self.open(*args, **kw)
|
|
1037
|
+
|
|
1038
|
+
def trace(self, *args, **kw):
|
|
1039
|
+
"""Like open but method is enforced to TRACE."""
|
|
1040
|
+
kw["method"] = "TRACE"
|
|
1041
|
+
return self.open(*args, **kw)
|
|
1042
|
+
|
|
1043
|
+
def __repr__(self):
|
|
1044
|
+
return "<%s %r>" % (self.__class__.__name__, self.application)
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
def create_environ(*args, **kwargs):
|
|
1048
|
+
"""Create a new WSGI environ dict based on the values passed. The first
|
|
1049
|
+
parameter should be the path of the request which defaults to '/'. The
|
|
1050
|
+
second one can either be an absolute path (in that case the host is
|
|
1051
|
+
localhost:80) or a full path to the request with scheme, netloc port and
|
|
1052
|
+
the path to the script.
|
|
1053
|
+
|
|
1054
|
+
This accepts the same arguments as the :class:`EnvironBuilder`
|
|
1055
|
+
constructor.
|
|
1056
|
+
|
|
1057
|
+
.. versionchanged:: 0.5
|
|
1058
|
+
This function is now a thin wrapper over :class:`EnvironBuilder` which
|
|
1059
|
+
was added in 0.5. The `headers`, `environ_base`, `environ_overrides`
|
|
1060
|
+
and `charset` parameters were added.
|
|
1061
|
+
"""
|
|
1062
|
+
builder = EnvironBuilder(*args, **kwargs)
|
|
1063
|
+
try:
|
|
1064
|
+
return builder.get_environ()
|
|
1065
|
+
finally:
|
|
1066
|
+
builder.close()
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
def run_wsgi_app(app, environ, buffered=False):
|
|
1070
|
+
"""Return a tuple in the form (app_iter, status, headers) of the
|
|
1071
|
+
application output. This works best if you pass it an application that
|
|
1072
|
+
returns an iterator all the time.
|
|
1073
|
+
|
|
1074
|
+
Sometimes applications may use the `write()` callable returned
|
|
1075
|
+
by the `start_response` function. This tries to resolve such edge
|
|
1076
|
+
cases automatically. But if you don't get the expected output you
|
|
1077
|
+
should set `buffered` to `True` which enforces buffering.
|
|
1078
|
+
|
|
1079
|
+
If passed an invalid WSGI application the behavior of this function is
|
|
1080
|
+
undefined. Never pass non-conforming WSGI applications to this function.
|
|
1081
|
+
|
|
1082
|
+
:param app: the application to execute.
|
|
1083
|
+
:param buffered: set to `True` to enforce buffering.
|
|
1084
|
+
:return: tuple in the form ``(app_iter, status, headers)``
|
|
1085
|
+
"""
|
|
1086
|
+
environ = _get_environ(environ)
|
|
1087
|
+
response = []
|
|
1088
|
+
buffer = []
|
|
1089
|
+
|
|
1090
|
+
def start_response(status, headers, exc_info=None):
|
|
1091
|
+
if exc_info is not None:
|
|
1092
|
+
reraise(*exc_info)
|
|
1093
|
+
response[:] = [status, headers]
|
|
1094
|
+
return buffer.append
|
|
1095
|
+
|
|
1096
|
+
app_rv = app(environ, start_response)
|
|
1097
|
+
close_func = getattr(app_rv, "close", None)
|
|
1098
|
+
app_iter = iter(app_rv)
|
|
1099
|
+
|
|
1100
|
+
# when buffering we emit the close call early and convert the
|
|
1101
|
+
# application iterator into a regular list
|
|
1102
|
+
if buffered:
|
|
1103
|
+
try:
|
|
1104
|
+
app_iter = list(app_iter)
|
|
1105
|
+
finally:
|
|
1106
|
+
if close_func is not None:
|
|
1107
|
+
close_func()
|
|
1108
|
+
|
|
1109
|
+
# otherwise we iterate the application iter until we have a response, chain
|
|
1110
|
+
# the already received data with the already collected data and wrap it in
|
|
1111
|
+
# a new `ClosingIterator` if we need to restore a `close` callable from the
|
|
1112
|
+
# original return value.
|
|
1113
|
+
else:
|
|
1114
|
+
for item in app_iter:
|
|
1115
|
+
buffer.append(item)
|
|
1116
|
+
if response:
|
|
1117
|
+
break
|
|
1118
|
+
if buffer:
|
|
1119
|
+
app_iter = chain(buffer, app_iter)
|
|
1120
|
+
if close_func is not None and app_iter is not app_rv:
|
|
1121
|
+
app_iter = ClosingIterator(app_iter, close_func)
|
|
1122
|
+
|
|
1123
|
+
return app_iter, response[0], Headers(response[1])
|