Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: [VRD-894] New classes for PipelineStep, PipelineGraph, and RegisteredPipeline #4020

Merged
merged 125 commits into from
Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from 66 commits
Commits
Show all changes
125 commits
Select commit Hold shift + click to select a range
0f4dbc9
feature: New classes for handling pipelines
ewagner-verta Aug 17, 2023
87ab35c
Merge remote-tracking branch 'origin/main' into VRD-894_pipelines_CRUD
ewagner-verta Aug 22, 2023
ce4e3f3
fix: drop change to pytest fixture
ewagner-verta Aug 22, 2023
b1bd32d
test: fix up unit tests for changes to mock rmv fixture
ewagner-verta Aug 22, 2023
d54fc0a
test: update simple pipeline fixture and doc string to reflect that i…
ewagner-verta Aug 22, 2023
2d01a70
test: guarantee uniqueness in mock rmv fixture ids
ewagner-verta Aug 22, 2023
4a237cc
Update client/verta/tests/unit_tests/conftest.py
ewagner-verta Aug 22, 2023
b7a07c6
Update client/verta/tests/unit_tests/conftest.py
ewagner-verta Aug 22, 2023
2e24387
test: use static string for name in mock pipe step
ewagner-verta Aug 22, 2023
b8e70ff
Update client/verta/tests/unit_tests/conftest.py
ewagner-verta Aug 22, 2023
c6bb7e1
Update client/verta/tests/unit_tests/pipeline/test_pipeline_graph.py
ewagner-verta Aug 22, 2023
ed63443
test: change naming of function scoped variable
ewagner-verta Aug 22, 2023
6b6443f
Update client/verta/tests/unit_tests/pipeline/test_registered_pipelin…
ewagner-verta Aug 22, 2023
4828aab
test: update mocked responses to pull connection scheme and socket dy…
ewagner-verta Aug 22, 2023
b16ec5f
test: fix incorrect comment line
ewagner-verta Aug 22, 2023
1a1f826
Update client/verta/tests/unit_tests/pipeline/test_registered_pipelin…
ewagner-verta Aug 22, 2023
692c369
test: drop unused strategies and remove 'mock' from naming convention…
ewagner-verta Aug 22, 2023
63d0f6c
test: refactor pipeline definition strategy to be linear pipeline
ewagner-verta Aug 22, 2023
362c6b0
Update client/verta/verta/pipeline/_pipeline_graph.py
ewagner-verta Aug 22, 2023
a4389de
fix: make pipeline_step set_steps function return the steps
ewagner-verta Aug 22, 2023
37f9bbc
fix: update repr formatting for PipelineGraph
ewagner-verta Aug 22, 2023
1c18e23
Update client/verta/verta/pipeline/_pipeline_graph.py
ewagner-verta Aug 22, 2023
947b994
Update client/verta/verta/pipeline/_pipeline_step.py
ewagner-verta Aug 22, 2023
62ea6bb
fix: remove 'optional' tag in doc string for required variable
ewagner-verta Aug 22, 2023
97e940e
fix: use more sepcific error messaging for type violations when setti…
ewagner-verta Aug 22, 2023
eb44a21
docs: update doc string with missing params
ewagner-verta Aug 22, 2023
94d4819
docs: update doc strings for increased clarity on their purpose
ewagner-verta Aug 22, 2023
e935e0e
Update client/verta/verta/pipeline/_pipeline_step.py
ewagner-verta Aug 22, 2023
725e1c6
docs: correct param name in doc string for set_steps
ewagner-verta Aug 22, 2023
d652c8d
Update client/verta/verta/pipeline/_pipeline_step.py
ewagner-verta Aug 22, 2023
83ba0d6
docs: Fix doc string formatting
ewagner-verta Aug 22, 2023
937905d
docs: add missing param in doc string
ewagner-verta Aug 22, 2023
e932080
docs: remove indentation of attributes section in class docs
ewagner-verta Aug 22, 2023
c7a2210
refactor: drop redundant 'pipeline' from pipeline_graph param
ewagner-verta Aug 22, 2023
f52fd67
refactor: make attribute setter methods return their objects now that…
ewagner-verta Aug 22, 2023
e445735
refactor: use deepcopy when copying graphs from RPs
ewagner-verta Aug 22, 2023
82695fb
docs: fix doc string type for pipeline_resources
ewagner-verta Aug 22, 2023
029e087
Update client/verta/verta/pipeline/_registered_pipeline.py
ewagner-verta Aug 22, 2023
99ae54f
refactor: remove usage of .pop from RP _to_pipeline_configuration fun…
ewagner-verta Aug 22, 2023
d1dd479
Merge remote-tracking branch 'origin/main' into VRD-894_pipelines_CRUD
ewagner-verta Aug 22, 2023
d2924f2
refactor: drop resources fixture in favor of hypothesis strategy and …
ewagner-verta Aug 22, 2023
af9bf74
refactor: use sets instead of lists for predecessors and graph steps
ewagner-verta Aug 22, 2023
593c59b
tests: fix pipeline_definition fixture to use integer ids for model v…
ewagner-verta Aug 22, 2023
b22c9e8
test: use max_value on integer strategy only where required to preven…
ewagner-verta Aug 22, 2023
4bf8214
docs: fix typo in doc string
ewagner-verta Aug 23, 2023
0433567
refactor: use conn params from RMV to get RM and adjust unit tests to…
ewagner-verta Aug 23, 2023
c72c790
Merge remote-tracking branch 'origin/main' into VRD-894_pipelines_CRUD
ewagner-verta Aug 23, 2023
8390e73
test: be more specific when checking for error conditions on provided…
ewagner-verta Aug 23, 2023
c19a56c
test: be more specific when checking for error conditions on provided…
ewagner-verta Aug 23, 2023
2a6f69a
refactor: fix artifact handling and use conn params from RMV object
ewagner-verta Aug 23, 2023
2518199
test: make pipeline_definition strategy handle arbitrary numbers of s…
ewagner-verta Aug 23, 2023
f87bfaf
refactor: separate validation logic from setter methods to allow vali…
ewagner-verta Aug 23, 2023
8f4ad67
refactor: use registered_model_version as naming convention everywher…
ewagner-verta Aug 24, 2023
271cecf
Merge remote-tracking branch 'origin/main' into VRD-894_pipelines_CRUD
ewagner-verta Aug 24, 2023
05eec31
refactor: update all related params when changing RMV for a step
ewagner-verta Aug 24, 2023
2321822
refactor: use conn params from RMV instead of passing in
ewagner-verta Aug 24, 2023
0e9b6eb
fix: final formatting and accuracy changes for _pipeline_step.py
ewagner-verta Aug 24, 2023
c731001
fix: final formatting and accuracy changes for _pipeline_graph.py
ewagner-verta Aug 24, 2023
130e18f
fix: final formatting and accuracy changes for _registered_pipeline.py
ewagner-verta Aug 24, 2023
b82930e
fix: final formatting and accuracy changes for strategies.py
ewagner-verta Aug 24, 2023
a2f5ea1
fix: final formatting and accuracy changes for conftest.py
ewagner-verta Aug 24, 2023
fb0557a
fix: black format
ewagner-verta Aug 24, 2023
fa42034
fix: final formatting and accuracy changes for unit test files
ewagner-verta Aug 24, 2023
6329110
docs: tweak public docs
ewagner-verta Aug 24, 2023
b42c8af
test: fix order of test operations
ewagner-verta Aug 24, 2023
916755c
fix: annotation error cuaght by pylint
ewagner-verta Aug 25, 2023
3109eeb
Update client/verta/tests/unit_tests/pipeline/test_pipeline_graph.py
ewagner-verta Aug 27, 2023
ab7590b
Update client/verta/tests/unit_tests/pipeline/test_pipeline_graph.py
ewagner-verta Aug 27, 2023
c0ac34e
Update client/verta/tests/unit_tests/pipeline/test_pipeline_graph.py
ewagner-verta Aug 27, 2023
72388cf
Update client/verta/tests/unit_tests/pipeline/test_pipeline_step.py
ewagner-verta Aug 27, 2023
c21180b
Update client/verta/tests/unit_tests/pipeline/test_pipeline_step.py
ewagner-verta Aug 27, 2023
ec2cdb5
Update client/verta/tests/unit_tests/pipeline/test_pipeline_step.py
ewagner-verta Aug 27, 2023
fbb1742
Update client/verta/tests/unit_tests/pipeline/test_registered_pipelin…
ewagner-verta Aug 27, 2023
e120e0f
Update client/verta/tests/unit_tests/pipeline/test_registered_pipelin…
ewagner-verta Aug 27, 2023
2478022
Update client/verta/tests/unit_tests/pipeline/test_registered_pipelin…
ewagner-verta Aug 27, 2023
e7aeaa4
test: use RM instead of RMV for patched object
ewagner-verta Aug 27, 2023
b51953b
Update client/verta/tests/unit_tests/pipeline/test_registered_pipelin…
ewagner-verta Aug 27, 2023
330ca5d
Update client/verta/tests/unit_tests/pipeline/test_registered_pipelin…
ewagner-verta Aug 27, 2023
73e42d6
Update client/verta/verta/pipeline/_pipeline_step.py
ewagner-verta Aug 27, 2023
e690e29
Update client/verta/verta/pipeline/_registered_pipeline.py
ewagner-verta Aug 27, 2023
261a074
Update client/verta/verta/pipeline/_registered_pipeline.py
ewagner-verta Aug 27, 2023
88c0151
Update client/verta/verta/pipeline/_registered_pipeline.py
ewagner-verta Aug 27, 2023
3226704
Update client/verta/verta/pipeline/_registered_pipeline.py
ewagner-verta Aug 27, 2023
80f3f10
Update client/verta/verta/pipeline/_registered_pipeline.py
ewagner-verta Aug 27, 2023
33f5083
Update client/verta/verta/pipeline/_registered_pipeline.py
ewagner-verta Aug 27, 2023
39f5d4b
test: fix scope of unique IDs variable in mocked RMV fixture
ewagner-verta Aug 27, 2023
0af5258
fix: clean up after recent commits
ewagner-verta Aug 27, 2023
af2fad7
test: pull scheme and socket dynamically from mock_conn test fixture
ewagner-verta Aug 27, 2023
9d9682e
test: fix incorrect assertion comment
ewagner-verta Aug 27, 2023
25ab753
test: expand test coverage of user provided resources
ewagner-verta Aug 27, 2023
ed25f1b
test: move assembly of test component to more logical location in fun…
ewagner-verta Aug 27, 2023
f725f3e
refactor: drop validation from attribute getters, and add tests for m…
ewagner-verta Aug 27, 2023
92efc2c
docs: simplify language in doc strings for functions that format data…
ewagner-verta Aug 27, 2023
7af253a
docs: simplify language in doc strings for functions that format data…
ewagner-verta Aug 27, 2023
c3689c7
fix: spacing on repr function
ewagner-verta Aug 28, 2023
3513fc2
Merge remote-tracking branch 'origin/main' into VRD-894_pipelines_CRUD
ewagner-verta Aug 28, 2023
1942bf7
Update client/verta/verta/pipeline/_pipeline_step.py
ewagner-verta Aug 28, 2023
65ef63b
refactor: use ID from RM object instead of duplicating via class var
ewagner-verta Aug 28, 2023
36086fe
refactor: support setting predecessros to None
ewagner-verta Aug 28, 2023
c3c43ec
Merge remote-tracking branch 'origin/main' into VRD-894_pipelines_CRUD
ewagner-verta Aug 28, 2023
83589df
refactor: black formatting
ewagner-verta Aug 28, 2023
50c333f
refactor: Allow users to input list, set, or tuple for step predecess…
ewagner-verta Aug 29, 2023
d9d98f9
refactor: make RegisteredPipeline properly responsive to changes in R…
ewagner-verta Aug 29, 2023
79576ff
Merge remote-tracking branch 'origin/main' into VRD-894_pipelines_CRUD
ewagner-verta Aug 29, 2023
86b1032
Update client/verta/verta/pipeline/_pipeline_graph.py
ewagner-verta Aug 29, 2023
93df142
refactor: move type casting from validation function to setter function
ewagner-verta Aug 29, 2023
0c04859
fix: type annotation for PipelineStep init to accept a union of itera…
ewagner-verta Aug 29, 2023
1fb8e1e
Update client/verta/tests/unit_tests/pipeline/test_pipeline_graph.py
ewagner-verta Aug 29, 2023
e58cb00
Update client/verta/tests/unit_tests/pipeline/test_pipeline_step.py
ewagner-verta Aug 29, 2023
33e5511
Update client/verta/tests/unit_tests/pipeline/test_pipeline_step.py
ewagner-verta Aug 29, 2023
304bef1
Update client/verta/tests/unit_tests/pipeline/test_pipeline_step.py
ewagner-verta Aug 29, 2023
3db95af
Update client/verta/tests/unit_tests/pipeline/test_pipeline_step.py
ewagner-verta Aug 29, 2023
073a058
Update client/verta/tests/unit_tests/pipeline/test_pipeline_step.py
ewagner-verta Aug 29, 2023
4eade59
Update client/verta/tests/unit_tests/pipeline/test_pipeline_step.py
ewagner-verta Aug 29, 2023
0085a8d
docs: simplify doc strings
ewagner-verta Aug 29, 2023
38b1add
Update client/verta/tests/unit_tests/pipeline/test_registered_pipelin…
ewagner-verta Aug 29, 2023
695ff3a
Update client/verta/verta/pipeline/_pipeline_step.py
ewagner-verta Aug 29, 2023
530fc31
refactor: drop newline from repr
ewagner-verta Aug 29, 2023
3b1807f
refactor: move validate predecessors call to validate steps function
ewagner-verta Aug 29, 2023
c2a367e
feature: check for uniqueness of step names
ewagner-verta Aug 29, 2023
0712c0f
refactor: black formatting
ewagner-verta Aug 29, 2023
0266f9f
fix: use len comparison to avoid empty list != empty set issue, and p…
ewagner-verta Aug 29, 2023
95b1b6a
Merge remote-tracking branch 'origin/main' into VRD-894_pipelines_CRUD
ewagner-verta Aug 30, 2023
95bbca9
fix: circular import problem
ewagner-verta Aug 30, 2023
fe3c11d
Update client/verta/verta/pipeline/_pipeline_graph.py
ewagner-verta Aug 30, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/verta/docs/python.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Verta
endpoint
environment
integrations
pipeline
registry
runtime
tracking
Expand Down
156 changes: 148 additions & 8 deletions client/verta/tests/unit_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

