peeringdb 2.4.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.
- peeringdb/__init__.py +117 -0
- peeringdb/_debug/__init__.py +22 -0
- peeringdb/_debug/http.py +8 -0
- peeringdb/_sync.py +111 -0
- peeringdb/_update.py +309 -0
- peeringdb/backend.py +518 -0
- peeringdb/cli.py +67 -0
- peeringdb/client.py +105 -0
- peeringdb/commands.py +386 -0
- peeringdb/config.py +242 -0
- peeringdb/fetch.py +205 -0
- peeringdb/output/__init__.py +0 -0
- peeringdb/output/_dict.py +96 -0
- peeringdb/private.py +26 -0
- peeringdb/py.typed +0 -0
- peeringdb/resource.py +51 -0
- peeringdb/util.py +225 -0
- peeringdb/whois.py +207 -0
- peeringdb-2.4.1.dist-info/METADATA +42 -0
- peeringdb-2.4.1.dist-info/RECORD +23 -0
- peeringdb-2.4.1.dist-info/WHEEL +4 -0
- peeringdb-2.4.1.dist-info/entry_points.txt +2 -0
- peeringdb-2.4.1.dist-info/licenses/LICENSE.txt +24 -0
peeringdb/__init__.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PeeringDB API
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
from importlib import import_module
|
|
8
|
+
from importlib import metadata as importlib_metadata
|
|
9
|
+
from typing import TYPE_CHECKING, Optional, Union
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from peeringdb.backend import Interface
|
|
13
|
+
|
|
14
|
+
from peeringdb.util import get_log_level, str_to_bool
|
|
15
|
+
|
|
16
|
+
__version__: str = importlib_metadata.version("peeringdb")
|
|
17
|
+
_log_level: int = logging.INFO
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _config_logs(
|
|
21
|
+
level: Optional[Union[str, int]] = None,
|
|
22
|
+
name: Optional[str] = None,
|
|
23
|
+
allow_other_loggers: Union[bool, str] = False,
|
|
24
|
+
) -> None:
|
|
25
|
+
"""
|
|
26
|
+
Set up or change logging configuration.
|
|
27
|
+
|
|
28
|
+
_config_logs() => idempotent setup;
|
|
29
|
+
_config_logs(L) => change log level
|
|
30
|
+
"""
|
|
31
|
+
# print('_config_log', 'from %s' %name if name else '')
|
|
32
|
+
logging_format = "%(message)s"
|
|
33
|
+
# maybe better for log files
|
|
34
|
+
# FORMAT='[%(levelname)s]:%(message)s',
|
|
35
|
+
|
|
36
|
+
if isinstance(level, str):
|
|
37
|
+
level = get_log_level(level)
|
|
38
|
+
|
|
39
|
+
global _log_level
|
|
40
|
+
if level:
|
|
41
|
+
_log_level = level
|
|
42
|
+
|
|
43
|
+
if not isinstance(allow_other_loggers, bool):
|
|
44
|
+
try:
|
|
45
|
+
allow_other_loggers = str_to_bool(str(allow_other_loggers))
|
|
46
|
+
except Exception:
|
|
47
|
+
allow_other_loggers = False
|
|
48
|
+
|
|
49
|
+
if not allow_other_loggers:
|
|
50
|
+
# Reset handlers
|
|
51
|
+
for h in list(logging.root.handlers):
|
|
52
|
+
logging.root.removeHandler(h)
|
|
53
|
+
|
|
54
|
+
logging.basicConfig(level=_log_level, format=logging_format, stream=sys.stdout)
|
|
55
|
+
_log = logging.getLogger(__name__)
|
|
56
|
+
_log.setLevel(_log_level)
|
|
57
|
+
|
|
58
|
+
# external
|
|
59
|
+
for log in ["urllib3", "asyncio"]:
|
|
60
|
+
logging.getLogger(log).setLevel(_log_level)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class BackendError(Exception):
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Map external module names to adaptor modules
|
|
68
|
+
SUPPORTED_BACKENDS: dict[str, str] = {
|
|
69
|
+
"django_peeringdb": "django_peeringdb.client_adaptor",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
__backend: Optional[tuple["Interface", tuple[str, str]]] = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def backend_initialized() -> bool:
|
|
76
|
+
return __backend is not None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _get() -> tuple["Interface", tuple[str, str]]:
|
|
80
|
+
global __backend
|
|
81
|
+
if __backend:
|
|
82
|
+
return __backend # type: ignore[unreachable]
|
|
83
|
+
# mypy has trouble with global analysis here
|
|
84
|
+
raise BackendError("Backend not initialized")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_backend() -> "Interface":
|
|
88
|
+
return _get()[0]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_backend_info() -> tuple[str, str]:
|
|
92
|
+
return _get()[1]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def initialize_backend(name: str, **kwargs: Union[str, int, bool, dict]) -> None:
|
|
96
|
+
global __backend
|
|
97
|
+
if __backend:
|
|
98
|
+
raise BackendError("Backend already initialized")
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
modname = SUPPORTED_BACKENDS[name]
|
|
102
|
+
except KeyError:
|
|
103
|
+
raise ValueError(f"Not a supported backend module: '{name}'")
|
|
104
|
+
# Load internal module associated with the ORM module
|
|
105
|
+
supportmod = import_module(modname)
|
|
106
|
+
# Backend is any object returned from load_backend
|
|
107
|
+
backend = supportmod.load_backend(**kwargs)
|
|
108
|
+
|
|
109
|
+
backend.Backend.setup()
|
|
110
|
+
__backend = (backend.Backend(), (name, backend.__version__))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# TODO
|
|
114
|
+
# def is_valid_backend(backend): ...
|
|
115
|
+
|
|
116
|
+
# namespace imports - import client at the end to avoid circular imports
|
|
117
|
+
from peeringdb import client # noqa: E402
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
# import pdb
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def log_validation_errors(backend, e, obj, k):
|
|
7
|
+
log = logging.getLogger("peeringdb.sync")
|
|
8
|
+
log.debug(f"{e} : errors: {e.message_dict}")
|
|
9
|
+
for k, v in e.message_dict.items():
|
|
10
|
+
field = backend.get_field(obj, k)
|
|
11
|
+
try:
|
|
12
|
+
log.debug(f"{k}: {getattr(obj, k)}, dict: {field.__dict__}")
|
|
13
|
+
except backend.object_missing_error():
|
|
14
|
+
log.debug(f"{k}: Missing Object, dict: {field.__dict__}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def try_or_debug(f):
|
|
18
|
+
try:
|
|
19
|
+
return f()
|
|
20
|
+
except Exception:
|
|
21
|
+
# pdb.set_trace() ???
|
|
22
|
+
raise
|
peeringdb/_debug/http.py
ADDED
peeringdb/_sync.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sync implementation module
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from typing import TYPE_CHECKING, Union
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from peeringdb.backend import Interface
|
|
10
|
+
|
|
11
|
+
from peeringdb.util import group_fields
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _field_resource(backend: "Interface", concrete: type, field: object) -> type:
|
|
15
|
+
field_name = getattr(field, "name", None)
|
|
16
|
+
if field_name is None:
|
|
17
|
+
raise ValueError("Field has no name")
|
|
18
|
+
field_concrete = backend.get_field_concrete(concrete, field_name)
|
|
19
|
+
if not isinstance(field_concrete, type):
|
|
20
|
+
raise ValueError(f"Expected type, got {type(field_concrete)}")
|
|
21
|
+
return backend.get_resource(field_concrete)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get_subrow(
|
|
25
|
+
row: dict[str, Union[str, int, bool, list, dict]], fname: str, field: object
|
|
26
|
+
) -> tuple[str, Union[str, int, bool, list, dict, None]]:
|
|
27
|
+
key = getattr(field, "column", None)
|
|
28
|
+
if key is not None and isinstance(key, str):
|
|
29
|
+
subrow = row.get(key)
|
|
30
|
+
else:
|
|
31
|
+
key = fname
|
|
32
|
+
subrow = None
|
|
33
|
+
if subrow is None: # e.g. use "org" if "org_id" is missing
|
|
34
|
+
key = fname
|
|
35
|
+
try:
|
|
36
|
+
subrow = row[key]
|
|
37
|
+
except KeyError:
|
|
38
|
+
subrow = None
|
|
39
|
+
return key, subrow
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def extract_relations(
|
|
43
|
+
backend: "Interface", res: type, row: dict[str, Union[str, int, bool, list, dict]]
|
|
44
|
+
) -> tuple[
|
|
45
|
+
dict[type, dict[Union[str, int], dict[str, Union[str, int, bool, list, dict]]]],
|
|
46
|
+
dict[type, set[Union[str, int]]],
|
|
47
|
+
]:
|
|
48
|
+
field_groups = group_fields(backend, backend.get_concrete(res))
|
|
49
|
+
# Already-fetched, and id-only refs
|
|
50
|
+
fetched: dict = defaultdict(dict)
|
|
51
|
+
dangling = defaultdict(set)
|
|
52
|
+
|
|
53
|
+
# Handle subrows that might be shallow (id) or deep (dict)
|
|
54
|
+
def _handle_subrow(resource, subrow):
|
|
55
|
+
if isinstance(subrow, dict):
|
|
56
|
+
pk = subrow["id"]
|
|
57
|
+
fetched[resource][pk] = subrow
|
|
58
|
+
elif subrow is None:
|
|
59
|
+
return
|
|
60
|
+
else:
|
|
61
|
+
pk = subrow
|
|
62
|
+
dangling[resource].add(pk)
|
|
63
|
+
return pk
|
|
64
|
+
|
|
65
|
+
for fname, field in field_groups["single_refs"].items():
|
|
66
|
+
fieldres = _field_resource(backend, backend.get_concrete(res), field)
|
|
67
|
+
_, subrow = _get_subrow(row, fname, field)
|
|
68
|
+
_handle_subrow(fieldres, subrow)
|
|
69
|
+
|
|
70
|
+
for fname, field in field_groups["many_refs"].items():
|
|
71
|
+
fieldres = _field_resource(backend, backend.get_concrete(res), field)
|
|
72
|
+
many_data = row.get(fname, [])
|
|
73
|
+
if isinstance(many_data, list):
|
|
74
|
+
for subrow in many_data:
|
|
75
|
+
_handle_subrow(fieldres, subrow)
|
|
76
|
+
|
|
77
|
+
return fetched, dangling
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def set_single_relations(
|
|
81
|
+
backend: "Interface",
|
|
82
|
+
res: type,
|
|
83
|
+
obj: object,
|
|
84
|
+
row: dict[str, Union[str, int, bool, list, dict]],
|
|
85
|
+
) -> None:
|
|
86
|
+
field_groups = group_fields(backend, backend.get_concrete(res))
|
|
87
|
+
for fname, field in field_groups["single_refs"].items():
|
|
88
|
+
key, subrow = _get_subrow(row, fname, field)
|
|
89
|
+
if isinstance(subrow, dict):
|
|
90
|
+
pk = subrow["id"]
|
|
91
|
+
else:
|
|
92
|
+
pk = subrow
|
|
93
|
+
setattr(obj, key, pk)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def set_many_relations(
|
|
97
|
+
backend: "Interface",
|
|
98
|
+
res: type,
|
|
99
|
+
obj: object,
|
|
100
|
+
row: dict[str, Union[str, int, bool, list, dict]],
|
|
101
|
+
) -> None:
|
|
102
|
+
field_groups = group_fields(backend, backend.get_concrete(res))
|
|
103
|
+
for fname, field in field_groups["many_refs"].items():
|
|
104
|
+
fieldres = _field_resource(backend, backend.get_concrete(res), field)
|
|
105
|
+
pks_data = row.get(fname, [])
|
|
106
|
+
if isinstance(pks_data, list):
|
|
107
|
+
pks = pks_data
|
|
108
|
+
else:
|
|
109
|
+
pks = []
|
|
110
|
+
objs = [backend.get_object(backend.get_concrete(fieldres), pk) for pk in pks]
|
|
111
|
+
backend.set_relation_many_to_many(obj, fname, objs)
|
peeringdb/_update.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module defining main interface classes for sync
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import TYPE_CHECKING, Optional, Union
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
from peeringdb import config, get_backend
|
|
13
|
+
from peeringdb._sync import extract_relations, set_many_relations, set_single_relations
|
|
14
|
+
from peeringdb.fetch import Fetcher
|
|
15
|
+
from peeringdb.private import private_data_has_been_fetched
|
|
16
|
+
from peeringdb.util import group_fields, log_error
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Updater:
|
|
20
|
+
"""
|
|
21
|
+
Handles initial and incremental update from a PeeringDB remote API
|
|
22
|
+
to the local backend.
|
|
23
|
+
|
|
24
|
+
The updater is responsible for creating and updating objects in the local
|
|
25
|
+
backend. It does this by fetching objects from the remote API and creating
|
|
26
|
+
or updating the corresponding objects in the local backend.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, fetcher: Fetcher):
|
|
30
|
+
self._log = logging.getLogger(__name__)
|
|
31
|
+
self.resources: dict = {}
|
|
32
|
+
self.backend = get_backend()
|
|
33
|
+
self.fetcher = fetcher
|
|
34
|
+
self.config = config.load_config()
|
|
35
|
+
|
|
36
|
+
def copy_object(self, new):
|
|
37
|
+
"""
|
|
38
|
+
Copies a new object to an existing one
|
|
39
|
+
:return:
|
|
40
|
+
"""
|
|
41
|
+
old = self.backend.get_object(new.__class__, new.id)
|
|
42
|
+
for field in self.backend.get_fields(new.__class__):
|
|
43
|
+
try:
|
|
44
|
+
setattr(old, field.name, getattr(new, field.name))
|
|
45
|
+
except TypeError as e:
|
|
46
|
+
if "Direct assignment" in str(e):
|
|
47
|
+
pass # Ignore refs
|
|
48
|
+
else:
|
|
49
|
+
raise
|
|
50
|
+
|
|
51
|
+
self.clean_obj(old)
|
|
52
|
+
self.backend.save(old)
|
|
53
|
+
|
|
54
|
+
def clean_obj(self, obj):
|
|
55
|
+
"""
|
|
56
|
+
Run object through backend validation
|
|
57
|
+
|
|
58
|
+
Will raise an exception if the object is not valid
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
self.backend.clean(obj)
|
|
63
|
+
except self.backend.validation_error() as e:
|
|
64
|
+
# e.message_dict contains field names as keys and lists of errors as values
|
|
65
|
+
for errors in e.message_dict.values():
|
|
66
|
+
for error in errors:
|
|
67
|
+
# Checking if the error message is the one we want to ignore
|
|
68
|
+
# field is allowed to be None in the db, but not blank according
|
|
69
|
+
# to backend validation. We ignore this error since the data is
|
|
70
|
+
# already validated and writing None to the db is fine.
|
|
71
|
+
if error != "This field cannot be blank.":
|
|
72
|
+
raise e
|
|
73
|
+
|
|
74
|
+
def create_obj(
|
|
75
|
+
self, row: dict[str, Union[str, int, bool, list, dict]], res: type
|
|
76
|
+
) -> tuple[object, bool]: # noqa: C901
|
|
77
|
+
"""
|
|
78
|
+
Create a model instance from a row
|
|
79
|
+
:param row: Object from API
|
|
80
|
+
:param res: Resource to create
|
|
81
|
+
"""
|
|
82
|
+
_, dangling = extract_relations(self.backend, res, row)
|
|
83
|
+
for resource, pks in dangling.items():
|
|
84
|
+
for pk in pks:
|
|
85
|
+
# Check if we have it
|
|
86
|
+
rel_obj = None
|
|
87
|
+
try:
|
|
88
|
+
self.backend.get_object(self.backend.get_concrete(resource), pk)
|
|
89
|
+
except self.backend.object_missing_error(
|
|
90
|
+
self.backend.get_concrete(resource)
|
|
91
|
+
):
|
|
92
|
+
# We dont have the dangling relationship, so we try to fetch it
|
|
93
|
+
# from the api and create it.
|
|
94
|
+
|
|
95
|
+
self._log.info("Fetching dangling relationship %s %s", resource, pk)
|
|
96
|
+
related_row = self.fetcher.get(resource.tag, int(pk))
|
|
97
|
+
|
|
98
|
+
# instantiate the relationship object
|
|
99
|
+
|
|
100
|
+
rel_obj, _ = self.create_obj(related_row, resource)
|
|
101
|
+
try:
|
|
102
|
+
self.clean_obj(rel_obj)
|
|
103
|
+
except self.backend.validation_error() as e:
|
|
104
|
+
self._log.error(
|
|
105
|
+
"Failed to clean dangling object %s %s: %s", resource, pk, e
|
|
106
|
+
)
|
|
107
|
+
return None, False
|
|
108
|
+
|
|
109
|
+
# save the relationship object
|
|
110
|
+
|
|
111
|
+
if rel_obj:
|
|
112
|
+
self.backend.save(rel_obj)
|
|
113
|
+
|
|
114
|
+
# Initialize object
|
|
115
|
+
field_groups = group_fields(self.backend, self.backend.get_concrete(res))
|
|
116
|
+
try:
|
|
117
|
+
obj = self.backend.get_object(self.backend.get_concrete(res), row["id"])
|
|
118
|
+
except self.backend.object_missing_error(self.backend.get_concrete(res)):
|
|
119
|
+
tbl = self.backend.get_concrete(res)
|
|
120
|
+
obj = tbl()
|
|
121
|
+
|
|
122
|
+
# set_scalars
|
|
123
|
+
for fname, field in field_groups["scalars"].items():
|
|
124
|
+
value = row.get(fname, getattr(obj, fname, None))
|
|
125
|
+
value = self.backend.convert_field(obj.__class__, fname, value)
|
|
126
|
+
|
|
127
|
+
# TODO: datetimes are strings for some reason
|
|
128
|
+
if (
|
|
129
|
+
fname in ["created", "updated", "rir_status_updated", "ixf_last_import"]
|
|
130
|
+
and isinstance(value, str)
|
|
131
|
+
) or (getattr(obj, "tzinfo", None) is not None):
|
|
132
|
+
value = datetime.fromisoformat(value.rstrip("Z"))
|
|
133
|
+
# elif (isinstance(value, str) and "T" in value and
|
|
134
|
+
# "Z" in value and "-" in value):
|
|
135
|
+
# print("NOT DATETIME", fname, value, type(value))
|
|
136
|
+
|
|
137
|
+
# Remove timezone info
|
|
138
|
+
if isinstance(value, datetime):
|
|
139
|
+
value = value.replace(tzinfo=None)
|
|
140
|
+
|
|
141
|
+
setattr(obj, fname, value)
|
|
142
|
+
self._log.debug(" %s: %s (%s)", fname, value, type(value))
|
|
143
|
+
|
|
144
|
+
set_single_relations(self.backend, res, obj, row)
|
|
145
|
+
set_many_relations(self.backend, res, obj, row)
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
self.clean_obj(obj)
|
|
149
|
+
except self.backend.validation_error() as e:
|
|
150
|
+
self._log.debug("[%s] Failed to clean %s %s: %s", res.tag, res, obj, e)
|
|
151
|
+
if "already exists" in str(e):
|
|
152
|
+
self.update_collision(res, row, e)
|
|
153
|
+
self.clean_obj(obj)
|
|
154
|
+
return obj, True
|
|
155
|
+
|
|
156
|
+
return obj, False
|
|
157
|
+
|
|
158
|
+
def _handle_initial_sync(self, entries: list, res):
|
|
159
|
+
"""
|
|
160
|
+
Called during the first sync of a resource
|
|
161
|
+
|
|
162
|
+
This will do a batch create of all objects in the resource
|
|
163
|
+
:param entries: List of objects from API
|
|
164
|
+
:param res: Resource to sync
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
objs = []
|
|
168
|
+
for row in entries:
|
|
169
|
+
try:
|
|
170
|
+
obj, _ = self.create_obj(row, res)
|
|
171
|
+
objs.append(obj)
|
|
172
|
+
except self.backend.object_missing_error(self.backend.get_concrete(res)):
|
|
173
|
+
try:
|
|
174
|
+
obj, _ = self.create_obj(row, res)
|
|
175
|
+
self.backend.save(obj)
|
|
176
|
+
except Exception as e:
|
|
177
|
+
obj_id = row.get("id", "Unknown")
|
|
178
|
+
self._log.info(f"Error creating {res.tag} with id {obj_id}: {e}")
|
|
179
|
+
log_error(self.config, res.tag, row.get("id", "Unknown"), str(e))
|
|
180
|
+
except Exception as e:
|
|
181
|
+
obj_id = row.get("id", "Unknown")
|
|
182
|
+
self._log.info(f"Error updating {res.tag} with id {obj_id}: {e}")
|
|
183
|
+
log_error(self.config, res.tag, row.get("id", "Unknown"), str(e))
|
|
184
|
+
|
|
185
|
+
self.backend.get_concrete(res).objects.bulk_create(objs)
|
|
186
|
+
|
|
187
|
+
def _handle_incremental_sync(self, entries: list, res):
|
|
188
|
+
"""
|
|
189
|
+
Called during an incremental sync of a resource (i.e. not the first sync)
|
|
190
|
+
|
|
191
|
+
Entries will only contain objects that have changed since the last sync
|
|
192
|
+
:param entries: List of objects from API
|
|
193
|
+
:param res: Resource to sync
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
for row in entries:
|
|
197
|
+
try:
|
|
198
|
+
self.backend.get_object(self.backend.get_concrete(res), row["id"])
|
|
199
|
+
obj, _ = self.create_obj(row, res)
|
|
200
|
+
self.copy_object(obj)
|
|
201
|
+
except self.backend.object_missing_error(self.backend.get_concrete(res)):
|
|
202
|
+
try:
|
|
203
|
+
obj, _ = self.create_obj(row, res)
|
|
204
|
+
self.backend.save(obj)
|
|
205
|
+
except Exception as e:
|
|
206
|
+
obj_id = row.get("id", "Unknown")
|
|
207
|
+
self._log.info(f"Error creating {res.tag} with id {obj_id}: {e}")
|
|
208
|
+
log_error(self.config, res.tag, row.get("id", "Unknown"), str(e))
|
|
209
|
+
except Exception as e:
|
|
210
|
+
obj_id = row.get("id", "Unknown")
|
|
211
|
+
self._log.info(f"Error updating {res.tag} with id {obj_id}: {e}")
|
|
212
|
+
log_error(self.config, res.tag, row.get("id", "Unknown"), str(e))
|
|
213
|
+
|
|
214
|
+
def update_all(
|
|
215
|
+
self,
|
|
216
|
+
rs: list[type],
|
|
217
|
+
since: Optional[int] = None,
|
|
218
|
+
skip: Union[list[str], None] = None,
|
|
219
|
+
fetch_private: bool = False,
|
|
220
|
+
):
|
|
221
|
+
"""
|
|
222
|
+
Update all objects of a given type
|
|
223
|
+
:param rs: List of resources to update
|
|
224
|
+
:param since: Unix timestamp of last update
|
|
225
|
+
:param skip: List of resource tags to skip
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
for res in rs:
|
|
229
|
+
if skip is not None:
|
|
230
|
+
for i in skip:
|
|
231
|
+
self.fetcher.load(i, since)
|
|
232
|
+
|
|
233
|
+
if skip and res.tag in skip:
|
|
234
|
+
self._log.info("[%s] Skipping", res.tag)
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
if since is None:
|
|
238
|
+
_since = self.backend.last_change(self.backend.get_concrete(res))
|
|
239
|
+
else:
|
|
240
|
+
_since = since
|
|
241
|
+
|
|
242
|
+
initial_private = False
|
|
243
|
+
if fetch_private:
|
|
244
|
+
initial_private = not private_data_has_been_fetched(self.backend, res)
|
|
245
|
+
|
|
246
|
+
self.fetcher.load(
|
|
247
|
+
res.tag,
|
|
248
|
+
_since + 1 if _since and isinstance(_since, int) else None,
|
|
249
|
+
fetch_private=fetch_private,
|
|
250
|
+
initial_private=initial_private,
|
|
251
|
+
)
|
|
252
|
+
entries = self.fetcher.entries(res.tag)
|
|
253
|
+
self._log.info("[%s] Processing %d objects", res.tag, len(entries))
|
|
254
|
+
|
|
255
|
+
if not _since:
|
|
256
|
+
self._handle_initial_sync(entries, res)
|
|
257
|
+
else:
|
|
258
|
+
self._handle_incremental_sync(entries, res)
|
|
259
|
+
|
|
260
|
+
def update_one(self, res, pk: int, depth=0):
|
|
261
|
+
"""
|
|
262
|
+
Update a single object
|
|
263
|
+
:param res: Resource to update
|
|
264
|
+
:param pk: Primary key of object to update
|
|
265
|
+
:param depth: Depth of recursion
|
|
266
|
+
:return:
|
|
267
|
+
"""
|
|
268
|
+
if depth != 0:
|
|
269
|
+
# no longer relevant, deprecation warning
|
|
270
|
+
self._log.warning(
|
|
271
|
+
"update_one: depth parameter is not used and will be removed "
|
|
272
|
+
"in a future version"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
row = self.fetcher.get(res.tag, pk, depth=0, force_fetch=True)
|
|
276
|
+
# create object instance (unsaved)
|
|
277
|
+
obj, _ = self.create_obj(row, res)
|
|
278
|
+
try:
|
|
279
|
+
# attempt update existing instance of object (if exists, will save)
|
|
280
|
+
self.copy_object(obj)
|
|
281
|
+
except self.backend.object_missing_error(self.backend.get_concrete(res)):
|
|
282
|
+
# object does not exist, create object instance and save as
|
|
283
|
+
# new object
|
|
284
|
+
obj, _ = self.create_obj(row, res)
|
|
285
|
+
self.backend.save(obj)
|
|
286
|
+
|
|
287
|
+
def update_collision(self, res, row: dict, exc: Exception):
|
|
288
|
+
"""
|
|
289
|
+
Sometimes we encounter edge-case collisions triggered by a
|
|
290
|
+
unique constraint validation error. This function attempts to
|
|
291
|
+
resolve the collision by updating the colliding object from the api
|
|
292
|
+
:param res: Resource to update
|
|
293
|
+
:param row: Row from API
|
|
294
|
+
:param exc: Exception raised by validation error
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
filters = {}
|
|
298
|
+
for field, error_message in exc.error_dict.items():
|
|
299
|
+
if "already exists" in str(error_message):
|
|
300
|
+
filters[field] = row[field]
|
|
301
|
+
|
|
302
|
+
for field, value in filters.items():
|
|
303
|
+
self._log.debug(
|
|
304
|
+
"[%s] Updating collision %s %s=%s", res.tag, res, field, value
|
|
305
|
+
)
|
|
306
|
+
collision = self.backend.get_object_by(
|
|
307
|
+
self.backend.get_concrete(res), field, value
|
|
308
|
+
)
|
|
309
|
+
self.update_one(res, collision.id)
|