mirror of
https://github.com/kevin1024/vcrpy.git
synced 2025-12-08 16:53:23 +00:00
Compare commits
6 Commits
enable-dec
...
b122b5c701
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b122b5c701 | ||
|
|
4883e3eefa | ||
|
|
5678b13b47 | ||
|
|
48f5f84f86 | ||
|
|
31d8c3498b | ||
|
|
b28316ab10 |
@@ -7,6 +7,14 @@ For a full list of triaged issues, bugs and PRs and what release they are target
|
|||||||
|
|
||||||
All help in providing PRs to close out bug issues is appreciated. Even if that is providing a repo that fully replicates issues. We have very generous contributors that have added these to bug issues which meant another contributor picked up the bug and closed it out.
|
All help in providing PRs to close out bug issues is appreciated. Even if that is providing a repo that fully replicates issues. We have very generous contributors that have added these to bug issues which meant another contributor picked up the bug and closed it out.
|
||||||
|
|
||||||
|
- 8.1.0
|
||||||
|
- Enable brotli decompression if available (via ``brotli``, ``brotlipy`` or ``brotlicffi``) (#620) - thanks @immerrr
|
||||||
|
- Fix aiohttp allowing both ``data`` and ``json`` arguments when one is None (#624) - thanks @leorochael
|
||||||
|
- Fix usage of io-like interface with VCR.py (#906) - thanks @tito and @kevdevg
|
||||||
|
- Migrate to declarative Python package config (#767) - thanks @deronnax
|
||||||
|
- Various linting fixes - thanks @jairhenrique
|
||||||
|
- CI: bump actions/checkout from 5 to 6 (#955)
|
||||||
|
|
||||||
- 8.0.0
|
- 8.0.0
|
||||||
- BREAKING: Drop support for Python 3.9 (major version bump) - thanks @jairhenrique
|
- BREAKING: Drop support for Python 3.9 (major version bump) - thanks @jairhenrique
|
||||||
- BREAKING: Drop support for urllib3 < 2 - fixes CVE warnings from urllib3 1.x (#926, #880) - thanks @jairhenrique
|
- BREAKING: Drop support for urllib3 < 2 - fixes CVE warnings from urllib3 1.x (#926, #880) - thanks @jairhenrique
|
||||||
|
|||||||
@@ -1,3 +1,72 @@
|
|||||||
|
[project]
|
||||||
|
name = "vcrpy"
|
||||||
|
authors = [{name = "Kevin McCarthy", email = "me@kevinmccarthy.org"}]
|
||||||
|
license = {text = "MIT"}
|
||||||
|
description = "Automatically mock your HTTP interactions to simplify and speed up testing"
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 5 - Production/Stable",
|
||||||
|
"Environment :: Console",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"Programming Language :: Python",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Programming Language :: Python :: 3.13",
|
||||||
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
|
"Programming Language :: Python :: Implementation :: CPython",
|
||||||
|
"Programming Language :: Python :: Implementation :: PyPy",
|
||||||
|
"Topic :: Software Development :: Testing",
|
||||||
|
"Topic :: Internet :: WWW/HTTP",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
]
|
||||||
|
urls = {Homepage = "https://github.com/kevin1024/vcrpy"}
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = [
|
||||||
|
"PyYAML",
|
||||||
|
"wrapt",
|
||||||
|
]
|
||||||
|
dynamic = ["version"]
|
||||||
|
|
||||||
|
[project.readme]
|
||||||
|
file = "README.rst"
|
||||||
|
content-type = "text/x-rst"
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
tests = [
|
||||||
|
"aiohttp",
|
||||||
|
"boto3",
|
||||||
|
"cryptography",
|
||||||
|
"httpbin",
|
||||||
|
"httpcore",
|
||||||
|
"httplib2",
|
||||||
|
"httpx",
|
||||||
|
"pycurl; platform_python_implementation !='PyPy'",
|
||||||
|
"pytest",
|
||||||
|
"pytest-aiohttp",
|
||||||
|
"pytest-asyncio",
|
||||||
|
"pytest-cov",
|
||||||
|
"pytest-httpbin",
|
||||||
|
"requests>=2.22.0",
|
||||||
|
"tornado",
|
||||||
|
"urllib3",
|
||||||
|
"werkzeug==2.0.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
include-package-data = false
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
exclude = ["tests*"]
|
||||||
|
namespaces = false
|
||||||
|
|
||||||
|
[tool.setuptools.dynamic]
|
||||||
|
version = {attr = "vcr.__version__"}
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61.2"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[tool.codespell]
|
[tool.codespell]
|
||||||
skip = '.git,*.pdf,*.svg,.tox'
|
skip = '.git,*.pdf,*.svg,.tox'
|
||||||
ignore-regex = "\\\\[fnrstv]"
|
ignore-regex = "\\\\[fnrstv]"
|
||||||
|
|||||||
60
setup.cfg
60
setup.cfg
@@ -1,2 +1,58 @@
|
|||||||
[bdist_wheel]
|
[metadata]
|
||||||
universal=1
|
name = vcrpy
|
||||||
|
version = attr: vcr.__version__
|
||||||
|
author = Kevin McCarthy
|
||||||
|
author_email = me@kevinmccarthy.org
|
||||||
|
license = MIT
|
||||||
|
description = Automatically mock your HTTP interactions to simplify and speed up testing
|
||||||
|
url = https://github.com/kevin1024/vcrpy
|
||||||
|
long_description = file: README.rst
|
||||||
|
long_description_content_type = text/x-rst
|
||||||
|
classifiers =
|
||||||
|
Development Status :: 5 - Production/Stable
|
||||||
|
Environment :: Console
|
||||||
|
Intended Audience :: Developers
|
||||||
|
Programming Language :: Python
|
||||||
|
Programming Language :: Python :: 3
|
||||||
|
Programming Language :: Python :: 3.10
|
||||||
|
Programming Language :: Python :: 3.11
|
||||||
|
Programming Language :: Python :: 3.12
|
||||||
|
Programming Language :: Python :: 3.13
|
||||||
|
Programming Language :: Python :: 3 :: Only
|
||||||
|
Programming Language :: Python :: Implementation :: CPython
|
||||||
|
Programming Language :: Python :: Implementation :: PyPy
|
||||||
|
Topic :: Software Development :: Testing
|
||||||
|
Topic :: Internet :: WWW/HTTP
|
||||||
|
License :: OSI Approved :: MIT License
|
||||||
|
|
||||||
|
[options]
|
||||||
|
packages = find:
|
||||||
|
python_requires = >=3.10
|
||||||
|
install_requires =
|
||||||
|
PyYAML
|
||||||
|
wrapt
|
||||||
|
tests_require =
|
||||||
|
vcrpy[tests]
|
||||||
|
|
||||||
|
[options.packages.find]
|
||||||
|
exclude = tests*
|
||||||
|
|
||||||
|
[options.extras_require]
|
||||||
|
tests =
|
||||||
|
aiohttp
|
||||||
|
boto3
|
||||||
|
cryptography
|
||||||
|
httpbin
|
||||||
|
httpcore
|
||||||
|
httplib2
|
||||||
|
httpx
|
||||||
|
pycurl; platform_python_implementation !='PyPy'
|
||||||
|
pytest
|
||||||
|
pytest-aiohttp
|
||||||
|
pytest-asyncio
|
||||||
|
pytest-cov
|
||||||
|
pytest-httpbin
|
||||||
|
requests>=2.22.0
|
||||||
|
tornado
|
||||||
|
urllib3
|
||||||
|
werkzeug==2.0.3
|
||||||
|
|||||||
89
setup.py
89
setup.py
@@ -1,89 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
import codecs
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from setuptools import find_packages, setup
|
|
||||||
|
|
||||||
long_description = Path("README.rst").read_text()
|
|
||||||
here = os.path.abspath(os.path.dirname(__file__))
|
|
||||||
|
|
||||||
|
|
||||||
def read(*parts):
|
|
||||||
# intentionally *not* adding an encoding option to open, See:
|
|
||||||
# https://github.com/pypa/virtualenv/issues/201#issuecomment-3145690
|
|
||||||
with codecs.open(os.path.join(here, *parts), "r") as fp:
|
|
||||||
return fp.read()
|
|
||||||
|
|
||||||
|
|
||||||
def find_version(*file_paths):
|
|
||||||
version_file = read(*file_paths)
|
|
||||||
version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M)
|
|
||||||
if version_match:
|
|
||||||
return version_match.group(1)
|
|
||||||
|
|
||||||
raise RuntimeError("Unable to find version string.")
|
|
||||||
|
|
||||||
|
|
||||||
install_requires = [
|
|
||||||
"PyYAML",
|
|
||||||
"wrapt",
|
|
||||||
]
|
|
||||||
|
|
||||||
extras_require = {
|
|
||||||
"tests": [
|
|
||||||
"aiohttp",
|
|
||||||
"boto3",
|
|
||||||
"cryptography",
|
|
||||||
"httpbin",
|
|
||||||
"httpcore",
|
|
||||||
"httplib2",
|
|
||||||
"httpx",
|
|
||||||
"pycurl; platform_python_implementation !='PyPy'",
|
|
||||||
"pytest",
|
|
||||||
"pytest-aiohttp",
|
|
||||||
"pytest-asyncio",
|
|
||||||
"pytest-cov",
|
|
||||||
"pytest-httpbin",
|
|
||||||
"requests>=2.22.0",
|
|
||||||
"tornado",
|
|
||||||
"urllib3",
|
|
||||||
"werkzeug==2.0.3",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name="vcrpy",
|
|
||||||
version=find_version("vcr", "__init__.py"),
|
|
||||||
description=("Automatically mock your HTTP interactions to simplify and speed up testing"),
|
|
||||||
long_description=long_description,
|
|
||||||
long_description_content_type="text/x-rst",
|
|
||||||
author="Kevin McCarthy",
|
|
||||||
author_email="me@kevinmccarthy.org",
|
|
||||||
url="https://github.com/kevin1024/vcrpy",
|
|
||||||
packages=find_packages(exclude=["tests*"]),
|
|
||||||
python_requires=">=3.10",
|
|
||||||
install_requires=install_requires,
|
|
||||||
license="MIT",
|
|
||||||
extras_require=extras_require,
|
|
||||||
tests_require=extras_require["tests"],
|
|
||||||
classifiers=[
|
|
||||||
"Development Status :: 5 - Production/Stable",
|
|
||||||
"Environment :: Console",
|
|
||||||
"Intended Audience :: Developers",
|
|
||||||
"Programming Language :: Python",
|
|
||||||
"Programming Language :: Python :: 3",
|
|
||||||
"Programming Language :: Python :: 3.10",
|
|
||||||
"Programming Language :: Python :: 3.11",
|
|
||||||
"Programming Language :: Python :: 3.12",
|
|
||||||
"Programming Language :: Python :: 3.13",
|
|
||||||
"Programming Language :: Python :: 3 :: Only",
|
|
||||||
"Programming Language :: Python :: Implementation :: CPython",
|
|
||||||
"Programming Language :: Python :: Implementation :: PyPy",
|
|
||||||
"Topic :: Software Development :: Testing",
|
|
||||||
"Topic :: Internet :: WWW/HTTP",
|
|
||||||
"License :: OSI Approved :: MIT License",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
@@ -137,19 +137,29 @@ def test_stream(tmpdir, httpbin):
|
|||||||
assert cassette.play_count == 1
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
POST_DATA = {"key1": "value1", "key2": "value2"}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.online
|
@pytest.mark.online
|
||||||
@pytest.mark.parametrize("body", ["data", "json"])
|
@pytest.mark.parametrize(
|
||||||
def test_post(tmpdir, body, caplog, httpbin):
|
"kwargs",
|
||||||
|
[
|
||||||
|
{"data": POST_DATA},
|
||||||
|
{"json": POST_DATA},
|
||||||
|
{"data": POST_DATA, "json": None},
|
||||||
|
{"data": None, "json": POST_DATA},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_post(tmpdir, kwargs, caplog, httpbin):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
data = {"key1": "value1", "key2": "value2"}
|
url = httpbin.url + "/post"
|
||||||
url = httpbin.url
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("post.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("post.yaml"))):
|
||||||
_, response_json = post(url, **{body: data})
|
_, response_json = post(url, **kwargs)
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("post.yaml"))) as cassette:
|
with vcr.use_cassette(str(tmpdir.join("post.yaml"))) as cassette:
|
||||||
request = cassette.requests[0]
|
request = cassette.requests[0]
|
||||||
assert request.body == data
|
assert request.body == POST_DATA
|
||||||
_, cassette_response_json = post(url, **{body: data})
|
_, cassette_response_json = post(url, **kwargs)
|
||||||
assert cassette_response_json == response_json
|
assert cassette_response_json == response_json
|
||||||
assert cassette.play_count == 1
|
assert cassette.play_count == 1
|
||||||
|
|
||||||
@@ -163,6 +173,17 @@ def test_post(tmpdir, body, caplog, httpbin):
|
|||||||
), "Log message not found."
|
), "Log message not found."
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.online
|
||||||
|
def test_post_data_plus_json_error(tmpdir, httpbin):
|
||||||
|
url = httpbin.url + "/post"
|
||||||
|
with (
|
||||||
|
vcr.use_cassette(str(tmpdir.join("post.yaml"))) as cassette,
|
||||||
|
pytest.raises(ValueError, match="data and json parameters can not be used at the same time"),
|
||||||
|
):
|
||||||
|
post(url, data=POST_DATA, json=POST_DATA)
|
||||||
|
assert cassette.requests == []
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.online
|
@pytest.mark.online
|
||||||
def test_params(tmpdir, httpbin):
|
def test_params(tmpdir, httpbin):
|
||||||
url = httpbin.url + "/get?d=d"
|
url = httpbin.url + "/get?d=d"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from urllib.request import Request, urlopen
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import vcr
|
import vcr
|
||||||
|
from vcr.filters import brotli
|
||||||
|
|
||||||
from ..assertions import assert_cassette_has_one_response, assert_is_json_bytes
|
from ..assertions import assert_cassette_has_one_response, assert_is_json_bytes
|
||||||
|
|
||||||
@@ -138,6 +139,22 @@ def test_decompress_deflate(tmpdir, httpbin):
|
|||||||
assert_is_json_bytes(decoded_response)
|
assert_is_json_bytes(decoded_response)
|
||||||
|
|
||||||
|
|
||||||
|
def test_decompress_brotli(tmpdir, httpbin):
|
||||||
|
if brotli is None:
|
||||||
|
# XXX: this is never true, because brotlipy is installed with "httpbin"
|
||||||
|
pytest.skip("Brotli is not installed")
|
||||||
|
|
||||||
|
url = httpbin.url + "/brotli"
|
||||||
|
request = Request(url, headers={"Accept-Encoding": ["gzip, deflate, br"]})
|
||||||
|
cass_file = str(tmpdir.join("brotli_response.yaml"))
|
||||||
|
with vcr.use_cassette(cass_file, decode_compressed_response=True):
|
||||||
|
urlopen(request)
|
||||||
|
with vcr.use_cassette(cass_file) as cass:
|
||||||
|
decoded_response = urlopen(url).read()
|
||||||
|
assert_cassette_has_one_response(cass)
|
||||||
|
assert_is_json_bytes(decoded_response)
|
||||||
|
|
||||||
|
|
||||||
def test_decompress_regular(tmpdir, httpbin):
|
def test_decompress_regular(tmpdir, httpbin):
|
||||||
"""Test that it doesn't try to decompress content that isn't compressed"""
|
"""Test that it doesn't try to decompress content that isn't compressed"""
|
||||||
url = httpbin.url + "/get"
|
url = httpbin.url + "/get"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from logging import NullHandler
|
|||||||
from .config import VCR
|
from .config import VCR
|
||||||
from .record_mode import RecordMode as mode # noqa: F401
|
from .record_mode import RecordMode as mode # noqa: F401
|
||||||
|
|
||||||
__version__ = "8.0.0"
|
__version__ = "8.1.0"
|
||||||
|
|
||||||
logging.getLogger(__name__).addHandler(NullHandler())
|
logging.getLogger(__name__).addHandler(NullHandler())
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,49 @@ from urllib.parse import urlencode, urlparse, urlunparse
|
|||||||
|
|
||||||
from .util import CaseInsensitiveDict
|
from .util import CaseInsensitiveDict
|
||||||
|
|
||||||
|
try:
|
||||||
|
# This supports both brotli & brotlipy packages
|
||||||
|
import brotli
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
import brotlicffi as brotli
|
||||||
|
except ImportError:
|
||||||
|
brotli = None
|
||||||
|
|
||||||
|
|
||||||
|
def decompress_deflate(body):
|
||||||
|
try:
|
||||||
|
return zlib.decompress(body)
|
||||||
|
except zlib.error:
|
||||||
|
# Assume the response was already decompressed
|
||||||
|
return body
|
||||||
|
|
||||||
|
|
||||||
|
def decompress_gzip(body):
|
||||||
|
# To (de-)compress gzip format, use wbits = zlib.MAX_WBITS | 16.
|
||||||
|
try:
|
||||||
|
return zlib.decompress(body, zlib.MAX_WBITS | 16)
|
||||||
|
except zlib.error:
|
||||||
|
# Assume the response was already decompressed
|
||||||
|
return body
|
||||||
|
|
||||||
|
|
||||||
|
AVAILABLE_DECOMPRESSORS = {
|
||||||
|
"deflate": decompress_deflate,
|
||||||
|
"gzip": decompress_gzip,
|
||||||
|
}
|
||||||
|
|
||||||
|
if brotli is not None:
|
||||||
|
|
||||||
|
def decompress_brotli(body):
|
||||||
|
try:
|
||||||
|
return brotli.decompress(body)
|
||||||
|
except brotli.error:
|
||||||
|
# Assume the response was already decompressed
|
||||||
|
return body
|
||||||
|
|
||||||
|
AVAILABLE_DECOMPRESSORS["br"] = decompress_brotli
|
||||||
|
|
||||||
|
|
||||||
def replace_headers(request, replacements):
|
def replace_headers(request, replacements):
|
||||||
"""Replace headers in request according to replacements.
|
"""Replace headers in request according to replacements.
|
||||||
@@ -136,45 +179,30 @@ def remove_post_data_parameters(request, post_data_parameters_to_remove):
|
|||||||
|
|
||||||
def decode_response(response):
|
def decode_response(response):
|
||||||
"""
|
"""
|
||||||
If the response is compressed with gzip or deflate:
|
If the response is compressed with any supported compression (gzip,
|
||||||
|
deflate, br if available):
|
||||||
1. decompress the response body
|
1. decompress the response body
|
||||||
2. delete the content-encoding header
|
2. delete the content-encoding header
|
||||||
3. update content-length header to decompressed length
|
3. update content-length header to decompressed length
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def is_compressed(headers):
|
|
||||||
encoding = headers.get("content-encoding", [])
|
|
||||||
return encoding and encoding[0] in ("gzip", "deflate")
|
|
||||||
|
|
||||||
def decompress_body(body, encoding):
|
|
||||||
"""Returns decompressed body according to encoding using zlib.
|
|
||||||
to (de-)compress gzip format, use wbits = zlib.MAX_WBITS | 16
|
|
||||||
"""
|
|
||||||
if not body:
|
|
||||||
return ""
|
|
||||||
if encoding == "gzip":
|
|
||||||
try:
|
|
||||||
return zlib.decompress(body, zlib.MAX_WBITS | 16)
|
|
||||||
except zlib.error:
|
|
||||||
return body # assumes that the data was already decompressed
|
|
||||||
else: # encoding == 'deflate'
|
|
||||||
try:
|
|
||||||
return zlib.decompress(body)
|
|
||||||
except zlib.error:
|
|
||||||
return body # assumes that the data was already decompressed
|
|
||||||
|
|
||||||
# Deepcopy here in case `headers` contain objects that could
|
# Deepcopy here in case `headers` contain objects that could
|
||||||
# be mutated by a shallow copy and corrupt the real response.
|
# be mutated by a shallow copy and corrupt the real response.
|
||||||
response = copy.deepcopy(response)
|
response = copy.deepcopy(response)
|
||||||
headers = CaseInsensitiveDict(response["headers"])
|
headers = CaseInsensitiveDict(response["headers"])
|
||||||
if is_compressed(headers):
|
content_encoding = headers.get("content-encoding")
|
||||||
encoding = headers["content-encoding"][0]
|
if not content_encoding:
|
||||||
headers["content-encoding"].remove(encoding)
|
return response
|
||||||
if not headers["content-encoding"]:
|
decompressor = AVAILABLE_DECOMPRESSORS.get(content_encoding[0])
|
||||||
del headers["content-encoding"]
|
if not decompressor:
|
||||||
|
return response
|
||||||
|
|
||||||
new_body = decompress_body(response["body"]["string"], encoding)
|
headers["content-encoding"].remove(content_encoding[0])
|
||||||
response["body"]["string"] = new_body
|
if not headers["content-encoding"]:
|
||||||
headers["content-length"] = [str(len(new_body))]
|
del headers["content-encoding"]
|
||||||
response["headers"] = dict(headers)
|
|
||||||
|
new_body = decompressor(response["body"]["string"])
|
||||||
|
response["body"]["string"] = new_body
|
||||||
|
headers["content-length"] = [str(len(new_body))]
|
||||||
|
response["headers"] = dict(headers)
|
||||||
return response
|
return response
|
||||||
|
|||||||
@@ -245,7 +245,11 @@ def vcr_request(cassette, real_request):
|
|||||||
headers = kwargs.get("headers")
|
headers = kwargs.get("headers")
|
||||||
auth = kwargs.get("auth")
|
auth = kwargs.get("auth")
|
||||||
headers = self._prepare_headers(headers)
|
headers = self._prepare_headers(headers)
|
||||||
data = kwargs.get("data", kwargs.get("json"))
|
data = kwargs.get("data")
|
||||||
|
if data is None:
|
||||||
|
data = kwargs.get("json")
|
||||||
|
elif kwargs.get("json") is not None:
|
||||||
|
raise ValueError("data and json parameters can not be used at the same time")
|
||||||
params = kwargs.get("params")
|
params = kwargs.get("params")
|
||||||
cookies = kwargs.get("cookies")
|
cookies = kwargs.get("cookies")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user