"""Pytest fixtures for use in client unit tests."""

import json
import os
import random
from typing import Any, Callable, Dict, Optional
from unittest.mock import patch

import pytest
Expand All @@ -13,7 +16,8 @@
from verta.client import Client
from verta.credentials import EmailCredentials
from verta.endpoint import Endpoint
from verta.registry.entities import RegisteredModelVersion
from verta.pipeline import PipelineGraph, PipelineStep
from verta.registry.entities import RegisteredModel, RegisteredModelVersion


@pytest.fixture(scope="session")
Expand Down Expand Up @@ -64,15 +68,151 @@ def __repr__(self): # avoid network calls when displaying test results


@pytest.fixture(scope="session")
def mock_registered_model_version(mock_conn, mock_config):
ewagner-verta marked this conversation as resolved.
Show resolved Hide resolved
"""Return a mocked object of the RegisteredModelVersion class for use in tests"""
def make_mock_simple_pipeline_definition() -> Callable:
"""Return a callable function for creating a simple mocked pipeline
definition.

For use in tests, including a parameter for the pipeline
id to ensure consistency in tests that mock creation of a pipeline
object from a pipeline definition.
"""

def simple_pipeline_definition(id: int) -> Dict[str, Any]:
return {
"graph": [
{"predecessors": [], "name": "step1"},
{"predecessors": ["step1"], "name": "step2"},
],
"pipeline_version_id": id,
"steps": [
{
"model_version_id": 1,
"name": "step1",
},
{
"model_version_id": 2,
"name": "step2",
},
],
}

