ckanapi-harvesters 0.0.0__py3-none-any.whl → 0.0.2__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.
Files changed (110) hide show
  1. ckanapi_harvesters/__init__.py +32 -10
  2. ckanapi_harvesters/auxiliary/__init__.py +26 -0
  3. ckanapi_harvesters/auxiliary/ckan_action.py +93 -0
  4. ckanapi_harvesters/auxiliary/ckan_api_key.py +213 -0
  5. ckanapi_harvesters/auxiliary/ckan_auxiliary.py +293 -0
  6. ckanapi_harvesters/auxiliary/ckan_configuration.py +50 -0
  7. ckanapi_harvesters/auxiliary/ckan_defs.py +10 -0
  8. ckanapi_harvesters/auxiliary/ckan_errors.py +129 -0
  9. ckanapi_harvesters/auxiliary/ckan_map.py +509 -0
  10. ckanapi_harvesters/auxiliary/ckan_model.py +992 -0
  11. ckanapi_harvesters/auxiliary/ckan_vocabulary_deprecated.py +104 -0
  12. ckanapi_harvesters/auxiliary/deprecated.py +82 -0
  13. ckanapi_harvesters/auxiliary/error_level_message.py +51 -0
  14. ckanapi_harvesters/auxiliary/external_code_import.py +98 -0
  15. ckanapi_harvesters/auxiliary/list_records.py +60 -0
  16. ckanapi_harvesters/auxiliary/login.py +163 -0
  17. ckanapi_harvesters/auxiliary/path.py +208 -0
  18. ckanapi_harvesters/auxiliary/proxy_config.py +298 -0
  19. ckanapi_harvesters/auxiliary/urls.py +40 -0
  20. ckanapi_harvesters/builder/__init__.py +40 -0
  21. ckanapi_harvesters/builder/builder_aux.py +20 -0
  22. ckanapi_harvesters/builder/builder_ckan.py +238 -0
  23. ckanapi_harvesters/builder/builder_errors.py +36 -0
  24. ckanapi_harvesters/builder/builder_field.py +122 -0
  25. ckanapi_harvesters/builder/builder_package.py +9 -0
  26. ckanapi_harvesters/builder/builder_package_1_basic.py +1291 -0
  27. ckanapi_harvesters/builder/builder_package_2_harvesters.py +40 -0
  28. ckanapi_harvesters/builder/builder_package_3_multi_threaded.py +45 -0
  29. ckanapi_harvesters/builder/builder_package_example.xlsx +0 -0
  30. ckanapi_harvesters/builder/builder_resource.py +589 -0
  31. ckanapi_harvesters/builder/builder_resource_datastore.py +561 -0
  32. ckanapi_harvesters/builder/builder_resource_datastore_multi_abc.py +367 -0
  33. ckanapi_harvesters/builder/builder_resource_datastore_multi_folder.py +273 -0
  34. ckanapi_harvesters/builder/builder_resource_datastore_multi_harvester.py +278 -0
  35. ckanapi_harvesters/builder/builder_resource_datastore_unmanaged.py +145 -0
  36. ckanapi_harvesters/builder/builder_resource_datastore_url.py +150 -0
  37. ckanapi_harvesters/builder/builder_resource_init.py +126 -0
  38. ckanapi_harvesters/builder/builder_resource_multi_abc.py +361 -0
  39. ckanapi_harvesters/builder/builder_resource_multi_datastore.py +146 -0
  40. ckanapi_harvesters/builder/builder_resource_multi_file.py +505 -0
  41. ckanapi_harvesters/builder/example/__init__.py +21 -0
  42. ckanapi_harvesters/builder/example/builder_example.py +21 -0
  43. ckanapi_harvesters/builder/example/builder_example_aux_fun.py +24 -0
  44. ckanapi_harvesters/builder/example/builder_example_download.py +44 -0
  45. ckanapi_harvesters/builder/example/builder_example_generate_data.py +73 -0
  46. ckanapi_harvesters/builder/example/builder_example_patch_upload.py +51 -0
  47. ckanapi_harvesters/builder/example/builder_example_policy.py +114 -0
  48. ckanapi_harvesters/builder/example/builder_example_test_sql.py +53 -0
  49. ckanapi_harvesters/builder/example/builder_example_tests.py +87 -0
  50. ckanapi_harvesters/builder/example/builder_example_tests_offline.py +57 -0
  51. ckanapi_harvesters/builder/example/package/ckan-dpg.svg +74 -0
  52. ckanapi_harvesters/builder/example/package/users_local.csv +3 -0
  53. ckanapi_harvesters/builder/mapper_datastore.py +93 -0
  54. ckanapi_harvesters/builder/mapper_datastore_multi.py +262 -0
  55. ckanapi_harvesters/builder/specific/__init__.py +11 -0
  56. ckanapi_harvesters/builder/specific/configuration_builder.py +66 -0
  57. ckanapi_harvesters/builder/specific_builder_abc.py +23 -0
  58. ckanapi_harvesters/ckan_api/__init__.py +20 -0
  59. ckanapi_harvesters/ckan_api/ckan_api.py +11 -0
  60. ckanapi_harvesters/ckan_api/ckan_api_0_base.py +896 -0
  61. ckanapi_harvesters/ckan_api/ckan_api_1_map.py +1028 -0
  62. ckanapi_harvesters/ckan_api/ckan_api_2_readonly.py +934 -0
  63. ckanapi_harvesters/ckan_api/ckan_api_3_policy.py +229 -0
  64. ckanapi_harvesters/ckan_api/ckan_api_4_readwrite.py +579 -0
  65. ckanapi_harvesters/ckan_api/ckan_api_5_manage.py +1225 -0
  66. ckanapi_harvesters/ckan_api/ckan_api_params.py +192 -0
  67. ckanapi_harvesters/ckan_api/deprecated/__init__.py +9 -0
  68. ckanapi_harvesters/ckan_api/deprecated/ckan_api_deprecated.py +267 -0
  69. ckanapi_harvesters/ckan_api/deprecated/ckan_api_deprecated_vocabularies.py +189 -0
  70. ckanapi_harvesters/harvesters/__init__.py +23 -0
  71. ckanapi_harvesters/harvesters/data_cleaner/__init__.py +17 -0
  72. ckanapi_harvesters/harvesters/data_cleaner/data_cleaner_abc.py +240 -0
  73. ckanapi_harvesters/harvesters/data_cleaner/data_cleaner_errors.py +23 -0
  74. ckanapi_harvesters/harvesters/data_cleaner/data_cleaner_upload.py +9 -0
  75. ckanapi_harvesters/harvesters/data_cleaner/data_cleaner_upload_1_basic.py +430 -0
  76. ckanapi_harvesters/harvesters/data_cleaner/data_cleaner_upload_2_geom.py +98 -0
  77. ckanapi_harvesters/harvesters/file_formats/__init__.py +10 -0
  78. ckanapi_harvesters/harvesters/file_formats/csv_format.py +43 -0
  79. ckanapi_harvesters/harvesters/file_formats/file_format_abc.py +39 -0
  80. ckanapi_harvesters/harvesters/file_formats/file_format_init.py +25 -0
  81. ckanapi_harvesters/harvesters/file_formats/shp_format.py +129 -0
  82. ckanapi_harvesters/harvesters/harvester_abc.py +190 -0
  83. ckanapi_harvesters/harvesters/harvester_errors.py +31 -0
  84. ckanapi_harvesters/harvesters/harvester_init.py +30 -0
  85. ckanapi_harvesters/harvesters/harvester_model.py +49 -0
  86. ckanapi_harvesters/harvesters/harvester_params.py +323 -0
  87. ckanapi_harvesters/harvesters/postgre_harvester.py +495 -0
  88. ckanapi_harvesters/harvesters/postgre_params.py +86 -0
  89. ckanapi_harvesters/harvesters/pymongo_data_cleaner.py +173 -0
  90. ckanapi_harvesters/harvesters/pymongo_harvester.py +355 -0
  91. ckanapi_harvesters/harvesters/pymongo_params.py +54 -0
  92. ckanapi_harvesters/policies/__init__.py +20 -0
  93. ckanapi_harvesters/policies/data_format_policy.py +269 -0
  94. ckanapi_harvesters/policies/data_format_policy_abc.py +97 -0
  95. ckanapi_harvesters/policies/data_format_policy_custom_fields.py +156 -0
  96. ckanapi_harvesters/policies/data_format_policy_defs.py +135 -0
  97. ckanapi_harvesters/policies/data_format_policy_errors.py +79 -0
  98. ckanapi_harvesters/policies/data_format_policy_lists.py +234 -0
  99. ckanapi_harvesters/policies/data_format_policy_tag_groups.py +35 -0
  100. ckanapi_harvesters/reports/__init__.py +11 -0
  101. ckanapi_harvesters/reports/admin_report.py +292 -0
  102. {ckanapi_harvesters-0.0.0.dist-info → ckanapi_harvesters-0.0.2.dist-info}/METADATA +74 -38
  103. ckanapi_harvesters-0.0.2.dist-info/RECORD +105 -0
  104. ckanapi_harvesters/divider/__init__.py +0 -27
  105. ckanapi_harvesters/divider/divider.py +0 -53
  106. ckanapi_harvesters/divider/divider_error.py +0 -59
  107. ckanapi_harvesters/main.py +0 -30
  108. ckanapi_harvesters-0.0.0.dist-info/RECORD +0 -9
  109. {ckanapi_harvesters-0.0.0.dist-info → ckanapi_harvesters-0.0.2.dist-info}/WHEEL +0 -0
  110. {ckanapi_harvesters-0.0.0.dist-info → ckanapi_harvesters-0.0.2.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
+