Fix error response in test_expiration_epoch_in_http
Signed-off-by: Yulia Kovshova <y.kovshova@yadro.com>
This commit is contained in:
parent
588292dfb5
commit
30703bf701
1 changed files with 71 additions and 57 deletions
|
@ -5,40 +5,47 @@ from time import sleep
|
||||||
|
|
||||||
import allure
|
import allure
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from common import COMPLEX_OBJ_SIZE
|
from common import COMPLEX_OBJ_SIZE
|
||||||
from container import create_container
|
from container import create_container
|
||||||
from epoch import get_epoch, tick_epoch
|
from epoch import get_epoch, tick_epoch
|
||||||
from grpc_responses import OBJECT_NOT_FOUND, error_matches_status
|
from python_keywords.http_gate import (
|
||||||
from python_keywords.http_gate import (get_via_http_curl, get_via_http_gate,
|
get_via_http_curl,
|
||||||
get_via_http_gate_by_attribute, get_via_zip_http_gate,
|
get_via_http_gate,
|
||||||
upload_via_http_gate, upload_via_http_gate_curl)
|
get_via_http_gate_by_attribute,
|
||||||
|
get_via_zip_http_gate,
|
||||||
|
upload_via_http_gate,
|
||||||
|
upload_via_http_gate_curl,
|
||||||
|
)
|
||||||
from python_keywords.neofs_verbs import get_object, put_object
|
from python_keywords.neofs_verbs import get_object, put_object
|
||||||
from python_keywords.storage_policy import get_nodes_without_object
|
from python_keywords.storage_policy import get_nodes_without_object
|
||||||
from python_keywords.utility_keywords import generate_file, get_file_hash
|
from python_keywords.utility_keywords import generate_file, get_file_hash
|
||||||
from utility import wait_for_gc_pass_on_storage_nodes
|
from utility import wait_for_gc_pass_on_storage_nodes
|
||||||
from wellknown_acl import PUBLIC_ACL
|
from wellknown_acl import PUBLIC_ACL
|
||||||
|
|
||||||
logger = logging.getLogger('NeoLogger')
|
logger = logging.getLogger("NeoLogger")
|
||||||
|
OBJECT_NOT_FOUND_ERROR = "object not found"
|
||||||
|
|
||||||
# For some reason object uploaded via http gateway is not immediately available for downloading
|
# For some reason object uploaded via http gateway is not immediately available for downloading
|
||||||
# Until this issue is resolved we are waiting for some time before attempting to read an object
|
# Until this issue is resolved we are waiting for some time before attempting to read an object
|
||||||
# TODO: remove after https://github.com/nspcc-dev/neofs-http-gw/issues/176 is fixed
|
# TODO: remove after https://github.com/nspcc-dev/neofs-http-gw/issues/176 is fixed
|
||||||
OBJECT_UPLOAD_DELAY = 10
|
OBJECT_UPLOAD_DELAY = 10
|
||||||
|
|
||||||
@allure.link('https://github.com/nspcc-dev/neofs-http-gw#neofs-http-gateway', name='neofs-http-gateway')
|
|
||||||
@allure.link('https://github.com/nspcc-dev/neofs-http-gw#uploading', name='uploading')
|
@allure.link(
|
||||||
@allure.link('https://github.com/nspcc-dev/neofs-http-gw#downloading', name='downloading')
|
"https://github.com/nspcc-dev/neofs-http-gw#neofs-http-gateway", name="neofs-http-gateway"
|
||||||
|
)
|
||||||
|
@allure.link("https://github.com/nspcc-dev/neofs-http-gw#uploading", name="uploading")
|
||||||
|
@allure.link("https://github.com/nspcc-dev/neofs-http-gw#downloading", name="downloading")
|
||||||
@pytest.mark.http_gate
|
@pytest.mark.http_gate
|
||||||
class TestHttpGate:
|
class TestHttpGate:
|
||||||
PLACEMENT_RULE = "REP 1 IN X CBF 1 SELECT 1 FROM * AS X"
|
PLACEMENT_RULE = "REP 1 IN X CBF 1 SELECT 1 FROM * AS X"
|
||||||
|
|
||||||
@pytest.fixture(scope="class", autouse=True)
|
@pytest.fixture(scope="class", autouse=True)
|
||||||
@allure.title('[Class/Autouse]: Prepare wallet and deposit')
|
@allure.title("[Class/Autouse]: Prepare wallet and deposit")
|
||||||
def prepare_wallet(self, prepare_wallet_and_deposit):
|
def prepare_wallet(self, prepare_wallet_and_deposit):
|
||||||
TestHttpGate.wallet = prepare_wallet_and_deposit
|
TestHttpGate.wallet = prepare_wallet_and_deposit
|
||||||
|
|
||||||
@allure.title('Test Put over gRPC, Get over HTTP')
|
@allure.title("Test Put over gRPC, Get over HTTP")
|
||||||
def test_put_grpc_get_http(self):
|
def test_put_grpc_get_http(self):
|
||||||
"""
|
"""
|
||||||
Test that object can be put using gRPC interface and get using HTTP.
|
Test that object can be put using gRPC interface and get using HTTP.
|
||||||
|
@ -57,17 +64,17 @@ class TestHttpGate:
|
||||||
cid = create_container(self.wallet, rule=self.PLACEMENT_RULE, basic_acl=PUBLIC_ACL)
|
cid = create_container(self.wallet, rule=self.PLACEMENT_RULE, basic_acl=PUBLIC_ACL)
|
||||||
file_path_simple, file_path_large = generate_file(), generate_file(COMPLEX_OBJ_SIZE)
|
file_path_simple, file_path_large = generate_file(), generate_file(COMPLEX_OBJ_SIZE)
|
||||||
|
|
||||||
with allure.step('Put objects using gRPC'):
|
with allure.step("Put objects using gRPC"):
|
||||||
oid_simple = put_object(wallet=self.wallet, path=file_path_simple, cid=cid)
|
oid_simple = put_object(wallet=self.wallet, path=file_path_simple, cid=cid)
|
||||||
oid_large = put_object(wallet=self.wallet, path=file_path_large, cid=cid)
|
oid_large = put_object(wallet=self.wallet, path=file_path_large, cid=cid)
|
||||||
|
|
||||||
for oid, file_path in ((oid_simple, file_path_simple), (oid_large, file_path_large)):
|
for oid, file_path in ((oid_simple, file_path_simple), (oid_large, file_path_large)):
|
||||||
self.get_object_and_verify_hashes(oid, file_path, self.wallet, cid)
|
self.get_object_and_verify_hashes(oid, file_path, self.wallet, cid)
|
||||||
|
|
||||||
@allure.link('https://github.com/nspcc-dev/neofs-http-gw#uploading', name='uploading')
|
@allure.link("https://github.com/nspcc-dev/neofs-http-gw#uploading", name="uploading")
|
||||||
@allure.link('https://github.com/nspcc-dev/neofs-http-gw#downloading', name='downloading')
|
@allure.link("https://github.com/nspcc-dev/neofs-http-gw#downloading", name="downloading")
|
||||||
@pytest.mark.sanity
|
@pytest.mark.sanity
|
||||||
@allure.title('Test Put over HTTP, Get over HTTP')
|
@allure.title("Test Put over HTTP, Get over HTTP")
|
||||||
def test_put_http_get_http(self):
|
def test_put_http_get_http(self):
|
||||||
"""
|
"""
|
||||||
Test that object can be put and get using HTTP interface.
|
Test that object can be put and get using HTTP interface.
|
||||||
|
@ -84,23 +91,25 @@ class TestHttpGate:
|
||||||
cid = create_container(self.wallet, rule=self.PLACEMENT_RULE, basic_acl=PUBLIC_ACL)
|
cid = create_container(self.wallet, rule=self.PLACEMENT_RULE, basic_acl=PUBLIC_ACL)
|
||||||
file_path_simple, file_path_large = generate_file(), generate_file(COMPLEX_OBJ_SIZE)
|
file_path_simple, file_path_large = generate_file(), generate_file(COMPLEX_OBJ_SIZE)
|
||||||
|
|
||||||
with allure.step('Put objects using HTTP'):
|
with allure.step("Put objects using HTTP"):
|
||||||
oid_simple = upload_via_http_gate(cid=cid, path=file_path_simple)
|
oid_simple = upload_via_http_gate(cid=cid, path=file_path_simple)
|
||||||
oid_large = upload_via_http_gate(cid=cid, path=file_path_large)
|
oid_large = upload_via_http_gate(cid=cid, path=file_path_large)
|
||||||
|
|
||||||
for oid, file_path in ((oid_simple, file_path_simple), (oid_large, file_path_large)):
|
for oid, file_path in ((oid_simple, file_path_simple), (oid_large, file_path_large)):
|
||||||
self.get_object_and_verify_hashes(oid, file_path, self.wallet, cid)
|
self.get_object_and_verify_hashes(oid, file_path, self.wallet, cid)
|
||||||
|
|
||||||
@allure.link('https://github.com/nspcc-dev/neofs-http-gw#by-attributes', name='download by attributes')
|
@allure.link(
|
||||||
@allure.title('Test Put over HTTP, Get over HTTP with headers')
|
"https://github.com/nspcc-dev/neofs-http-gw#by-attributes", name="download by attributes"
|
||||||
|
)
|
||||||
|
@allure.title("Test Put over HTTP, Get over HTTP with headers")
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'attributes',
|
"attributes",
|
||||||
[
|
[
|
||||||
{'fileName': 'simple_obj_filename'},
|
{"fileName": "simple_obj_filename"},
|
||||||
{'file-Name': 'simple obj filename'},
|
{"file-Name": "simple obj filename"},
|
||||||
{'cat%jpeg': 'cat%jpeg'}
|
{"cat%jpeg": "cat%jpeg"},
|
||||||
],
|
],
|
||||||
ids=['simple', 'hyphen', 'percent']
|
ids=["simple", "hyphen", "percent"],
|
||||||
)
|
)
|
||||||
def test_put_http_get_http_with_headers(self, attributes: dict):
|
def test_put_http_get_http_with_headers(self, attributes: dict):
|
||||||
"""
|
"""
|
||||||
|
@ -118,7 +127,7 @@ class TestHttpGate:
|
||||||
cid = create_container(self.wallet, rule=self.PLACEMENT_RULE, basic_acl=PUBLIC_ACL)
|
cid = create_container(self.wallet, rule=self.PLACEMENT_RULE, basic_acl=PUBLIC_ACL)
|
||||||
file_path = generate_file()
|
file_path = generate_file()
|
||||||
|
|
||||||
with allure.step('Put objects using HTTP with attribute'):
|
with allure.step("Put objects using HTTP with attribute"):
|
||||||
headers = self._attr_into_header(attributes)
|
headers = self._attr_into_header(attributes)
|
||||||
oid = upload_via_http_gate(cid=cid, path=file_path, headers=headers)
|
oid = upload_via_http_gate(cid=cid, path=file_path, headers=headers)
|
||||||
|
|
||||||
|
@ -126,7 +135,7 @@ class TestHttpGate:
|
||||||
|
|
||||||
self.get_object_by_attr_and_verify_hashes(oid, file_path, cid, attributes)
|
self.get_object_by_attr_and_verify_hashes(oid, file_path, cid, attributes)
|
||||||
|
|
||||||
@allure.title('Test Expiration-Epoch in HTTP header')
|
@allure.title("Test Expiration-Epoch in HTTP header")
|
||||||
def test_expiration_epoch_in_http(self):
|
def test_expiration_epoch_in_http(self):
|
||||||
cid = create_container(self.wallet, rule=self.PLACEMENT_RULE, basic_acl=PUBLIC_ACL)
|
cid = create_container(self.wallet, rule=self.PLACEMENT_RULE, basic_acl=PUBLIC_ACL)
|
||||||
file_path = generate_file()
|
file_path = generate_file()
|
||||||
|
@ -136,14 +145,14 @@ class TestHttpGate:
|
||||||
epochs = (curr_epoch, curr_epoch + 1, curr_epoch + 2, curr_epoch + 100)
|
epochs = (curr_epoch, curr_epoch + 1, curr_epoch + 2, curr_epoch + 100)
|
||||||
|
|
||||||
for epoch in epochs:
|
for epoch in epochs:
|
||||||
headers = {'X-Attribute-Neofs-Expiration-Epoch': str(epoch)}
|
headers = {"X-Attribute-Neofs-Expiration-Epoch": str(epoch)}
|
||||||
|
|
||||||
with allure.step('Put objects using HTTP with attribute Expiration-Epoch'):
|
with allure.step("Put objects using HTTP with attribute Expiration-Epoch"):
|
||||||
oids.append(upload_via_http_gate(cid=cid, path=file_path, headers=headers))
|
oids.append(upload_via_http_gate(cid=cid, path=file_path, headers=headers))
|
||||||
|
|
||||||
assert len(oids) == len(epochs), 'Expected all objects have been put successfully'
|
assert len(oids) == len(epochs), "Expected all objects have been put successfully"
|
||||||
|
|
||||||
with allure.step('All objects can be get'):
|
with allure.step("All objects can be get"):
|
||||||
for oid in oids:
|
for oid in oids:
|
||||||
get_via_http_gate(cid=cid, oid=oid)
|
get_via_http_gate(cid=cid, oid=oid)
|
||||||
|
|
||||||
|
@ -155,23 +164,21 @@ class TestHttpGate:
|
||||||
|
|
||||||
for oid in expired_objects:
|
for oid in expired_objects:
|
||||||
self.try_to_get_object_and_expect_error(
|
self.try_to_get_object_and_expect_error(
|
||||||
cid=cid,
|
cid=cid, oid=oid, error_pattern=OBJECT_NOT_FOUND_ERROR
|
||||||
oid=oid,
|
|
||||||
error_pattern=OBJECT_NOT_FOUND
|
|
||||||
)
|
)
|
||||||
|
|
||||||
with allure.step('Other objects can be get'):
|
with allure.step("Other objects can be get"):
|
||||||
for oid in not_expired_objects:
|
for oid in not_expired_objects:
|
||||||
get_via_http_gate(cid=cid, oid=oid)
|
get_via_http_gate(cid=cid, oid=oid)
|
||||||
|
|
||||||
@allure.title('Test Zip in HTTP header')
|
@allure.title("Test Zip in HTTP header")
|
||||||
def test_zip_in_http(self):
|
def test_zip_in_http(self):
|
||||||
cid = create_container(self.wallet, rule=self.PLACEMENT_RULE, basic_acl=PUBLIC_ACL)
|
cid = create_container(self.wallet, rule=self.PLACEMENT_RULE, basic_acl=PUBLIC_ACL)
|
||||||
file_path_simple, file_path_large = generate_file(), generate_file(COMPLEX_OBJ_SIZE)
|
file_path_simple, file_path_large = generate_file(), generate_file(COMPLEX_OBJ_SIZE)
|
||||||
common_prefix = 'my_files'
|
common_prefix = "my_files"
|
||||||
|
|
||||||
headers1 = {'X-Attribute-FilePath': f'{common_prefix}/file1'}
|
headers1 = {"X-Attribute-FilePath": f"{common_prefix}/file1"}
|
||||||
headers2 = {'X-Attribute-FilePath': f'{common_prefix}/file2'}
|
headers2 = {"X-Attribute-FilePath": f"{common_prefix}/file2"}
|
||||||
|
|
||||||
upload_via_http_gate(cid=cid, path=file_path_simple, headers=headers1)
|
upload_via_http_gate(cid=cid, path=file_path_simple, headers=headers1)
|
||||||
upload_via_http_gate(cid=cid, path=file_path_large, headers=headers2)
|
upload_via_http_gate(cid=cid, path=file_path_large, headers=headers2)
|
||||||
|
@ -180,23 +187,23 @@ class TestHttpGate:
|
||||||
|
|
||||||
dir_path = get_via_zip_http_gate(cid=cid, prefix=common_prefix)
|
dir_path = get_via_zip_http_gate(cid=cid, prefix=common_prefix)
|
||||||
|
|
||||||
with allure.step('Verify hashes'):
|
with allure.step("Verify hashes"):
|
||||||
assert get_file_hash(f'{dir_path}/file1') == get_file_hash(file_path_simple)
|
assert get_file_hash(f"{dir_path}/file1") == get_file_hash(file_path_simple)
|
||||||
assert get_file_hash(f'{dir_path}/file2') == get_file_hash(file_path_large)
|
assert get_file_hash(f"{dir_path}/file2") == get_file_hash(file_path_large)
|
||||||
|
|
||||||
@pytest.mark.curl
|
@pytest.mark.curl
|
||||||
@pytest.mark.long
|
@pytest.mark.long
|
||||||
@allure.title('Test Put over HTTP/Curl, Get over HTTP/Curl for large object')
|
@allure.title("Test Put over HTTP/Curl, Get over HTTP/Curl for large object")
|
||||||
def test_put_http_get_http_large_file(self):
|
def test_put_http_get_http_large_file(self):
|
||||||
"""
|
"""
|
||||||
This test checks upload and download using curl with 'large' object. Large is object with size up to 20Mb.
|
This test checks upload and download using curl with 'large' object. Large is object with size up to 20Mb.
|
||||||
"""
|
"""
|
||||||
cid = create_container(self.wallet, rule=self.PLACEMENT_RULE, basic_acl=PUBLIC_ACL)
|
cid = create_container(self.wallet, rule=self.PLACEMENT_RULE, basic_acl=PUBLIC_ACL)
|
||||||
|
|
||||||
obj_size = int(os.getenv('BIG_OBJ_SIZE', COMPLEX_OBJ_SIZE))
|
obj_size = int(os.getenv("BIG_OBJ_SIZE", COMPLEX_OBJ_SIZE))
|
||||||
file_path = generate_file(obj_size)
|
file_path = generate_file(obj_size)
|
||||||
|
|
||||||
with allure.step('Put objects using HTTP'):
|
with allure.step("Put objects using HTTP"):
|
||||||
oid_gate = upload_via_http_gate(cid=cid, path=file_path)
|
oid_gate = upload_via_http_gate(cid=cid, path=file_path)
|
||||||
oid_curl = upload_via_http_gate_curl(cid=cid, filepath=file_path, large_object=True)
|
oid_curl = upload_via_http_gate_curl(cid=cid, filepath=file_path, large_object=True)
|
||||||
|
|
||||||
|
@ -204,7 +211,7 @@ class TestHttpGate:
|
||||||
self.get_object_and_verify_hashes(oid_curl, file_path, self.wallet, cid, get_via_http_curl)
|
self.get_object_and_verify_hashes(oid_curl, file_path, self.wallet, cid, get_via_http_curl)
|
||||||
|
|
||||||
@pytest.mark.curl
|
@pytest.mark.curl
|
||||||
@allure.title('Test Put/Get over HTTP using Curl utility')
|
@allure.title("Test Put/Get over HTTP using Curl utility")
|
||||||
def test_put_http_get_http_curl(self):
|
def test_put_http_get_http_curl(self):
|
||||||
"""
|
"""
|
||||||
Test checks upload and download over HTTP using curl utility.
|
Test checks upload and download over HTTP using curl utility.
|
||||||
|
@ -212,7 +219,7 @@ class TestHttpGate:
|
||||||
cid = create_container(self.wallet, rule=self.PLACEMENT_RULE, basic_acl=PUBLIC_ACL)
|
cid = create_container(self.wallet, rule=self.PLACEMENT_RULE, basic_acl=PUBLIC_ACL)
|
||||||
file_path_simple, file_path_large = generate_file(), generate_file(COMPLEX_OBJ_SIZE)
|
file_path_simple, file_path_large = generate_file(), generate_file(COMPLEX_OBJ_SIZE)
|
||||||
|
|
||||||
with allure.step('Put objects using curl utility'):
|
with allure.step("Put objects using curl utility"):
|
||||||
oid_simple = upload_via_http_gate_curl(cid=cid, filepath=file_path_simple)
|
oid_simple = upload_via_http_gate_curl(cid=cid, filepath=file_path_simple)
|
||||||
oid_large = upload_via_http_gate_curl(cid=cid, filepath=file_path_large)
|
oid_large = upload_via_http_gate_curl(cid=cid, filepath=file_path_large)
|
||||||
|
|
||||||
|
@ -220,26 +227,29 @@ class TestHttpGate:
|
||||||
self.get_object_and_verify_hashes(oid, file_path, self.wallet, cid, get_via_http_curl)
|
self.get_object_and_verify_hashes(oid, file_path, self.wallet, cid, get_via_http_curl)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@allure.step('Try to get object and expect error')
|
@allure.step("Try to get object and expect error")
|
||||||
def try_to_get_object_and_expect_error(cid: str, oid: str, error_pattern: str) -> None:
|
def try_to_get_object_and_expect_error(cid: str, oid: str, error_pattern: str) -> None:
|
||||||
try:
|
try:
|
||||||
get_via_http_gate(cid=cid, oid=oid)
|
get_via_http_gate(cid=cid, oid=oid)
|
||||||
raise AssertionError(f'Expected error on getting object with cid: {cid}')
|
raise AssertionError(f"Expected error on getting object with cid: {cid}")
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
assert error_matches_status(err, error_pattern), \
|
assert error_pattern in err, f"Expected {err} to match {error_pattern}"
|
||||||
f'Expected {err} to match {error_pattern}'
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@allure.step('Verify object can be get using HTTP header attribute')
|
@allure.step("Verify object can be get using HTTP header attribute")
|
||||||
def get_object_by_attr_and_verify_hashes(oid: str, file_name: str, cid: str, attrs: dict):
|
def get_object_by_attr_and_verify_hashes(oid: str, file_name: str, cid: str, attrs: dict):
|
||||||
got_file_path_http = get_via_http_gate(cid=cid, oid=oid)
|
got_file_path_http = get_via_http_gate(cid=cid, oid=oid)
|
||||||
got_file_path_http_attr = get_via_http_gate_by_attribute(cid=cid, attribute=attrs)
|
got_file_path_http_attr = get_via_http_gate_by_attribute(cid=cid, attribute=attrs)
|
||||||
|
|
||||||
TestHttpGate._assert_hashes_are_equal(file_name, got_file_path_http, got_file_path_http_attr)
|
TestHttpGate._assert_hashes_are_equal(
|
||||||
|
file_name, got_file_path_http, got_file_path_http_attr
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@allure.step('Verify object can be get using HTTP')
|
@allure.step("Verify object can be get using HTTP")
|
||||||
def get_object_and_verify_hashes(oid: str, file_name: str, wallet: str, cid: str, object_getter=None):
|
def get_object_and_verify_hashes(
|
||||||
|
oid: str, file_name: str, wallet: str, cid: str, object_getter=None
|
||||||
|
):
|
||||||
nodes = get_nodes_without_object(wallet=wallet, cid=cid, oid=oid)
|
nodes = get_nodes_without_object(wallet=wallet, cid=cid, oid=oid)
|
||||||
random_node = choice(nodes)
|
random_node = choice(nodes)
|
||||||
object_getter = object_getter or get_via_http_gate
|
object_getter = object_getter or get_via_http_gate
|
||||||
|
@ -251,11 +261,15 @@ class TestHttpGate:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _assert_hashes_are_equal(orig_file_name: str, got_file_1: str, got_file_2: str):
|
def _assert_hashes_are_equal(orig_file_name: str, got_file_1: str, got_file_2: str):
|
||||||
msg = 'Expected hashes are equal for files {f1} and {f2}'
|
msg = "Expected hashes are equal for files {f1} and {f2}"
|
||||||
got_file_hash_http = get_file_hash(got_file_1)
|
got_file_hash_http = get_file_hash(got_file_1)
|
||||||
assert get_file_hash(got_file_2) == got_file_hash_http, msg.format(f1=got_file_2, f2=got_file_1)
|
assert get_file_hash(got_file_2) == got_file_hash_http, msg.format(
|
||||||
assert get_file_hash(orig_file_name) == got_file_hash_http, msg.format(f1=orig_file_name, f2=got_file_1)
|
f1=got_file_2, f2=got_file_1
|
||||||
|
)
|
||||||
|
assert get_file_hash(orig_file_name) == got_file_hash_http, msg.format(
|
||||||
|
f1=orig_file_name, f2=got_file_1
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _attr_into_header(attrs: dict) -> dict:
|
def _attr_into_header(attrs: dict) -> dict:
|
||||||
return {f'X-Attribute-{_key}': _value for _key, _value in attrs.items()}
|
return {f"X-Attribute-{_key}": _value for _key, _value in attrs.items()}
|
||||||
|
|
Loading…
Reference in a new issue