return simple_pipeline_definition


@pytest.fixture(scope="session")
def make_mock_registered_model(mock_conn, mock_config) -> Callable:
"""Return a callable function for creating mocked objects of the
RegisteredModel class.
"""

class MockRegisteredModel(RegisteredModel):
def __repr__(self): # avoid network calls when displaying test results
return object.__repr__(self)

def _make_mock_registered_model(id: int, name: str):
"""Return a mocked RegisteredModel object."""

return MockRegisteredModel(
mock_conn,
mock_config,
_RegistryService.RegisteredModel(
id=id,
name=name,
),
)

return _make_mock_registered_model


@pytest.fixture(scope="session")
def make_mock_registered_model_version(
mock_conn, mock_config, make_mock_simple_pipeline_definition
) -> Callable:
"""Return a callable function for creating mocked objects of the
RegisteredModelVersion class.
"""

class MockRegisteredModelVersion(RegisteredModelVersion):
def __repr__(self): # avoid network calls when displaying test results
return object.__repr__(self)

return MockRegisteredModelVersion(
mock_conn,
mock_config,
_RegistryService.ModelVersion(id=555, registered_model_id=123),
)
def _get_artifact(self, key=None, artifact_type=None):
if key == "pipeline.json":
return json.dumps(
make_mock_simple_pipeline_definition(id=self.id)
).encode("utf-8")

