ckanapi-harvesters 0.0.0__py3-none-any.whl → 0.0.3__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.
- ckanapi_harvesters/__init__.py +32 -10
- ckanapi_harvesters/auxiliary/__init__.py +26 -0
- ckanapi_harvesters/auxiliary/ckan_action.py +93 -0
- ckanapi_harvesters/auxiliary/ckan_api_key.py +213 -0
- ckanapi_harvesters/auxiliary/ckan_auxiliary.py +293 -0
- ckanapi_harvesters/auxiliary/ckan_configuration.py +50 -0
- ckanapi_harvesters/auxiliary/ckan_defs.py +10 -0
- ckanapi_harvesters/auxiliary/ckan_errors.py +129 -0
- ckanapi_harvesters/auxiliary/ckan_map.py +509 -0
- ckanapi_harvesters/auxiliary/ckan_model.py +992 -0
- ckanapi_harvesters/auxiliary/ckan_vocabulary_deprecated.py +104 -0
- ckanapi_harvesters/auxiliary/deprecated.py +82 -0
- ckanapi_harvesters/auxiliary/error_level_message.py +51 -0
- ckanapi_harvesters/auxiliary/external_code_import.py +98 -0
- ckanapi_harvesters/auxiliary/list_records.py +60 -0
- ckanapi_harvesters/auxiliary/login.py +163 -0
- ckanapi_harvesters/auxiliary/path.py +208 -0
- ckanapi_harvesters/auxiliary/proxy_config.py +298 -0
- ckanapi_harvesters/auxiliary/urls.py +40 -0
- ckanapi_harvesters/builder/__init__.py +40 -0
- ckanapi_harvesters/builder/builder_aux.py +20 -0
- ckanapi_harvesters/builder/builder_ckan.py +238 -0
- ckanapi_harvesters/builder/builder_errors.py +36 -0
- ckanapi_harvesters/builder/builder_field.py +122 -0
- ckanapi_harvesters/builder/builder_package.py +9 -0
- ckanapi_harvesters/builder/builder_package_1_basic.py +1291 -0
- ckanapi_harvesters/builder/builder_package_2_harvesters.py +40 -0
- ckanapi_harvesters/builder/builder_package_3_multi_threaded.py +45 -0
- ckanapi_harvesters/builder/builder_package_example.xlsx +0 -0
- ckanapi_harvesters/builder/builder_resource.py +589 -0
- ckanapi_harvesters/builder/builder_resource_datastore.py +561 -0
- ckanapi_harvesters/builder/builder_resource_datastore_multi_abc.py +367 -0
- ckanapi_harvesters/builder/builder_resource_datastore_multi_folder.py +273 -0
- ckanapi_harvesters/builder/builder_resource_datastore_multi_harvester.py +278 -0
- ckanapi_harvesters/builder/builder_resource_datastore_unmanaged.py +145 -0
- ckanapi_harvesters/builder/builder_resource_datastore_url.py +150 -0
- ckanapi_harvesters/builder/builder_resource_init.py +126 -0
- ckanapi_harvesters/builder/builder_resource_multi_abc.py +361 -0
- ckanapi_harvesters/builder/builder_resource_multi_datastore.py +146 -0
- ckanapi_harvesters/builder/builder_resource_multi_file.py +505 -0
- ckanapi_harvesters/builder/example/__init__.py +21 -0
- ckanapi_harvesters/builder/example/builder_example.py +21 -0
- ckanapi_harvesters/builder/example/builder_example_aux_fun.py +24 -0
- ckanapi_harvesters/builder/example/builder_example_download.py +44 -0
- ckanapi_harvesters/builder/example/builder_example_generate_data.py +73 -0
- ckanapi_harvesters/builder/example/builder_example_patch_upload.py +51 -0
- ckanapi_harvesters/builder/example/builder_example_policy.py +114 -0
- ckanapi_harvesters/builder/example/builder_example_test_sql.py +53 -0
- ckanapi_harvesters/builder/example/builder_example_tests.py +87 -0
- ckanapi_harvesters/builder/example/builder_example_tests_offline.py +57 -0
- ckanapi_harvesters/builder/example/package/ckan-dpg.svg +74 -0
- ckanapi_harvesters/builder/example/package/users_local.csv +3 -0
- ckanapi_harvesters/builder/mapper_datastore.py +93 -0
- ckanapi_harvesters/builder/mapper_datastore_multi.py +262 -0
- ckanapi_harvesters/builder/specific/__init__.py +11 -0
- ckanapi_harvesters/builder/specific/configuration_builder.py +66 -0
- ckanapi_harvesters/builder/specific_builder_abc.py +23 -0
- ckanapi_harvesters/ckan_api/__init__.py +20 -0
- ckanapi_harvesters/ckan_api/ckan_api.py +11 -0
- ckanapi_harvesters/ckan_api/ckan_api_0_base.py +896 -0
- ckanapi_harvesters/ckan_api/ckan_api_1_map.py +1028 -0
- ckanapi_harvesters/ckan_api/ckan_api_2_readonly.py +934 -0
- ckanapi_harvesters/ckan_api/ckan_api_3_policy.py +229 -0
- ckanapi_harvesters/ckan_api/ckan_api_4_readwrite.py +579 -0
- ckanapi_harvesters/ckan_api/ckan_api_5_manage.py +1225 -0
- ckanapi_harvesters/ckan_api/ckan_api_params.py +192 -0
- ckanapi_harvesters/ckan_api/deprecated/__init__.py +9 -0
- ckanapi_harvesters/ckan_api/deprecated/ckan_api_deprecated.py +267 -0
- ckanapi_harvesters/ckan_api/deprecated/ckan_api_deprecated_vocabularies.py +189 -0
- ckanapi_harvesters/harvesters/__init__.py +23 -0
- ckanapi_harvesters/harvesters/data_cleaner/__init__.py +17 -0
- ckanapi_harvesters/harvesters/data_cleaner/data_cleaner_abc.py +240 -0
- ckanapi_harvesters/harvesters/data_cleaner/data_cleaner_errors.py +23 -0
- ckanapi_harvesters/harvesters/data_cleaner/data_cleaner_upload.py +9 -0
- ckanapi_harvesters/harvesters/data_cleaner/data_cleaner_upload_1_basic.py +430 -0
- ckanapi_harvesters/harvesters/data_cleaner/data_cleaner_upload_2_geom.py +98 -0
- ckanapi_harvesters/harvesters/file_formats/__init__.py +10 -0
- ckanapi_harvesters/harvesters/file_formats/csv_format.py +43 -0
- ckanapi_harvesters/harvesters/file_formats/file_format_abc.py +39 -0
- ckanapi_harvesters/harvesters/file_formats/file_format_init.py +25 -0
- ckanapi_harvesters/harvesters/file_formats/shp_format.py +129 -0
- ckanapi_harvesters/harvesters/harvester_abc.py +190 -0
- ckanapi_harvesters/harvesters/harvester_errors.py +31 -0
- ckanapi_harvesters/harvesters/harvester_init.py +30 -0
- ckanapi_harvesters/harvesters/harvester_model.py +49 -0
- ckanapi_harvesters/harvesters/harvester_params.py +323 -0
- ckanapi_harvesters/harvesters/postgre_harvester.py +495 -0
- ckanapi_harvesters/harvesters/postgre_params.py +86 -0
- ckanapi_harvesters/harvesters/pymongo_data_cleaner.py +173 -0
- ckanapi_harvesters/harvesters/pymongo_harvester.py +355 -0
- ckanapi_harvesters/harvesters/pymongo_params.py +54 -0
- ckanapi_harvesters/policies/__init__.py +20 -0
- ckanapi_harvesters/policies/data_format_policy.py +269 -0
- ckanapi_harvesters/policies/data_format_policy_abc.py +97 -0
- ckanapi_harvesters/policies/data_format_policy_custom_fields.py +156 -0
- ckanapi_harvesters/policies/data_format_policy_defs.py +135 -0
- ckanapi_harvesters/policies/data_format_policy_errors.py +79 -0
- ckanapi_harvesters/policies/data_format_policy_lists.py +234 -0
- ckanapi_harvesters/policies/data_format_policy_tag_groups.py +35 -0
- ckanapi_harvesters/reports/__init__.py +11 -0
- ckanapi_harvesters/reports/admin_report.py +292 -0
- {ckanapi_harvesters-0.0.0.dist-info → ckanapi_harvesters-0.0.3.dist-info}/METADATA +84 -38
- ckanapi_harvesters-0.0.3.dist-info/RECORD +105 -0
- ckanapi_harvesters/divider/__init__.py +0 -27
- ckanapi_harvesters/divider/divider.py +0 -53
- ckanapi_harvesters/divider/divider_error.py +0 -59
- ckanapi_harvesters/main.py +0 -30
- ckanapi_harvesters-0.0.0.dist-info/RECORD +0 -9
- {ckanapi_harvesters-0.0.0.dist-info → ckanapi_harvesters-0.0.3.dist-info}/WHEEL +0 -0
- {ckanapi_harvesters-0.0.0.dist-info → ckanapi_harvesters-0.0.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,992 @@
|
|
|
1
|
+
#!python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Data model to represent a CKAN database architecture
|
|
5
|
+
"""
|
|
6
|
+
import datetime
|
|
7
|
+
import re
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from enum import IntEnum, IntFlag
|
|
10
|
+
from typing import List, Dict, Union, Tuple
|
|
11
|
+
from warnings import warn
|
|
12
|
+
import copy
|
|
13
|
+
from collections import OrderedDict
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
from ckanapi_harvesters.auxiliary.ckan_auxiliary import assert_or_raise, _bool_from_string, bytes_to_megabytes
|
|
18
|
+
from ckanapi_harvesters.auxiliary.ckan_auxiliary import CkanFieldInternalAttrs
|
|
19
|
+
from ckanapi_harvesters.auxiliary.ckan_errors import IntegrityError, MissingIdError
|
|
20
|
+
from ckanapi_harvesters.auxiliary.ckan_auxiliary import dict_recursive_update
|
|
21
|
+
|
|
22
|
+
ckan_package_name_re = "^[0-9a-z-_]*$"
|
|
23
|
+
|
|
24
|
+
## Enumerations ------------------
|
|
25
|
+
class UpsertChoice(IntEnum):
|
|
26
|
+
Insert = 1
|
|
27
|
+
Update = 2
|
|
28
|
+
Upsert = 3
|
|
29
|
+
|
|
30
|
+
def __str__(self):
|
|
31
|
+
return self.name.lower()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CkanFieldTypeABC(ABC):
|
|
35
|
+
@staticmethod
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def from_str(s):
|
|
38
|
+
raise NotImplementedError()
|
|
39
|
+
|
|
40
|
+
class CkanFieldType(str, CkanFieldTypeABC):
|
|
41
|
+
"""
|
|
42
|
+
Role previously managed by CkanFieldTypeEnum, but accepts any string
|
|
43
|
+
"""
|
|
44
|
+
@staticmethod
|
|
45
|
+
def from_str(s):
|
|
46
|
+
return CkanFieldType(s)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CkanFieldTypeEnum(IntEnum): #, CkanFieldTypeABC):
|
|
50
|
+
"""
|
|
51
|
+
Enumeration of types encountered during development + documentation
|
|
52
|
+
"""
|
|
53
|
+
# CKAN web interface
|
|
54
|
+
Text = 1
|
|
55
|
+
Numeric = 2
|
|
56
|
+
TimeStamp = 3
|
|
57
|
+
# [CKAN documentation](https://docs.ckan.org/en/2.9/maintaining/datastore.html#field-types)
|
|
58
|
+
json = 30
|
|
59
|
+
date = 11 # This type is used to represent a calendar date (year, month, day). The oldest date that can be represented is 4713 BC and the latest date is 5874897 AD. The resolution is 1 day.
|
|
60
|
+
time = 12 # This type is used to represent a time of day without time zone. The low value is 00:00:00 and the high value is 24:00:00 with a resolution of 1 microsecond.
|
|
61
|
+
int = 13
|
|
62
|
+
float = 14
|
|
63
|
+
bool = 15
|
|
64
|
+
# [Postgre documentation](https://www.postgresql.org/docs/9.1/datatype.html)
|
|
65
|
+
timetz = 20 # time of day, including time zone
|
|
66
|
+
timestamptz = 21 # date and time, including time zone
|
|
67
|
+
# structured data
|
|
68
|
+
jsonb = 31
|
|
69
|
+
xml = 32
|
|
70
|
+
# numeric types
|
|
71
|
+
int2 = 101
|
|
72
|
+
integer = int # alias
|
|
73
|
+
int4 = int # alias
|
|
74
|
+
int8 = 102
|
|
75
|
+
bigint = int8 # alias
|
|
76
|
+
int16 = 103
|
|
77
|
+
int32 = 104
|
|
78
|
+
int64 = 105
|
|
79
|
+
float4 = 106 # real
|
|
80
|
+
float8 = 107 # double
|
|
81
|
+
money = 108
|
|
82
|
+
# decimal [ (p, s) ] : exact numeric of selectable precision
|
|
83
|
+
# other types
|
|
84
|
+
bit = 200 # fixed-length bit string
|
|
85
|
+
char = 201 # This type is used to represent fixed-length, space padded strings of the specified width. Storing character strings longer than the specified length will result in an error unless the excess characters are spaces, in which case the string will be truncated to the maximum length. If the string to be stored is shorter than the declared length, the value will be space padded.
|
|
86
|
+
varbit = 202 # variable-length
|
|
87
|
+
varchar = 203 # variable-length
|
|
88
|
+
bytea = 204 # This type is used to represent binary strings (a “byte array”). A binary string is a sequence of octets (or bytes). Unlike character strings, binary strings allow storing octets of value zero and other non-printable octets (outside the range 32 to 126).
|
|
89
|
+
# identifiers
|
|
90
|
+
serial4 = 150 # This type is used to represent an auto incrementing four-byte integer. The range is 1 to 2147483647. This is similar to specifying an integer column that has default values to be assigned from a sequence generator. It also has a NOT NULL constraint applied to it.
|
|
91
|
+
serial = serial4 # alias
|
|
92
|
+
serial8 = 151
|
|
93
|
+
bigserial = serial8 # alias
|
|
94
|
+
uuid = 152 # This type is used to represent Universally Unique Identifiers (UUIDs). These identifiers are 128-bit values generated by an algorithm. A UUID is a sequence of lower-case hexadecimal digits in several groups separated by hyphens. Specifically, it is a group of 8 digits, followed by three groups of 4 digits, followed by a group of 12 digits. An example of a UUID in this standard form is a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11.
|
|
95
|
+
oid = 153 # numeric object identifier. It is currently implemented as an unsigned four-byte integer. Its use as a primary key in a user-created table is discouraged. OIDs are best used only for references to system tables.
|
|
96
|
+
macaddr = 154
|
|
97
|
+
inet = 155
|
|
98
|
+
cidr = 156
|
|
99
|
+
# geometries, on a plane
|
|
100
|
+
point = 220
|
|
101
|
+
path = 221
|
|
102
|
+
polygon = 222
|
|
103
|
+
box = 223
|
|
104
|
+
circle = 224
|
|
105
|
+
lseg = 225 # line segment
|
|
106
|
+
line = 226
|
|
107
|
+
# PostGRE extensions:
|
|
108
|
+
geometry = 228 # PostGIS
|
|
109
|
+
bson = 230
|
|
110
|
+
|
|
111
|
+
def __str__(self):
|
|
112
|
+
return self.name.lower()
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def from_str(s):
|
|
116
|
+
s = s.lower().strip()
|
|
117
|
+
if s == "text":
|
|
118
|
+
return CkanFieldTypeEnum.Text
|
|
119
|
+
elif s == "numeric":
|
|
120
|
+
return CkanFieldTypeEnum.Numeric
|
|
121
|
+
elif s == "timestamp":
|
|
122
|
+
return CkanFieldTypeEnum.TimeStamp
|
|
123
|
+
# elif s == "" or ((not isinstance(s, str)) and np.isnan(s)):
|
|
124
|
+
# return CkanFieldTypeEnum.Default
|
|
125
|
+
elif hasattr(CkanFieldTypeEnum, s): # other attributes are lower case
|
|
126
|
+
return getattr(CkanFieldTypeEnum, s)
|
|
127
|
+
else:
|
|
128
|
+
raise ValueError(s)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class CkanState(IntEnum):
|
|
132
|
+
Draft = 0
|
|
133
|
+
Active = 1
|
|
134
|
+
Deleted = 2
|
|
135
|
+
|
|
136
|
+
def __str__(self):
|
|
137
|
+
return self.name.lower()
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def from_str(s):
|
|
141
|
+
s = s.lower().strip()
|
|
142
|
+
if s == "active":
|
|
143
|
+
return CkanState.Active
|
|
144
|
+
elif s == "draft":
|
|
145
|
+
return CkanState.Draft
|
|
146
|
+
elif s == "deleted":
|
|
147
|
+
return CkanState.Deleted
|
|
148
|
+
else:
|
|
149
|
+
raise ValueError(s)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class CkanVisibility(IntEnum):
|
|
153
|
+
Private = 0
|
|
154
|
+
Public = 1
|
|
155
|
+
|
|
156
|
+
def __str__(self):
|
|
157
|
+
return self.name.lower()
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def from_str(s):
|
|
161
|
+
s = s.lower().strip()
|
|
162
|
+
if s == "private":
|
|
163
|
+
return CkanVisibility.Private
|
|
164
|
+
elif s == "public":
|
|
165
|
+
return CkanVisibility.Public
|
|
166
|
+
else:
|
|
167
|
+
raise ValueError(s)
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
def from_bool_is_private(value):
|
|
171
|
+
if value:
|
|
172
|
+
return CkanVisibility.Private
|
|
173
|
+
else:
|
|
174
|
+
return CkanVisibility.Public
|
|
175
|
+
|
|
176
|
+
def to_bool_is_private(self):
|
|
177
|
+
return CkanVisibility.Private == self.value
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class CkanLicenseDomain(IntFlag):
|
|
181
|
+
NoDomain = 0
|
|
182
|
+
Software = 1
|
|
183
|
+
Data = 2
|
|
184
|
+
Content = 4
|
|
185
|
+
|
|
186
|
+
@staticmethod
|
|
187
|
+
def from_bool(*, domain_software:bool=False, domain_data:bool=False, domain_content:bool=False) -> "CkanLicenseDomain":
|
|
188
|
+
flag = CkanLicenseDomain.NoDomain
|
|
189
|
+
if domain_software:
|
|
190
|
+
flag = flag | CkanLicenseDomain.Software
|
|
191
|
+
if domain_data:
|
|
192
|
+
flag = flag | CkanLicenseDomain.Data
|
|
193
|
+
if domain_content:
|
|
194
|
+
flag = flag | CkanLicenseDomain.Content
|
|
195
|
+
return flag
|
|
196
|
+
|
|
197
|
+
def to_dict(self) -> dict:
|
|
198
|
+
return OrderedDict([
|
|
199
|
+
("domain_software", self.value & CkanLicenseDomain.Software > 0),
|
|
200
|
+
("domain_data", self.value & CkanLicenseDomain.Data > 0),
|
|
201
|
+
("domain_content", self.value & CkanLicenseDomain.Content > 0),
|
|
202
|
+
])
|
|
203
|
+
|
|
204
|
+
@staticmethod
|
|
205
|
+
def from_dict(d: dict) -> "CkanLicenseDomain":
|
|
206
|
+
return CkanLicenseDomain.from_bool(domain_software=_bool_from_string(d["domain_software"]),
|
|
207
|
+
domain_data=_bool_from_string(d["domain_data"]),
|
|
208
|
+
domain_content=_bool_from_string(d["domain_content"]))
|
|
209
|
+
|
|
210
|
+
class CkanCapacity(IntEnum):
|
|
211
|
+
Excluded = 0
|
|
212
|
+
Member = 1
|
|
213
|
+
Editor = 2 # only for collaborators of a package
|
|
214
|
+
Admin = 3 # only for members of a group
|
|
215
|
+
SysAdmin = 4
|
|
216
|
+
Public = 5 # to notify access is publicly available
|
|
217
|
+
|
|
218
|
+
def __str__(self):
|
|
219
|
+
return self.name.lower()
|
|
220
|
+
|
|
221
|
+
@staticmethod
|
|
222
|
+
def from_str(s):
|
|
223
|
+
s = s.lower().strip()
|
|
224
|
+
if s == "excluded":
|
|
225
|
+
return CkanCapacity.Excluded
|
|
226
|
+
elif s == "member":
|
|
227
|
+
return CkanCapacity.Member
|
|
228
|
+
elif s == "editor":
|
|
229
|
+
return CkanCapacity.Editor
|
|
230
|
+
elif s == "admin":
|
|
231
|
+
return CkanCapacity.Admin
|
|
232
|
+
elif s == "sysadmin":
|
|
233
|
+
return CkanCapacity.SysAdmin
|
|
234
|
+
elif s == "public":
|
|
235
|
+
return CkanCapacity.Public
|
|
236
|
+
else:
|
|
237
|
+
raise ValueError(s)
|
|
238
|
+
|
|
239
|
+
class CkanConfigurableObjectABC(ABC):
|
|
240
|
+
mandatory_attributes: set = None
|
|
241
|
+
configurable_attributes: set = None
|
|
242
|
+
extra_attributes: set = set()
|
|
243
|
+
|
|
244
|
+
@staticmethod
|
|
245
|
+
@abstractmethod
|
|
246
|
+
def get_resource_type() -> str:
|
|
247
|
+
raise NotImplementedError()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
## Field class ------------------
|
|
251
|
+
class CkanField(CkanConfigurableObjectABC):
|
|
252
|
+
"""
|
|
253
|
+
Object representation of a CKAN Field configuration
|
|
254
|
+
"""
|
|
255
|
+
mandatory_attributes = {"name"}
|
|
256
|
+
configurable_attributes = {"name", "notes", "label"}
|
|
257
|
+
|
|
258
|
+
# TODO: implement schema part of dict? e.g. {'index_name': None, 'is_index': False, 'native_type': 'numeric', 'notnull': False, 'uniquekey': False}
|
|
259
|
+
def __init__(self, name:str, data_type:str, *, native_type:str=None, notes:str=None,
|
|
260
|
+
type_override:bool=False, label:str=None):
|
|
261
|
+
if native_type is None:
|
|
262
|
+
native_type = data_type
|
|
263
|
+
self.name:str = name
|
|
264
|
+
self.data_type:Union[CkanFieldType,None] = CkanFieldType.from_str(data_type)
|
|
265
|
+
self.type_override:Union[bool,None] = type_override
|
|
266
|
+
self.label:Union[str,None] = label
|
|
267
|
+
self.notes:Union[str,None] = notes
|
|
268
|
+
self.is_index:Union[bool,None] = None
|
|
269
|
+
self.uniquekey:Union[bool,None] = None
|
|
270
|
+
self.notnull:Union[bool,None] = None
|
|
271
|
+
self.internal_attrs: CkanFieldInternalAttrs = CkanFieldInternalAttrs()
|
|
272
|
+
self.details:dict = {}
|
|
273
|
+
self.details = self.to_ckan_dict()
|
|
274
|
+
self.internal_attrs.init_from_native_type(self.data_type)
|
|
275
|
+
|
|
276
|
+
def __str__(self):
|
|
277
|
+
return f"Field '{self.name}' <{self.data_type}>"
|
|
278
|
+
|
|
279
|
+
def copy(self) -> "CkanField":
|
|
280
|
+
return copy.deepcopy(self)
|
|
281
|
+
|
|
282
|
+
def merge(self, new_values):
|
|
283
|
+
dest = self.copy()
|
|
284
|
+
if new_values.name is not None:
|
|
285
|
+
dest.name = new_values.name
|
|
286
|
+
if new_values.data_type is not None:
|
|
287
|
+
dest.data_type = new_values.data_type
|
|
288
|
+
if new_values.type_override is not None:
|
|
289
|
+
dest.type_override = new_values.type_override
|
|
290
|
+
if new_values.label is not None:
|
|
291
|
+
dest.label = new_values.label
|
|
292
|
+
if new_values.notes is not None:
|
|
293
|
+
dest.notes = new_values.notes
|
|
294
|
+
if new_values.is_index is not None:
|
|
295
|
+
dest.is_index = new_values.is_index
|
|
296
|
+
if new_values.uniquekey is not None:
|
|
297
|
+
dest.uniquekey = new_values.uniquekey
|
|
298
|
+
if new_values.notnull is not None:
|
|
299
|
+
dest.notnull = new_values.notnull
|
|
300
|
+
dest.internal_attrs = self.internal_attrs.merge(new_values.internal_attrs)
|
|
301
|
+
dest.details = dict_recursive_update(self.details, new_values.details)
|
|
302
|
+
return dest
|
|
303
|
+
|
|
304
|
+
def __eq__(self, other) -> bool:
|
|
305
|
+
equality = True
|
|
306
|
+
equality &= self.name == other.name
|
|
307
|
+
equality &= self.data_type == other.data_type
|
|
308
|
+
equality &= self.type_override == other.type_override
|
|
309
|
+
equality &= self.label == other.label
|
|
310
|
+
equality &= self.notes == other.notes
|
|
311
|
+
equality &= self.is_index == other.is_index
|
|
312
|
+
equality &= self.uniquekey == other.uniquekey
|
|
313
|
+
equality &= self.notnull == other.notnull
|
|
314
|
+
equality &= self.internal_attrs == other.internal_attrs
|
|
315
|
+
return equality
|
|
316
|
+
|
|
317
|
+
@staticmethod
|
|
318
|
+
def get_resource_type() -> str:
|
|
319
|
+
return "Field"
|
|
320
|
+
|
|
321
|
+
def to_ckan_dict(self, include_details:bool=True) -> dict:
|
|
322
|
+
d = dict()
|
|
323
|
+
if self.details is not None and include_details:
|
|
324
|
+
d.update(self.details)
|
|
325
|
+
d["id"] = self.name
|
|
326
|
+
if self.data_type is not None:
|
|
327
|
+
d["type"] = str(self.data_type)
|
|
328
|
+
field_info = self.details["info"] if include_details and self.details is not None and "info" in self.details.keys() else {}
|
|
329
|
+
if self.type_override:
|
|
330
|
+
field_info["type_override"] = str(self.data_type)
|
|
331
|
+
if self.label is not None:
|
|
332
|
+
field_info["label"] = self.label
|
|
333
|
+
if self.notes is not None:
|
|
334
|
+
field_info["notes"] = self.notes
|
|
335
|
+
if len(field_info) > 0:
|
|
336
|
+
d["info"] = field_info
|
|
337
|
+
schema_info = self.details["schema"] if include_details and self.details is not None and "schema" in self.details.keys() else {}
|
|
338
|
+
if self.is_index is not None:
|
|
339
|
+
schema_info["is_index"] = self.is_index
|
|
340
|
+
if self.uniquekey is not None:
|
|
341
|
+
schema_info["uniquekey"] = self.uniquekey
|
|
342
|
+
if self.notnull is not None:
|
|
343
|
+
schema_info["notnull"] = self.notnull
|
|
344
|
+
if self.data_type is not None:
|
|
345
|
+
schema_info["native_type"] = str(self.data_type)
|
|
346
|
+
if len(schema_info) > 0:
|
|
347
|
+
d["schema"] = schema_info
|
|
348
|
+
return d
|
|
349
|
+
|
|
350
|
+
@staticmethod
|
|
351
|
+
def from_ckan_dict(d:dict) -> "CkanField":
|
|
352
|
+
obj = CkanField(d["id"], d["type"])
|
|
353
|
+
obj.details = d
|
|
354
|
+
if "info" in d.keys():
|
|
355
|
+
field_info = d["info"]
|
|
356
|
+
if "type_override" in field_info.keys():
|
|
357
|
+
if isinstance(field_info["type_override"], str):
|
|
358
|
+
if len(field_info["type_override"]) > 0:
|
|
359
|
+
# the API usually returns a string representing the type override, equal to d["type"]
|
|
360
|
+
if not d["type"] == field_info["type_override"]:
|
|
361
|
+
obj.data_type = CkanFieldType.from_str(field_info["type_override"])
|
|
362
|
+
msg = f"Inconsistency between data type and type override for field {obj.name} (data: {d['type']} vs. override: {field_info['type_override']})"
|
|
363
|
+
warn(msg)
|
|
364
|
+
obj.type_override = True
|
|
365
|
+
elif isinstance(field_info["type_override"], bool):
|
|
366
|
+
obj.type_override = field_info["type_override"]
|
|
367
|
+
else:
|
|
368
|
+
obj.type_override = field_info["type_override"] > 0
|
|
369
|
+
if "label" in field_info.keys():
|
|
370
|
+
obj.label = field_info["label"]
|
|
371
|
+
if "notes" in field_info.keys():
|
|
372
|
+
obj.notes = field_info["notes"]
|
|
373
|
+
if "schema" in d.keys():
|
|
374
|
+
schema_info = d["schema"]
|
|
375
|
+
if "is_index" in schema_info.keys():
|
|
376
|
+
obj.is_index = schema_info["is_index"]
|
|
377
|
+
if "uniquekey" in schema_info.keys():
|
|
378
|
+
obj.uniquekey = schema_info["uniquekey"]
|
|
379
|
+
if "notnull" in schema_info.keys():
|
|
380
|
+
obj.notnull = schema_info["notnull"]
|
|
381
|
+
if "native_type" in schema_info.keys():
|
|
382
|
+
obj.data_type = CkanFieldType.from_str(schema_info["native_type"])
|
|
383
|
+
obj.internal_attrs.init_from_native_type(obj.data_type)
|
|
384
|
+
return obj
|
|
385
|
+
|
|
386
|
+
def to_dict(self, include_details:bool=True) -> dict:
|
|
387
|
+
return self.to_ckan_dict(include_details=include_details)
|
|
388
|
+
|
|
389
|
+
@staticmethod
|
|
390
|
+
def from_dict(d:dict) -> "CkanField":
|
|
391
|
+
return CkanField.from_ckan_dict(d)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class CkanAliasInfo:
|
|
395
|
+
def __init__(self, d:dict=None):
|
|
396
|
+
self.id: Union[str,None] = None
|
|
397
|
+
self.name: str = ""
|
|
398
|
+
self.alias_of: Union[str,None] = None
|
|
399
|
+
self.details:dict = d
|
|
400
|
+
if d is not None:
|
|
401
|
+
self.id: Union[str, None] = d["id"] if "id" in d.keys() else None
|
|
402
|
+
self.name: str = d["name"]
|
|
403
|
+
self.alias_of: Union[str, None] = d["alias_of"]
|
|
404
|
+
|
|
405
|
+
def __str__(self):
|
|
406
|
+
return f"Alias {self.name} of id {self.alias_of}"
|
|
407
|
+
|
|
408
|
+
def copy(self) -> "CkanAliasInfo":
|
|
409
|
+
return copy.deepcopy(self)
|
|
410
|
+
|
|
411
|
+
@staticmethod
|
|
412
|
+
def from_dict(d:dict) -> "CkanAliasInfo":
|
|
413
|
+
return CkanAliasInfo(d)
|
|
414
|
+
|
|
415
|
+
def to_dict(self, include_details:bool=True) -> dict:
|
|
416
|
+
d = dict()
|
|
417
|
+
if self.details is not None and include_details:
|
|
418
|
+
d.update(self.details)
|
|
419
|
+
d.update({"name": self.name, "alias_of": self.alias_of})
|
|
420
|
+
if id is not None:
|
|
421
|
+
d["id"] = id
|
|
422
|
+
return d
|
|
423
|
+
|
|
424
|
+
## Users and groups ------------------
|
|
425
|
+
class CkanUserInfo:
|
|
426
|
+
def __init__(self, d: dict):
|
|
427
|
+
self.id: str = d["id"]
|
|
428
|
+
self.name: str = d["name"]
|
|
429
|
+
self.display_name: str = d["display_name"]
|
|
430
|
+
self.fullname: str = d["fullname"]
|
|
431
|
+
self.about: str = d["about"]
|
|
432
|
+
self.sysadmin: bool = d["sysadmin"]
|
|
433
|
+
self.state: CkanState = CkanState.from_str(d["state"])
|
|
434
|
+
self.created: Union[datetime.datetime, None] = datetime.datetime.fromisoformat(
|
|
435
|
+
d["created"]) if "created" in d.keys() else None
|
|
436
|
+
self.last_active: Union[datetime.datetime, None] = datetime.datetime.fromisoformat(
|
|
437
|
+
d["last_active"]) if "last_active" in d.keys() else None
|
|
438
|
+
self.organizations: Union[None,List[str]] = None # used by consolidate (detailed_report)
|
|
439
|
+
self.details: dict = d
|
|
440
|
+
|
|
441
|
+
def __str__(self):
|
|
442
|
+
return f"User '{self.name}' ({self.id})"
|
|
443
|
+
|
|
444
|
+
@staticmethod
|
|
445
|
+
def get_resource_type() -> str:
|
|
446
|
+
return "User"
|
|
447
|
+
|
|
448
|
+
def to_dict(self, include_details: bool = True) -> dict:
|
|
449
|
+
d = dict()
|
|
450
|
+
if self.details is not None and include_details:
|
|
451
|
+
d.update(self.details)
|
|
452
|
+
d.update({"id": self.id, "name": self.name, "display_name": self.display_name, "fullname": self.fullname,
|
|
453
|
+
"about": self.about, "sysadmin": self.sysadmin, "state": str(self.state),
|
|
454
|
+
"created": self.created.isoformat() if self.created is not None else None,
|
|
455
|
+
"last_active": self.last_active.isoformat() if self.last_active is not None else None})
|
|
456
|
+
return d
|
|
457
|
+
|
|
458
|
+
@staticmethod
|
|
459
|
+
def from_dict(d: dict) -> "CkanUserInfo":
|
|
460
|
+
return CkanUserInfo(d)
|
|
461
|
+
|
|
462
|
+
def copy(self) -> "CkanUserInfo":
|
|
463
|
+
return copy.deepcopy(self)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
class CkanGroupInfo:
|
|
467
|
+
def __init__(self, d:dict):
|
|
468
|
+
self.id:str = d["id"]
|
|
469
|
+
self.name:str = d["name"]
|
|
470
|
+
self.title:str = d["title"]
|
|
471
|
+
self.description:str = d["description"]
|
|
472
|
+
self.package_count:Union[None,int] = d.get("package_count")
|
|
473
|
+
self.details:dict = d
|
|
474
|
+
# to be initialized with specific requests:
|
|
475
|
+
self.user_members:Union[dict[str,CkanCapacity],None] = None
|
|
476
|
+
self.package_members:Union[dict[str,CkanCapacity],None] = None
|
|
477
|
+
|
|
478
|
+
def __str__(self):
|
|
479
|
+
return f"Group '{self.title}' ({self.id})"
|
|
480
|
+
|
|
481
|
+
@staticmethod
|
|
482
|
+
def get_resource_type() -> str:
|
|
483
|
+
return "Group"
|
|
484
|
+
|
|
485
|
+
def to_dict(self, include_details:bool=True) -> dict:
|
|
486
|
+
d = dict()
|
|
487
|
+
if self.details is not None and include_details:
|
|
488
|
+
d.update(self.details)
|
|
489
|
+
d.update({"id": self.id, "name": self.name, "title": self.title, "description": self.description})
|
|
490
|
+
return d
|
|
491
|
+
|
|
492
|
+
@staticmethod
|
|
493
|
+
def from_dict(d:dict) -> "CkanGroupInfo":
|
|
494
|
+
return CkanGroupInfo(d)
|
|
495
|
+
|
|
496
|
+
def copy(self) -> "CkanGroupInfo":
|
|
497
|
+
return copy.deepcopy(self)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
## Package, resources and views map class ------------------
|
|
501
|
+
class CkanDataStoreInfo:
|
|
502
|
+
def __init__(self, d:dict=None):
|
|
503
|
+
self.resource_id:Union[str,None] = None
|
|
504
|
+
self.row_count: Union[int,None] = None
|
|
505
|
+
self.fields_id_list:Union[List[str],None] = None
|
|
506
|
+
self.fields_dict:Union[OrderedDict[str,CkanField],None] = None
|
|
507
|
+
self.index_fields:Union[List[str],None] = None
|
|
508
|
+
self.aliases:Union[List[str],None] = None
|
|
509
|
+
self.table_size_mb:Union[float,None] = None
|
|
510
|
+
self.index_size_mb:Union[float,None] = None
|
|
511
|
+
self.details:dict = d
|
|
512
|
+
if d is not None:
|
|
513
|
+
self.resource_id:str = d["meta"]["id"]
|
|
514
|
+
if "aliases" in d["meta"].keys():
|
|
515
|
+
self.aliases = d["meta"]["aliases"]
|
|
516
|
+
if "count" in d["meta"].keys():
|
|
517
|
+
self.row_count:int = d["meta"]["count"]
|
|
518
|
+
self.table_size_mb = bytes_to_megabytes(d["meta"]["size"])
|
|
519
|
+
self.index_size_mb = bytes_to_megabytes(d["meta"]["idx_size"])
|
|
520
|
+
# what does the field meta.db_size represent?
|
|
521
|
+
if "fields" in d.keys():
|
|
522
|
+
self.fields_id_list:List[str] = [e["id"] for e in d["fields"]]
|
|
523
|
+
self.fields_dict = OrderedDict()
|
|
524
|
+
for e in d["fields"]:
|
|
525
|
+
self.fields_dict[e["id"]] = CkanField.from_ckan_dict(d=e)
|
|
526
|
+
self.index_fields:List[str] = [e.name for e in self.fields_dict.values() if e.is_index]
|
|
527
|
+
|
|
528
|
+
def __str__(self):
|
|
529
|
+
return f"DataStore of resource id {self.resource_id}"
|
|
530
|
+
|
|
531
|
+
def get_basic_field_list_dict(self):
|
|
532
|
+
return [{"id": id} for id in self.fields_id_list]
|
|
533
|
+
|
|
534
|
+
def get_recomp_field_list_dict(self):
|
|
535
|
+
return [self.fields_dict[id].to_ckan_dict() for id in self.fields_id_list]
|
|
536
|
+
|
|
537
|
+
def get_original_field_list_dict(self):
|
|
538
|
+
return [self.fields_dict[id].details for id in self.fields_id_list]
|
|
539
|
+
|
|
540
|
+
def copy(self) -> "CkanDataStoreInfo":
|
|
541
|
+
return copy.deepcopy(self)
|
|
542
|
+
|
|
543
|
+
def to_dict(self, include_details:bool=True) -> dict:
|
|
544
|
+
d = dict()
|
|
545
|
+
if self.details is not None and include_details:
|
|
546
|
+
d.update(self.details)
|
|
547
|
+
if self.fields_dict is not None:
|
|
548
|
+
d["fields"] = [field.to_dict(include_details=include_details) for field in self.fields_dict.values()]
|
|
549
|
+
if "meta" not in d.keys():
|
|
550
|
+
d["meta"] = {}
|
|
551
|
+
d["meta"]["id"] = self.resource_id
|
|
552
|
+
if self.row_count is not None:
|
|
553
|
+
d["meta"]["count"] = self.row_count
|
|
554
|
+
return d
|
|
555
|
+
|
|
556
|
+
@staticmethod
|
|
557
|
+
def from_dict(d:dict) -> "CkanDataStoreInfo":
|
|
558
|
+
return CkanDataStoreInfo(d)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
class CkanViewInfo:
|
|
562
|
+
def __init__(self, d:dict):
|
|
563
|
+
self.id:str = d["id"]
|
|
564
|
+
self.title:str = d["title"]
|
|
565
|
+
self.view_type:str = d["view_type"]
|
|
566
|
+
self.resource_id:str = d["resource_id"]
|
|
567
|
+
self.package_id:str = d["package_id"]
|
|
568
|
+
self.details:dict = d
|
|
569
|
+
|
|
570
|
+
def __str__(self):
|
|
571
|
+
return f"View '{self.title}' ({self.id} of resource id {self.resource_id})"
|
|
572
|
+
|
|
573
|
+
def to_dict(self, include_details:bool=True) -> dict:
|
|
574
|
+
d = dict()
|
|
575
|
+
if self.details is not None and include_details:
|
|
576
|
+
d.update(self.details)
|
|
577
|
+
d.update({"id": self.id, "title": self.title, "view_type": self.view_type, "resource_id": self.resource_id, "package_id": self.package_id})
|
|
578
|
+
return d
|
|
579
|
+
|
|
580
|
+
@staticmethod
|
|
581
|
+
def from_dict(d:dict) -> "CkanViewInfo":
|
|
582
|
+
return CkanViewInfo(d)
|
|
583
|
+
|
|
584
|
+
def copy(self) -> "CkanViewInfo":
|
|
585
|
+
return copy.deepcopy(self)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
class CkanLicenseInfo:
|
|
589
|
+
def __init__(self, d:dict):
|
|
590
|
+
self.id:str = d["id"]
|
|
591
|
+
self.title:str = d["title"]
|
|
592
|
+
self.state:CkanState = CkanState.from_str(d["status"])
|
|
593
|
+
self.family:str = d["family"]
|
|
594
|
+
self.domain:CkanLicenseDomain = CkanLicenseDomain.from_bool(domain_software=_bool_from_string(d["domain_software"]),
|
|
595
|
+
domain_data=_bool_from_string(d["domain_data"]), domain_content=_bool_from_string(d["domain_content"]))
|
|
596
|
+
self.is_generic:bool = _bool_from_string(d["is_generic"])
|
|
597
|
+
self.url:str = d["url"]
|
|
598
|
+
self.details:dict = d
|
|
599
|
+
|
|
600
|
+
def __str__(self):
|
|
601
|
+
return f"License '{self.title}' ({self.id}) [{str(self.domain)}]"
|
|
602
|
+
|
|
603
|
+
def to_dict(self, include_details:bool=True) -> dict:
|
|
604
|
+
d = dict()
|
|
605
|
+
if self.details is not None and include_details:
|
|
606
|
+
d.update(self.details)
|
|
607
|
+
d.update({"id": self.id, "title": self.title, "state": str(self.state), "family": self.family})
|
|
608
|
+
d.update(self.domain.to_dict())
|
|
609
|
+
return d
|
|
610
|
+
|
|
611
|
+
@staticmethod
|
|
612
|
+
def from_dict(d:dict) -> "CkanLicenseInfo":
|
|
613
|
+
return CkanLicenseInfo(d)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
class CkanTagInfo:
|
|
617
|
+
def __init__(self, d:dict):
|
|
618
|
+
self.id:str = d["id"]
|
|
619
|
+
self.name:str = d["name"]
|
|
620
|
+
self.display_name:str = d["display_name"]
|
|
621
|
+
self.state: Union[CkanState,None] = None
|
|
622
|
+
if "state" in d.keys():
|
|
623
|
+
self.state = CkanState.from_str(d["state"])
|
|
624
|
+
self.vocabulary_id:Union[str,None] = d["vocabulary_id"]
|
|
625
|
+
self.details:dict = d
|
|
626
|
+
|
|
627
|
+
def __str__(self):
|
|
628
|
+
if self.vocabulary_id is None:
|
|
629
|
+
return f"Tag '{self.name}' ({self.id})"
|
|
630
|
+
else:
|
|
631
|
+
return f"Tag '{self.name}' ({self.id}) [vocabulary {self.vocabulary_id}]"
|
|
632
|
+
|
|
633
|
+
def to_dict(self, include_details:bool=True) -> dict:
|
|
634
|
+
d = dict()
|
|
635
|
+
if self.details is not None and include_details:
|
|
636
|
+
d.update(self.details)
|
|
637
|
+
d.update({"id": self.id, "name": self.name, "display_name": self.display_name,
|
|
638
|
+
"vocabulary_id": self.vocabulary_id})
|
|
639
|
+
if self.state is not None:
|
|
640
|
+
d["state"] = self.state
|
|
641
|
+
return d
|
|
642
|
+
|
|
643
|
+
@staticmethod
|
|
644
|
+
def from_dict(d:dict) -> "CkanTagInfo":
|
|
645
|
+
return CkanTagInfo(d)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
class CkanResourceInfo(CkanConfigurableObjectABC):
|
|
649
|
+
mandatory_attributes = {"name"}
|
|
650
|
+
configurable_attributes = {"name", "state", "format", "description"}
|
|
651
|
+
extra_attributes = {"download_url"}
|
|
652
|
+
|
|
653
|
+
def __init__(self, d:dict=None, name:str=None, format:str=None, description:str=None, state:CkanState=None):
|
|
654
|
+
self.id:Union[str,None] = None
|
|
655
|
+
self.name:Union[str,None] = name
|
|
656
|
+
self.package_id:Union[str,None] = None
|
|
657
|
+
self.state:Union[CkanState,None] = state
|
|
658
|
+
self.datastore_active:Union[bool,None] = None
|
|
659
|
+
self.download_url:Union[str,None] = None
|
|
660
|
+
self.format:Union[str,None] = format
|
|
661
|
+
self.description:Union[str,None] = description
|
|
662
|
+
self.datastore_info:Union[CkanDataStoreInfo,None] = None
|
|
663
|
+
self.datastore_info_error:Union[dict,None] = None
|
|
664
|
+
self.views:Union[OrderedDict[str,CkanViewInfo],None] = None # dict id -> view info (list of known views - full list not guaranteed)
|
|
665
|
+
self.view_is_full_list:bool = False
|
|
666
|
+
self.created: Union[datetime.datetime,None] = None
|
|
667
|
+
self.last_modified: Union[datetime.datetime,None] = None
|
|
668
|
+
self.metadata_modified: Union[datetime.datetime,None] = None
|
|
669
|
+
self.download_size_mb:Union[None,float] = None # obtained through a HEAD request
|
|
670
|
+
if d is not None:
|
|
671
|
+
self.id = d["id"]
|
|
672
|
+
self.name = d["name"]
|
|
673
|
+
self.package_id = d["package_id"]
|
|
674
|
+
if "state" in d.keys():
|
|
675
|
+
self.state = CkanState.from_str(d["state"])
|
|
676
|
+
self.datastore_active = d["datastore_active"]
|
|
677
|
+
self.download_url = d["url"]
|
|
678
|
+
self.format = d["format"]
|
|
679
|
+
self.description = d["description"]
|
|
680
|
+
if "datastore_info" in d.keys():
|
|
681
|
+
self.datastore_info = CkanDataStoreInfo.from_dict(d["datastore_info"])
|
|
682
|
+
if "datastore_info_error" in d.keys():
|
|
683
|
+
self.datastore_info_error = d["datastore_info_error"]
|
|
684
|
+
if "views" in d.keys():
|
|
685
|
+
self.views = OrderedDict()
|
|
686
|
+
for view_dict in d["views"]:
|
|
687
|
+
self.views[view_dict["id"]] = CkanViewInfo.from_dict(view_dict)
|
|
688
|
+
self.created = datetime.datetime.fromisoformat(d["created"]) if "created" in d.keys() else None
|
|
689
|
+
self.last_modified = datetime.datetime.fromisoformat(d["last_modified"]) if "last_modified" in d.keys() and d["last_modified"] is not None else None
|
|
690
|
+
self.metadata_modified = datetime.datetime.fromisoformat(d["metadata_modified"]) if "metadata_modified" in d.keys() else None
|
|
691
|
+
self.details:dict = d
|
|
692
|
+
self.index_in_package:Union[int,None] = -1
|
|
693
|
+
self.newly_created:bool = False
|
|
694
|
+
self.newly_updated:bool = False
|
|
695
|
+
|
|
696
|
+
def __str__(self):
|
|
697
|
+
if self.datastore_info is not None:
|
|
698
|
+
datastore_str = f"DataStore info"
|
|
699
|
+
elif self.datastore_active:
|
|
700
|
+
datastore_str = f"DataStore active"
|
|
701
|
+
else:
|
|
702
|
+
datastore_str = f"no DataStore"
|
|
703
|
+
return f"Resource '{self.name}' ({self.id}) [{self.state}, {datastore_str}]"
|
|
704
|
+
|
|
705
|
+
@staticmethod
|
|
706
|
+
def get_resource_type() -> str:
|
|
707
|
+
return "Resource"
|
|
708
|
+
|
|
709
|
+
def copy(self) -> "CkanResourceInfo":
|
|
710
|
+
return copy.deepcopy(self)
|
|
711
|
+
|
|
712
|
+
def datastore_queried(self) -> bool:
|
|
713
|
+
return self.datastore_info is not None or self.datastore_info_error is not None
|
|
714
|
+
|
|
715
|
+
def update_view(self, view_info: Union[CkanViewInfo, List[CkanViewInfo]], view_list:bool=False) -> None:
|
|
716
|
+
if isinstance(view_info, CkanViewInfo):
|
|
717
|
+
view_info = [view_info]
|
|
718
|
+
if self.views is None:
|
|
719
|
+
self.views = OrderedDict()
|
|
720
|
+
for view_info_update in view_info:
|
|
721
|
+
self.views[view_info_update.id] = view_info_update
|
|
722
|
+
self.view_is_full_list = self.view_is_full_list or view_list # bool indicating if the list comes from full view list API
|
|
723
|
+
|
|
724
|
+
def update(self, refresh) -> None:
|
|
725
|
+
refresh: CkanResourceInfo
|
|
726
|
+
self.id = refresh.id
|
|
727
|
+
self.name = refresh.name
|
|
728
|
+
self.state = refresh.state
|
|
729
|
+
self.package_id = refresh.package_id
|
|
730
|
+
self.datastore_active = refresh.datastore_active
|
|
731
|
+
self.details = refresh.details
|
|
732
|
+
|
|
733
|
+
def to_dict(self, include_details:bool=True) -> dict:
|
|
734
|
+
d = dict()
|
|
735
|
+
if self.details is not None and include_details:
|
|
736
|
+
d.update(self.details)
|
|
737
|
+
d.update({"id": self.id, "name": self.name, "package_id": self.package_id,
|
|
738
|
+
"datastore_active": self.datastore_active, "url": self.download_url,
|
|
739
|
+
"format": self.format, "description": self.description,
|
|
740
|
+
})
|
|
741
|
+
if self.state is not None:
|
|
742
|
+
d["state"] = str(self.state)
|
|
743
|
+
if self.datastore_info is not None:
|
|
744
|
+
d["datastore_info"] = self.datastore_info.to_dict(include_details=include_details)
|
|
745
|
+
if self.datastore_info_error is not None:
|
|
746
|
+
d["datastore_info_error"] = self.datastore_info_error
|
|
747
|
+
if self.views is not None:
|
|
748
|
+
d["views"] = [view.to_dict(include_details=include_details) for view in self.views.values()]
|
|
749
|
+
if self.created is not None:
|
|
750
|
+
d["created"] = self.created.isoformat()
|
|
751
|
+
if self.last_modified is not None:
|
|
752
|
+
d["last_modified"] = self.last_modified.isoformat()
|
|
753
|
+
if self.metadata_modified is not None:
|
|
754
|
+
d["metadata_modified"] = self.metadata_modified.isoformat()
|
|
755
|
+
return d
|
|
756
|
+
|
|
757
|
+
@staticmethod
|
|
758
|
+
def from_dict(d:dict) -> "CkanResourceInfo":
|
|
759
|
+
return CkanResourceInfo(d)
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
class CkanCollaboration:
|
|
763
|
+
def __init__(self, capacity:CkanCapacity=None, modified:datetime.datetime=None, group_id:str=None, d:dict=None):
|
|
764
|
+
self.capacity:CkanCapacity = capacity
|
|
765
|
+
self.group_id:Union[str,None] = group_id
|
|
766
|
+
self.modified: Union[datetime.datetime,None] = modified
|
|
767
|
+
if d is not None:
|
|
768
|
+
self.capacity = CkanCapacity.from_str(d["capacity"])
|
|
769
|
+
self.modified:datetime.datetime = datetime.datetime.fromisoformat(d["modified"])
|
|
770
|
+
|
|
771
|
+
def __str__(self):
|
|
772
|
+
return str(self.capacity)
|
|
773
|
+
|
|
774
|
+
def copy(self) -> "CkanCollaboration":
|
|
775
|
+
return copy.deepcopy(self)
|
|
776
|
+
|
|
777
|
+
def to_dict(self, user_info: CkanUserInfo, group_table: Dict[str,CkanGroupInfo], date_format:str) -> dict:
|
|
778
|
+
d = OrderedDict([
|
|
779
|
+
("full_name", user_info.fullname),
|
|
780
|
+
("capacity", str(self.capacity)),
|
|
781
|
+
])
|
|
782
|
+
if user_info.organizations is not None:
|
|
783
|
+
d["organizations"] = sorted(user_info.organizations)
|
|
784
|
+
if self.modified is not None:
|
|
785
|
+
if date_format is None:
|
|
786
|
+
d["date_modified"] = self.modified.isoformat()
|
|
787
|
+
else:
|
|
788
|
+
d["date_modified"] = self.modified.strftime(date_format)
|
|
789
|
+
if self.group_id is not None:
|
|
790
|
+
d["from_group"] = group_table[self.group_id].name
|
|
791
|
+
return d
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
class CkanPackageInfo(CkanConfigurableObjectABC):
|
|
795
|
+
mandatory_attributes = {"name"}
|
|
796
|
+
configurable_attributes = {"name", "state", "title", "description", "private", "version",
|
|
797
|
+
"author", "author_email", "maintainer", "maintainer_email"}
|
|
798
|
+
extra_attributes = {"tags", "custom_fields"}
|
|
799
|
+
|
|
800
|
+
def __init__(self, d:dict=None, *, package_name:str=None, package_id:str=None,
|
|
801
|
+
title:str=None, description:str=None, private:bool=None, state:CkanState=None, version:str=None,
|
|
802
|
+
url:str=None, tags:List[str]=None):
|
|
803
|
+
self.id:Union[str, None] = package_id
|
|
804
|
+
self.name:Union[str, None] = package_name
|
|
805
|
+
self.title:Union[str, None] = title
|
|
806
|
+
self.description:Union[str, None] = description
|
|
807
|
+
self.private:Union[bool, None] = private
|
|
808
|
+
self.state:Union[CkanState, None] = state
|
|
809
|
+
self.version:Union[str, None] = version
|
|
810
|
+
self.custom_fields:Dict[str,str] = {} # key, value pairs
|
|
811
|
+
self.details:dict = {}
|
|
812
|
+
self.package_resources:OrderedDict[str,CkanResourceInfo] = OrderedDict() # resource id -> info
|
|
813
|
+
self.resources_id_index:Dict[str,str] = {} # resource name -> id
|
|
814
|
+
self.resources_id_index_counts:Dict[str,int] = {} # resource name -> counter
|
|
815
|
+
self.organization_info: Union[CkanOrganizationInfo, None] = None
|
|
816
|
+
self.groups:List[CkanGroupInfo] = []
|
|
817
|
+
self.license_id:Union[str, None] = None
|
|
818
|
+
self.author:Union[str, None] = None
|
|
819
|
+
self.author_email:Union[str, None] = None
|
|
820
|
+
self.maintainer:Union[str, None] = None
|
|
821
|
+
self.maintainer_email:Union[str, None] = None
|
|
822
|
+
self.url:Union[str, None] = url
|
|
823
|
+
self.tags:Union[List[str],None] = tags
|
|
824
|
+
self.tags_info:Union[Dict[str, CkanTagInfo],None] = None # dict tag name -> tag info
|
|
825
|
+
self.metadata_created: Union[datetime.datetime,None] = None
|
|
826
|
+
self.metadata_modified: Union[datetime.datetime,None] = None
|
|
827
|
+
self.requested_datastore_info:bool = False
|
|
828
|
+
self.newly_created:bool = False
|
|
829
|
+
self.collaborators:Union[None,Dict[str,CkanCollaboration]] = None # given by API package_collaborator_list
|
|
830
|
+
self.user_access:Union[None,Dict[str,CkanCollaboration]] = None # given by function map_user_rights
|
|
831
|
+
|
|
832
|
+
if d is not None:
|
|
833
|
+
self.id = d["id"]
|
|
834
|
+
self.name = d["name"]
|
|
835
|
+
self.title = d["title"]
|
|
836
|
+
self.description = d["notes"]
|
|
837
|
+
self.private = d["private"]
|
|
838
|
+
if "state" in d.keys():
|
|
839
|
+
self.state = CkanState.from_str(d["state"])
|
|
840
|
+
self.version = d["version"]
|
|
841
|
+
self.custom_fields = {field["key"]: field["value"] for field in d["extras"]}
|
|
842
|
+
self.details = d
|
|
843
|
+
self.package_resources = OrderedDict()
|
|
844
|
+
for resource_info_dict in d["resources"]:
|
|
845
|
+
self.package_resources[resource_info_dict["id"]] = CkanResourceInfo(resource_info_dict)
|
|
846
|
+
self.resources_id_index = {resource_info.name: resource_info.id for resource_info in self.package_resources.values()} # resource name -> id
|
|
847
|
+
self.resources_id_index_counts = {} # resource name -> counter
|
|
848
|
+
for resource_info in self.package_resources.values():
|
|
849
|
+
if resource_info.name not in self.resources_id_index_counts.keys():
|
|
850
|
+
self.resources_id_index_counts[resource_info.name] = 1
|
|
851
|
+
else:
|
|
852
|
+
self.resources_id_index_counts[resource_info.name] += 1
|
|
853
|
+
self.organization_info = None
|
|
854
|
+
if "organization" in d.keys():
|
|
855
|
+
self.organization_info = CkanOrganizationInfo(d["organization"])
|
|
856
|
+
assert_or_raise(self.organization_info.id == d["owner_org"], IntegrityError("Unexpected: organization != owner_org"))
|
|
857
|
+
else:
|
|
858
|
+
assert_or_raise("owner_org" not in d.keys() or d["owner_org"] == "", IntegrityError("Unexpected: organization is not present but owner_org was found"))
|
|
859
|
+
self.groups = [CkanGroupInfo(info) for info in d["groups"]]
|
|
860
|
+
self.license_id = d["license_id"]
|
|
861
|
+
self.author = d["author"]
|
|
862
|
+
self.author_email = d["author_email"]
|
|
863
|
+
self.maintainer = d["maintainer"]
|
|
864
|
+
self.maintainer_email = d["maintainer_email"]
|
|
865
|
+
self.metadata_created = datetime.datetime.fromisoformat(d["metadata_created"]) if "metadata_created" in d.keys() else None
|
|
866
|
+
self.metadata_modified = datetime.datetime.fromisoformat(d["metadata_modified"]) if "metadata_modified" in d.keys() else None
|
|
867
|
+
self.url = d["url"]
|
|
868
|
+
self.tags_info = {tag_dict["name"]: CkanTagInfo(tag_dict) for tag_dict in d["tags"]} if d["tags"] is not None else None
|
|
869
|
+
self.tags = list(self.tags_info.keys()) if self.tags_info is not None else None
|
|
870
|
+
|
|
871
|
+
def __str__(self):
|
|
872
|
+
return f"Package '{self.name}' ({self.id}) [{self.state}]"
|
|
873
|
+
|
|
874
|
+
@staticmethod
|
|
875
|
+
def get_resource_type() -> str:
|
|
876
|
+
return "Package"
|
|
877
|
+
|
|
878
|
+
def copy(self) -> "CkanPackageInfo":
|
|
879
|
+
return copy.deepcopy(self)
|
|
880
|
+
|
|
881
|
+
def update(self, refresh: "CkanPackageInfo"):
|
|
882
|
+
refresh: CkanPackageInfo
|
|
883
|
+
self.id = refresh.id
|
|
884
|
+
self.name = refresh.name
|
|
885
|
+
self.state = refresh.state
|
|
886
|
+
self.details = refresh.details
|
|
887
|
+
self.package_resources = refresh.package_resources
|
|
888
|
+
self.resources_id_index = refresh.resources_id_index
|
|
889
|
+
self.resources_id_index_counts = refresh.resources_id_index_counts
|
|
890
|
+
|
|
891
|
+
def get_resource_index(self, resource_id:str) -> int:
|
|
892
|
+
i_found = None
|
|
893
|
+
for i, res_info in enumerate(self.package_resources.values()):
|
|
894
|
+
if res_info.id == resource_id:
|
|
895
|
+
i_found = i
|
|
896
|
+
break
|
|
897
|
+
return i_found
|
|
898
|
+
|
|
899
|
+
def update_resource(self, resource_info: CkanResourceInfo) -> int:
|
|
900
|
+
if resource_info.id is None:
|
|
901
|
+
raise MissingIdError("Resource", resource_info.name)
|
|
902
|
+
resource_id = resource_info.id
|
|
903
|
+
i_update = self.get_resource_index(resource_id)
|
|
904
|
+
if i_update is not None:
|
|
905
|
+
resource_info.index_in_package = i_update
|
|
906
|
+
self.package_resources[resource_id] = resource_info
|
|
907
|
+
else:
|
|
908
|
+
i_update = len(self.package_resources) - 1
|
|
909
|
+
resource_info.index_in_package = i_update
|
|
910
|
+
self.package_resources[resource_id] = resource_info
|
|
911
|
+
self.resources_id_index_counts[resource_info.name] = 1
|
|
912
|
+
self.resources_id_index[resource_info.name] = resource_id
|
|
913
|
+
return i_update
|
|
914
|
+
|
|
915
|
+
def to_dict(self, include_details:bool=True) -> dict:
|
|
916
|
+
d = dict()
|
|
917
|
+
if self.details is not None and include_details:
|
|
918
|
+
d.update(self.details)
|
|
919
|
+
d.update({"id": self.id, "name": self.name, "title": self.title,
|
|
920
|
+
"notes": self.description, "private": self.private, "version": self.version,
|
|
921
|
+
"tags": self.tags, "url": self.url,
|
|
922
|
+
"author": self.author, "author_email": self.author_email,
|
|
923
|
+
"maintainer": self.maintainer, "maintainer_email": self.maintainer_email,
|
|
924
|
+
"groups": [group.to_dict(include_details=include_details) for group in self.groups],
|
|
925
|
+
"license_id": self.license_id,
|
|
926
|
+
"resources": [resource.to_dict(include_details=include_details) for resource in self.package_resources.values()],
|
|
927
|
+
})
|
|
928
|
+
if self.metadata_created is not None:
|
|
929
|
+
d["metadata_created"] = self.metadata_created.isoformat()
|
|
930
|
+
if self.metadata_modified is not None:
|
|
931
|
+
d["metadata_modified"] = self.metadata_modified.isoformat()
|
|
932
|
+
if self.state is not None:
|
|
933
|
+
d["state"] = str(self.state)
|
|
934
|
+
if self.custom_fields is not None:
|
|
935
|
+
d["extras"] = [{"key": key, "value": value} for key, value in self.custom_fields.items()]
|
|
936
|
+
if self.organization_info is not None:
|
|
937
|
+
d["owner_org"] = self.organization_info.id
|
|
938
|
+
d["organization"] = self.organization_info.to_dict(include_details=include_details)
|
|
939
|
+
return d
|
|
940
|
+
|
|
941
|
+
@staticmethod
|
|
942
|
+
def from_dict(d:dict) -> "CkanPackageInfo":
|
|
943
|
+
return CkanPackageInfo(d)
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
class CkanOrganizationInfo:
|
|
947
|
+
def __init__(self, d:dict):
|
|
948
|
+
self.id = d["id"]
|
|
949
|
+
self.name = d["name"]
|
|
950
|
+
self.state = CkanState.from_str(d["state"])
|
|
951
|
+
self.title = d["title"]
|
|
952
|
+
self.user_members: Union[None,Dict[str,CkanCapacity]] = None
|
|
953
|
+
self.details = d
|
|
954
|
+
if "users" in d:
|
|
955
|
+
self.user_members = {user_dict["id"]: CkanCapacity.from_str(user_dict["capacity"]) for user_dict in d["users"]}
|
|
956
|
+
|
|
957
|
+
def __str__(self):
|
|
958
|
+
return f"Organization '{self.name}' ({self.id}) [{self.state}]"
|
|
959
|
+
|
|
960
|
+
def copy(self) -> "CkanOrganizationInfo":
|
|
961
|
+
return copy.deepcopy(self)
|
|
962
|
+
|
|
963
|
+
def get_owner_org(self):
|
|
964
|
+
"""
|
|
965
|
+
Returns the value used for the owner_org argument
|
|
966
|
+
|
|
967
|
+
:return:
|
|
968
|
+
"""
|
|
969
|
+
return self.name
|
|
970
|
+
|
|
971
|
+
def to_dict(self, include_details:bool=True) -> dict:
|
|
972
|
+
d = dict()
|
|
973
|
+
if self.details is not None and include_details:
|
|
974
|
+
d.update(self.details)
|
|
975
|
+
d.update({"id": self.id, "name": self.name, "title": self.title, "state": str(self.state)})
|
|
976
|
+
return d
|
|
977
|
+
|
|
978
|
+
@staticmethod
|
|
979
|
+
def from_dict(d:dict) -> "CkanOrganizationInfo":
|
|
980
|
+
return CkanOrganizationInfo(d)
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
class PackageShortDescriptor:
|
|
984
|
+
"""
|
|
985
|
+
Class to define more stable names to describe a package
|
|
986
|
+
"""
|
|
987
|
+
def __init__(self, package_name:str, owner_org:str, resource_names: Dict[str,str]):
|
|
988
|
+
self.name: str = package_name
|
|
989
|
+
self.owner_org: str = owner_org
|
|
990
|
+
self.resource_names: Dict[str,str] = resource_names
|
|
991
|
+
|
|
992
|
+
|