Skip to content

Commit

Permalink
refactor: move retry logic into the connection implementation (DEV-3110
Browse files Browse the repository at this point in the history
…) (#686)

Co-authored-by: Johannes Nussbaum <39048939+jnussbaum@users.noreply.github.com>
  • Loading branch information
BalduinLandolt and jnussbaum committed Dec 15, 2023
1 parent 71b754d commit f1a7051
Show file tree
Hide file tree
Showing 22 changed files with 169 additions and 292 deletions.
38 changes: 15 additions & 23 deletions src/dsp_tools/commands/project/create/project_create.py
Expand Up @@ -21,7 +21,7 @@
from dsp_tools.models.langstring import LangString
from dsp_tools.utils.connection import Connection
from dsp_tools.utils.create_logger import get_logger
from dsp_tools.utils.shared import login, parse_json_input, try_network_action
from dsp_tools.utils.shared import login, parse_json_input

logger = get_logger(__name__)

Expand Down Expand Up @@ -57,7 +57,7 @@ def _create_project_on_server(
with contextlib.suppress(BaseError):
# the normal, expected case is that this block fails
project_local = Project(con=con, shortcode=shortcode)
project_remote: Project = try_network_action(project_local.read)
project_remote: Project = project_local.read()
proj_designation = f"'{project_remote.shortname}' ({project_remote.shortcode})"
msg = f"Project {proj_designation} already exists on the DSP server. Updating it..."
print(f" WARNING: {msg}")
Expand Down Expand Up @@ -89,7 +89,7 @@ def _create_project_on_server(
status=True,
)
try:
project_remote = try_network_action(project_local.create)
project_remote = project_local.create()
except BaseError:
err_msg = f"Cannot create project '{shortname}' ({shortcode}) on DSP server."
logger.error(err_msg, exc_info=True)
Expand Down Expand Up @@ -134,7 +134,7 @@ def _update_basic_info_of_project(

# make the call to DSP-API
try:
project_remote: Project = try_network_action(project.update)
project_remote: Project = project.update()
if verbose:
print(f" Updated project '{shortname}' ({shortcode}).")
logger.info(f"Updated project '{shortname}' ({shortcode}).")
Expand Down Expand Up @@ -171,9 +171,7 @@ def _create_groups(
overall_success = True
current_project_groups: dict[str, Group] = {}
try:
remote_groups: list[Group] = try_network_action(
lambda: Group.getAllGroupsForProject(con=con, proj_iri=str(project.iri))
)
remote_groups = Group.getAllGroupsForProject(con=con, proj_iri=str(project.iri))
except BaseError:
err_msg = (
"Unable to check if group names are already existing on DSP server, because it is "
Expand Down Expand Up @@ -205,7 +203,7 @@ def _create_groups(
selfjoin=bool(group.get("selfjoin", False)),
)
try:
group_remote: Group = try_network_action(group_local.create)
group_remote: Group = group_local.create()
except BaseError:
print(f" WARNING: Unable to create group '{group_name}'.")
logger.warning(f"Unable to create group '{group_name}'.", exc_info=True)
Expand Down Expand Up @@ -282,7 +280,7 @@ def _get_group_iris_for_user(
# full_group_name refers to an already existing group on DSP
try:
# "remote_groups" might be available from a previous loop cycle
remote_groups = remote_groups or try_network_action(lambda: Group.getAllGroups(con=con))
remote_groups = remote_groups or Group.getAllGroups(con=con)
except BaseError:
err_msg = (
f"User '{username}' is referring to the group {full_group_name} that "
Expand Down Expand Up @@ -350,7 +348,7 @@ def _get_projects_where_user_is_admin(
# full_project_name refers to an already existing project on DSP
try:
# "remote_projects" might be available from a previous loop cycle
remote_projects = remote_projects or try_network_action(lambda: current_project.getAllProjects(con=con))
remote_projects = remote_projects or current_project.getAllProjects(con=con)
except BaseError:
err_msg = (
f"User '{username}' cannot be added to the projects {json_user_definition['projects']} "
Expand Down Expand Up @@ -407,7 +405,7 @@ def _create_users(
# skip the user if he already exists
with contextlib.suppress(BaseError):
# the normal case is that this block fails
try_network_action(User(con, email=json_user_definition["email"]).read)
User(con, email=json_user_definition["email"]).read()
print(f" WARNING: User '{username}' already exists on the DSP server. Skipping...")
logger.warning(f"User '{username}' already exists on the DSP server. Skipping...")
overall_success = False
Expand Down Expand Up @@ -448,7 +446,7 @@ def _create_users(
in_groups=group_iris,
)
try:
try_network_action(user_local.create)
user_local.create()
except BaseError:
print(f" WARNING: Unable to create user '{username}'.")
logger.warning(f"Unable to create user '{username}'.", exc_info=True)
Expand Down Expand Up @@ -578,7 +576,7 @@ def _create_ontology(
comment=onto_comment,
)
try:
ontology_remote: Ontology = try_network_action(ontology_local.create)
ontology_remote = ontology_local.create()
except BaseError:
# if ontology cannot be created, let the error escalate
logger.error(f"ERROR while trying to create ontology '{onto_name}'.", exc_info=True)
Expand Down Expand Up @@ -638,9 +636,7 @@ def _create_ontologies(
print("Create ontologies...")
logger.info("Create ontologies...")
try:
project_ontologies: list[Ontology] = try_network_action(
lambda: Ontology.getProjectOntologies(con=con, project_id=str(project_remote.iri))
)
project_ontologies = Ontology.getProjectOntologies(con=con, project_id=str(project_remote.iri))
except BaseError:
err_msg = "Unable to retrieve remote ontologies. Cannot check if your ontology already exists."
print("WARNING: {err_msg}")
Expand Down Expand Up @@ -750,10 +746,7 @@ def _add_resource_classes_to_remote_ontology(
comment=LangString(res_class.get("comments")) if res_class.get("comments") else None,
)
try:
last_modification_date, res_class_remote = try_network_action(
res_class_local.create, last_modification_date
)
res_class_remote = cast(ResourceClass, res_class_remote)
last_modification_date, res_class_remote = res_class_local.create(last_modification_date)
new_res_classes[str(res_class_remote.iri)] = res_class_remote
ontology_remote.lastModificationDate = last_modification_date
if verbose:
Expand Down Expand Up @@ -849,7 +842,7 @@ def _add_property_classes_to_remote_ontology(
comment=LangString(prop_class["comments"]) if prop_class.get("comments") else None,
)
try:
last_modification_date, _ = try_network_action(prop_class_local.create, last_modification_date)
last_modification_date, _ = prop_class_local.create(last_modification_date)
ontology_remote.lastModificationDate = last_modification_date
if verbose:
print(f" Created property class '{prop_class['name']}'")
Expand Down Expand Up @@ -915,8 +908,7 @@ def _add_cardinalities_to_resource_classes(
qualified_propname = knora_api_prefix + card_info["propname"]

try:
last_modification_date = try_network_action(
res_class_remote.addProperty,
last_modification_date = res_class_remote.addProperty(
property_id=qualified_propname,
cardinality=switcher[card_info["cardinality"]],
gui_order=card_info.get("gui_order"),
Expand Down
10 changes: 4 additions & 6 deletions src/dsp_tools/commands/project/create/project_create_lists.py
Expand Up @@ -7,7 +7,7 @@
from dsp_tools.models.exceptions import BaseError, UserError
from dsp_tools.utils.connection import Connection
from dsp_tools.utils.create_logger import get_logger
from dsp_tools.utils.shared import login, parse_json_input, try_network_action
from dsp_tools.utils.shared import login, parse_json_input

logger = get_logger(__name__)

Expand Down Expand Up @@ -48,7 +48,7 @@ def _create_list_node(
parent=parent_node,
)
try:
new_node = try_network_action(new_node.create)
new_node = new_node.create()
except BaseError:
print(f"WARNING: Cannot create list node '{node['name']}'.")
logger.warning("Cannot create list node '{node['name']}'.", exc_info=True)
Expand Down Expand Up @@ -98,9 +98,7 @@ def create_lists_on_server(

# retrieve existing lists
try:
existing_lists: list[ListNode] = try_network_action(
lambda: ListNode.getAllLists(con=con, project_iri=project_remote.iri)
)
existing_lists = ListNode.getAllLists(con=con, project_iri=project_remote.iri)
except BaseError:
err_msg = "Unable to retrieve existing lists on DSP server. Cannot check if your lists are already existing."
print(f"WARNING: {err_msg}")
Expand Down Expand Up @@ -191,7 +189,7 @@ def create_lists(
shortcode = project_definition["project"]["shortcode"]
project_local = Project(con=con, shortcode=shortcode)
try:
project_remote = try_network_action(project_local.read)
project_remote = project_local.read()
except BaseError:
err_msg = f"Unable to create the lists: The project {shortcode} cannot be found on the DSP server."
logger.error(err_msg, exc_info=True)
Expand Down
2 changes: 1 addition & 1 deletion src/dsp_tools/commands/project/models/group.py
Expand Up @@ -234,7 +234,7 @@ def getAllGroups(con: Connection) -> list[Group]:
return [Group.fromJsonObj(con, group_item) for group_item in result["groups"]]

@staticmethod
def getAllGroupsForProject(con: Connection, proj_iri: str) -> Optional[list[Group]]:
def getAllGroupsForProject(con: Connection, proj_iri: str) -> list[Group]:
return [g for g in Group.getAllGroups(con) if g.project == proj_iri]

def createDefinitionFileObj(self) -> dict[str, Any]:
Expand Down
1 change: 0 additions & 1 deletion src/dsp_tools/commands/project/models/listnode.py
Expand Up @@ -418,7 +418,6 @@ def create(self) -> ListNode:
:return: JSON-object from DSP-API
"""

jsonobj = self.toJsonObj(Actions.Create)
jsondata = json.dumps(jsonobj, cls=SetEncoder)
if self._parent:
Expand Down
6 changes: 0 additions & 6 deletions src/dsp_tools/commands/project/models/ontology.py
Expand Up @@ -305,12 +305,6 @@ def create(self) -> "Ontology":
result = self._con.post(Ontology.ROUTE, jsondata)
return Ontology.fromJsonObj(self._con, result)

def update(self) -> "Ontology":
jsonobj = self.toJsonObj(Actions.Update)
jsondata = json.dumps(jsonobj, cls=SetEncoder, indent=4)
result = self._con.put(Ontology.ROUTE + "/metadata", jsondata, "application/ld+json")
return Ontology.fromJsonObj(self._con, result)

def read(self) -> "Ontology":
result = self._con.get(Ontology.ROUTE + "/allentities/" + quote_plus(self._iri) + Ontology.ALL_LANGUAGES)
return Ontology.fromJsonObj(self._con, result)
Expand Down
2 changes: 0 additions & 2 deletions src/dsp_tools/commands/project/models/project.py
Expand Up @@ -413,7 +413,6 @@ def create(self) -> Project:
:return: JSON-object from DSP
"""

jsonobj = self.toJsonObj(Actions.Create)
jsondata = json.dumps(jsonobj, cls=SetEncoder)
result = self._con.post(Project.ROUTE, jsondata)
Expand Down Expand Up @@ -446,7 +445,6 @@ def update(self) -> Project:
Returns: JSON object returned as response from DSP reflecting the update
"""

jsonobj = self.toJsonObj(Actions.Update)
jsondata = json.dumps(jsonobj, cls=SetEncoder)
result = self._con.put(Project.IRI + quote_plus(self.iri), jsondata)
Expand Down
1 change: 0 additions & 1 deletion src/dsp_tools/commands/project/models/user.py
Expand Up @@ -545,7 +545,6 @@ def create(self) -> User:
:return: JSON-object from DSP
"""

jsonobj = self.toJsonObj(Actions.Create)
jsondata = json.dumps(jsonobj)
result = self._con.post(User.ROUTE, jsondata)
Expand Down
5 changes: 2 additions & 3 deletions src/dsp_tools/commands/xmlupload/list_client.py
Expand Up @@ -6,7 +6,6 @@

from dsp_tools.utils.connection import Connection
from dsp_tools.utils.create_logger import get_logger
from dsp_tools.utils.shared import try_network_action

logger = get_logger(__name__)

Expand Down Expand Up @@ -82,7 +81,7 @@ def _get_list_info_from_server(con: Connection, project_iri: str) -> ProjectList

def _get_list_iris_from_server(con: Connection, project_iri: str) -> list[str]:
iri = quote_plus(project_iri)
res: dict[str, Any] = try_network_action(con.get, f"/admin/lists?projectIri={iri}")
res = con.get(f"/admin/lists?projectIri={iri}")
lists: list[dict[str, Any]] = res["lists"]
logger.info(f"Found {len(lists)} lists for project")
return [lst["id"] for lst in lists]
Expand All @@ -91,7 +90,7 @@ def _get_list_iris_from_server(con: Connection, project_iri: str) -> list[str]:
def _get_list_from_server(con: Connection, list_iri: str) -> List:
logger.info(f"Retrieving nodes of list {list_iri}")
iri = quote_plus(list_iri)
res = try_network_action(con.get, f"/admin/lists/{iri}")
res = con.get(f"/admin/lists/{iri}")
list_object: dict[str, Any] = res["list"]
list_info = list_object["listinfo"]
children: list[dict[str, Any]] = list_object["children"]
Expand Down
93 changes: 8 additions & 85 deletions src/dsp_tools/commands/xmlupload/models/sipi.py
@@ -1,39 +1,20 @@
import json
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Any

import requests

from dsp_tools.utils.connection_live import check_for_api_error
from dsp_tools.utils.connection import Connection


@dataclass(frozen=True)
class Sipi:
"""
A Sipi instance represents a connection to a SIPI server.
Attributes:
sipi_server: address of the server, e.g https://iiif.dasch.swiss
token: session token received by the server after login
dump: if True, every request is written into a file
dump_directory: directory where the HTTP requests are written
A wrapper type around a connection to a SIPI server.
Provides functionality to upload bitstreams files to the SIPI server.
"""

sipi_server: str
token: str
dump: bool = False
dump_directory = Path("HTTP requests")

def __post_init__(self) -> None:
"""
Create dumping directory (if applicable)
"""
if self.dump:
self.dump_directory.mkdir(exist_ok=True)
con: Connection

def upload_bitstream(self, filepath: str) -> dict[Any, Any]:
def upload_bitstream(self, filepath: Path) -> dict[str, Any]:
"""
Uploads a bitstream to the Sipi server
Expand All @@ -44,65 +25,7 @@ def upload_bitstream(self, filepath: str) -> dict[Any, Any]:
API response
"""
with open(filepath, "rb") as bitstream_file:
files = {"file": (Path(filepath).name, bitstream_file)}
url = self.sipi_server + "/upload"
headers = {"Authorization": "Bearer " + self.token}
files = {"file": (filepath.name, bitstream_file)}
timeout = 5 * 60
response = requests.post(
url=url,
headers=headers,
files=files,
timeout=timeout,
)
if self.dump:
self.write_request_to_file(
method="POST",
url=url,
headers=headers,
filepath=filepath,
timeout=timeout,
response=response,
)
check_for_api_error(response)
res: dict[Any, Any] = response.json()
return res

def write_request_to_file(
self,
method: str,
url: str,
headers: dict[str, str],
filepath: str,
timeout: int,
response: requests.Response,
) -> None:
"""
Write the request and response to a file.
Args:
method: HTTP method of the request (GET, POST, PUT, DELETE)
url: complete URL (server + route of SIPI) that was called
headers: headers of the HTTP request
filepath: path to the file that was uploaded
timeout: timeout of the HTTP request
response: response of the server
"""
if response.status_code == 200:
_return = response.json()
else:
_return = {"status": response.status_code, "message": response.text}
dumpobj = {
"SIPI server": self.sipi_server,
"url": url,
"method": method,
"filepath": filepath,
"timeout": timeout,
"headers": headers,
"return-headers": dict(response.headers),
"return": _return,
}
timestamp = datetime.now().strftime("%Y-%m-%d %H.%M.%S.%f")
route_for_filename = url.replace(self.sipi_server, "").replace("/", "_")
filename = f"{timestamp} {method} SIPI {route_for_filename} {filepath.replace('/', '_')}.json"
with open(self.dump_directory / filename, "w", encoding="utf-8") as f:
json.dump(dumpobj, f, indent=4)
res = self.con.post(route="/upload", files=files, timeout=timeout)
return res

0 comments on commit f1a7051

Please sign in to comment.