def _make_mock_registered_model_version():
"""Return a mocked ``RegisteredModelVersion``.

``id`` and ``registered_model_id`` will be random and unique for the
test session.
ewagner-verta marked this conversation as resolved.
Show resolved Hide resolved

"""
ids = set()
ewagner-verta marked this conversation as resolved.
Show resolved Hide resolved
model_ver_id = random.randint(1, 1000000)
while model_ver_id in ids:
model_ver_id = random.randint(1, 1000000)
ids.add(model_ver_id)

reg_model_id = random.randint(1, 1000000)
while reg_model_id in ids:
reg_model_id = random.randint(1, 1000000)
ids.add(reg_model_id)

return MockRegisteredModelVersion(
mock_conn,
mock_config,
_RegistryService.ModelVersion(
id=model_ver_id,
registered_model_id=reg_model_id,
version="test_model_version_name",
),
)

return _make_mock_registered_model_version


@pytest.fixture(scope="session")
def make_mock_pipeline_step(make_mock_registered_model_version) -> Callable:
"""Return a callable function for creating mocked objects of the PipelineStep
class.

The optional `name` parameter is for use in tests where names must be
known for assertions.
"""

class MockPipelineStep(PipelineStep):
def __repr__(self): # avoid network calls when displaying test results
return object.__repr__(self)

def _make_mock_pipeline_step(name: Optional[str] = None):
return MockPipelineStep(
registered_model_version=make_mock_registered_model_version(),
name=name if name else "test_pipeline_step_name",
ewagner-verta marked this conversation as resolved.
Show resolved Hide resolved
predecessors=set(),
)

return _make_mock_pipeline_step


@pytest.fixture(scope="session")
def make_mock_pipeline_graph(make_mock_pipeline_step) -> Callable:
"""Return a callable function for creating mocked objects of the PipelineGraph
class.
"""

class MockPipelineGraph(PipelineGraph):
def __repr__(self): # avoid network calls when displaying test results
return object.__repr__(self)

def _make_mock_pipeline_graph():
step1 = make_mock_pipeline_step()
step1.set_name("step1")
step2 = make_mock_pipeline_step()
step2.set_name("step2")
step3 = make_mock_pipeline_step()
step3.set_name("step3")
return MockPipelineGraph(steps={step1, step2, step3})

return _make_mock_pipeline_graph
9 changes: 5 additions & 4 deletions client/verta/tests/unit_tests/deployment/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,15 @@ def test_endpoint_get_current_build(
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture])
@given(build_dicts=st.lists(build_dict(), unique_by=lambda d: d["id"]))
def test_model_version_list_builds(
mock_registered_model_version,
make_mock_registered_model_version,
mock_conn,
mocked_responses,
build_dicts,
):
"""Verify we can construct Build objects from list_builds()."""
rmv = make_mock_registered_model_version()
registry_url = f"{mock_conn.scheme}://{mock_conn.socket}/api/v1/registry"
model_version_url = f"{registry_url}/registered_models/{mock_registered_model_version.registered_model_id}"
model_version_url = f"{registry_url}/registered_models/{rmv.registered_model_id}"
deployment_url = f"{mock_conn.scheme}://{mock_conn.socket}/api/v1/deployment"
list_builds_url = f"{deployment_url}/builds"

Expand All @@ -77,7 +78,7 @@ def test_model_version_list_builds(
status=200,
match=[
query_param_matcher(
{"model_version_id": mock_registered_model_version.id},
{"model_version_id": rmv.id},
),
],
json={"builds": build_dicts},
Expand All @@ -88,7 +89,7 @@ def test_model_version_list_builds(
json={"workspace_id": "123"},
)

builds = mock_registered_model_version.list_builds()
builds = rmv.list_builds()

# verify builds are ordered by creation date
assert [b.id for b in builds] == [
Expand Down
16 changes: 8 additions & 8 deletions client/verta/tests/unit_tests/deployment/test_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def test_kafka_cluster_config_id_default(
mock_endpoint,
mock_conn,
mocked_responses,
mock_registered_model_version,
make_mock_registered_model_version,
) -> None:
"""Verify that, while updating an endpoint, not including a `cluster_config_id`
in the KafkaSettings results in the correct sequence of HTTP requests, including
Expand Down Expand Up @@ -181,7 +181,7 @@ def test_kafka_cluster_config_id_default(
)

mock_endpoint.update(
mock_registered_model_version, kafka_settings=kafka_settings
make_mock_registered_model_version(), kafka_settings=kafka_settings
)
_responses.assert_call_count(get_configs_url, 1)

Expand All @@ -193,7 +193,7 @@ def test_kafka_cluster_config_id_value(
mock_endpoint,
mock_conn,
mocked_responses,
mock_registered_model_version,
make_mock_registered_model_version,
) -> None:
"""Verify that, while updating an endpoint, the provided value for
`cluster_config_id` is used, resulting in the correct sequence of HTTP
Expand Down Expand Up @@ -226,7 +226,7 @@ def test_kafka_cluster_config_id_value(
url=stages_url + f"/{STAGE_ID}", status=200, json={"id": STAGE_ID}
)
mock_endpoint.update(
mock_registered_model_version, kafka_settings=kafka_settings
make_mock_registered_model_version(), kafka_settings=kafka_settings
)

@settings(suppress_health_check=[HealthCheck.function_scoped_fixture])
Expand All @@ -241,7 +241,7 @@ def test_kafka_config_missing_config_id_exception(
mock_endpoint,
mock_conn,
mocked_responses,
mock_registered_model_version,
make_mock_registered_model_version,
) -> None:
"""In the unlikely evert the ID of a found Kafka config is missing from the
backend response, the expected exception is raised.
Expand All @@ -259,7 +259,7 @@ def test_kafka_config_missing_config_id_exception(
_responses.get(url=get_configs_url, status=200, json=kafka_configs_response)
with pytest.raises(RuntimeError) as err:
mock_endpoint.update(
mock_registered_model_version, kafka_settings=kafka_settings
make_mock_registered_model_version(), kafka_settings=kafka_settings
)
assert (
str(err.value)
Expand All @@ -275,7 +275,7 @@ def test_no_kafka_configs_found_exception(
mock_endpoint,
mock_conn,
mocked_responses,
mock_registered_model_version,
make_mock_registered_model_version,
) -> None:
"""If no valid Kafka configurations are found, the expected exception is raised."""
deployment_url = f"{mock_conn.scheme}://{mock_conn.socket}/api/v1/deployment"
Expand All @@ -290,7 +290,7 @@ def test_no_kafka_configs_found_exception(
_responses.get(url=get_configs_url, status=200, json={"configurations": []})
with pytest.raises(RuntimeError) as err:
mock_endpoint.update(
mock_registered_model_version, kafka_settings=kafka_settings
make_mock_registered_model_version(), kafka_settings=kafka_settings
)
assert (
str(err.value)
Expand Down
Loading
Loading