ckanext-csvwmapandtransform 1.0.0__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.
- ckanext/csvwmapandtransform/__init__.py +0 -0
- ckanext/csvwmapandtransform/action.py +405 -0
- ckanext/csvwmapandtransform/assets/script.js +81 -0
- ckanext/csvwmapandtransform/assets/style.css +124 -0
- ckanext/csvwmapandtransform/assets/webassets.yml +13 -0
- ckanext/csvwmapandtransform/auth.py +23 -0
- ckanext/csvwmapandtransform/cli.py +18 -0
- ckanext/csvwmapandtransform/db.py +397 -0
- ckanext/csvwmapandtransform/helpers.py +67 -0
- ckanext/csvwmapandtransform/mapper.py +92 -0
- ckanext/csvwmapandtransform/plugin.py +140 -0
- ckanext/csvwmapandtransform/tasks.py +257 -0
- ckanext/csvwmapandtransform/templates/csvwmapandtransform/create_mapping.html +56 -0
- ckanext/csvwmapandtransform/templates/csvwmapandtransform/transform.html +108 -0
- ckanext/csvwmapandtransform/templates/package/resource_read.html +8 -0
- ckanext/csvwmapandtransform/templates/package/snippets/resource_item.html +23 -0
- ckanext/csvwmapandtransform/tests/__init__.py +0 -0
- ckanext/csvwmapandtransform/views.py +205 -0
- ckanext_csvwmapandtransform-1.0.0-py3.13-nspkg.pth +1 -0
- ckanext_csvwmapandtransform-1.0.0.dist-info/METADATA +121 -0
- ckanext_csvwmapandtransform-1.0.0.dist-info/RECORD +26 -0
- ckanext_csvwmapandtransform-1.0.0.dist-info/WHEEL +5 -0
- ckanext_csvwmapandtransform-1.0.0.dist-info/entry_points.txt +2 -0
- ckanext_csvwmapandtransform-1.0.0.dist-info/licenses/LICENSE +661 -0
- ckanext_csvwmapandtransform-1.0.0.dist-info/namespace_packages.txt +1 -0
- ckanext_csvwmapandtransform-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Abstracts a database. Used for storing logging when it aiembeddings a resource into
|
|
3
|
+
DataStore.
|
|
4
|
+
|
|
5
|
+
Loosely based on ckan-service-provider's db.py
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
Abstracts a database. Used for storing logging when it aiembeddings a resource into
|
|
10
|
+
DataStore.
|
|
11
|
+
|
|
12
|
+
Loosely based on ckan-service-provider's db.py
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import datetime
|
|
16
|
+
import json
|
|
17
|
+
|
|
18
|
+
import six
|
|
19
|
+
import sqlalchemy
|
|
20
|
+
from ckan.plugins import toolkit
|
|
21
|
+
|
|
22
|
+
ENGINE = None
|
|
23
|
+
_METADATA = None
|
|
24
|
+
JOBS_TABLE = None
|
|
25
|
+
METADATA_TABLE = None
|
|
26
|
+
LOGS_TABLE = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def init(db_uri: str = "", echo=False):
|
|
30
|
+
"""Initialise the database.
|
|
31
|
+
|
|
32
|
+
Initialise the sqlalchemy engine, metadata and table objects that we use to
|
|
33
|
+
connect to the database.
|
|
34
|
+
|
|
35
|
+
Create the database and the database tables themselves if they don't
|
|
36
|
+
already exist.
|
|
37
|
+
|
|
38
|
+
:param uri: the sqlalchemy database URI
|
|
39
|
+
:type uri: string
|
|
40
|
+
|
|
41
|
+
:param echo: whether or not to have the sqlalchemy engine log all
|
|
42
|
+
statements to stdout
|
|
43
|
+
:type echo: bool
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
if not db_uri:
|
|
47
|
+
db_uri = toolkit.config.get("ckanext.csvwmapandtransform.db_url")
|
|
48
|
+
global ENGINE, _METADATA, JOBS_TABLE, METADATA_TABLE, LOGS_TABLE
|
|
49
|
+
ENGINE = sqlalchemy.create_engine(db_uri, echo=echo, convert_unicode=True)
|
|
50
|
+
_METADATA = sqlalchemy.MetaData(ENGINE)
|
|
51
|
+
JOBS_TABLE = _init_jobs_table()
|
|
52
|
+
METADATA_TABLE = _init_metadata_table()
|
|
53
|
+
LOGS_TABLE = _init_logs_table()
|
|
54
|
+
_METADATA.create_all(ENGINE)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def drop_all():
|
|
58
|
+
"""Delete all the database tables (if they exist).
|
|
59
|
+
|
|
60
|
+
This is for tests to reset the DB. Note that this will delete *all* tables
|
|
61
|
+
in the database, not just tables created by this module (for example
|
|
62
|
+
apscheduler's tables will also be deleted).
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
if _METADATA:
|
|
66
|
+
_METADATA.drop_all(ENGINE)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def delete_job(job_id):
|
|
70
|
+
"""Delete a job from the jobs table by job_id.
|
|
71
|
+
|
|
72
|
+
:param job_id: the job_id of the job to be deleted
|
|
73
|
+
:type job_id: unicode
|
|
74
|
+
"""
|
|
75
|
+
if job_id:
|
|
76
|
+
job_id = six.text_type(job_id)
|
|
77
|
+
|
|
78
|
+
msg = ""
|
|
79
|
+
with ENGINE.connect() as conn:
|
|
80
|
+
trans = conn.begin()
|
|
81
|
+
try:
|
|
82
|
+
result = conn.execute(
|
|
83
|
+
JOBS_TABLE.delete().where(JOBS_TABLE.c.job_id == job_id)
|
|
84
|
+
)
|
|
85
|
+
if result.rowcount == 0:
|
|
86
|
+
msg = f"No job found with id: {job_id}"
|
|
87
|
+
else:
|
|
88
|
+
msg = f"Job with id: {job_id} has been deleted successfully."
|
|
89
|
+
trans.commit()
|
|
90
|
+
except Exception as e:
|
|
91
|
+
trans.rollback()
|
|
92
|
+
msg = f"An error occurred: {e}"
|
|
93
|
+
return msg
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_job(job_id):
|
|
97
|
+
"""Return the job with the given job_id as a dict."""
|
|
98
|
+
if job_id:
|
|
99
|
+
job_id = six.text_type(job_id)
|
|
100
|
+
|
|
101
|
+
with ENGINE.connect() as conn:
|
|
102
|
+
result = conn.execute(
|
|
103
|
+
JOBS_TABLE.select().where(JOBS_TABLE.c.job_id == job_id)
|
|
104
|
+
).first()
|
|
105
|
+
|
|
106
|
+
if not result:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
result_dict = {
|
|
110
|
+
field: (
|
|
111
|
+
value.isoformat()
|
|
112
|
+
if isinstance(value := getattr(result, field), datetime.datetime)
|
|
113
|
+
else value
|
|
114
|
+
)
|
|
115
|
+
for field in result.keys()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
result_dict["metadata"] = _get_metadata(job_id)
|
|
119
|
+
result_dict["logs"] = _get_logs(job_id)
|
|
120
|
+
|
|
121
|
+
return result_dict
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def add_pending_job(job_id, job_type, data=None, metadata=None, result_url=None):
|
|
125
|
+
"""Add a new job with status "pending" to the jobs table."""
|
|
126
|
+
if not data:
|
|
127
|
+
data = {}
|
|
128
|
+
data = six.text_type(json.dumps(data))
|
|
129
|
+
|
|
130
|
+
if job_id:
|
|
131
|
+
job_id = six.text_type(job_id)
|
|
132
|
+
if job_type:
|
|
133
|
+
job_type = six.text_type(job_type)
|
|
134
|
+
if result_url:
|
|
135
|
+
result_url = six.text_type(result_url)
|
|
136
|
+
|
|
137
|
+
if not metadata:
|
|
138
|
+
metadata = {}
|
|
139
|
+
|
|
140
|
+
with ENGINE.connect() as conn:
|
|
141
|
+
trans = conn.begin()
|
|
142
|
+
try:
|
|
143
|
+
conn.execute(
|
|
144
|
+
JOBS_TABLE.insert().values(
|
|
145
|
+
job_id=job_id,
|
|
146
|
+
job_type=job_type,
|
|
147
|
+
status="pending",
|
|
148
|
+
requested_timestamp=datetime.datetime.utcnow(),
|
|
149
|
+
sent_data=data,
|
|
150
|
+
result_url=result_url,
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
inserts = [
|
|
155
|
+
{
|
|
156
|
+
"job_id": job_id,
|
|
157
|
+
"key": six.text_type(key),
|
|
158
|
+
"value": six.text_type(
|
|
159
|
+
json.dumps(value)
|
|
160
|
+
if not isinstance(value, six.string_types)
|
|
161
|
+
else value
|
|
162
|
+
),
|
|
163
|
+
"type": (
|
|
164
|
+
"json" if not isinstance(value, six.string_types) else "string"
|
|
165
|
+
),
|
|
166
|
+
}
|
|
167
|
+
for key, value in metadata.items()
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
if inserts:
|
|
171
|
+
conn.execute(METADATA_TABLE.insert(), inserts)
|
|
172
|
+
trans.commit()
|
|
173
|
+
except Exception:
|
|
174
|
+
trans.rollback()
|
|
175
|
+
raise
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class InvalidErrorObjectError(Exception):
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _validate_error(error):
|
|
183
|
+
"""Validate and return the given error object.
|
|
184
|
+
|
|
185
|
+
Based on the given error object, return either None or a dict with a
|
|
186
|
+
"message" key whose value is a string (the dict may also have any other
|
|
187
|
+
keys that it wants).
|
|
188
|
+
|
|
189
|
+
The given "error" object can be:
|
|
190
|
+
|
|
191
|
+
- None, in which case None is returned
|
|
192
|
+
|
|
193
|
+
- A string, in which case a dict like this will be returned:
|
|
194
|
+
{"message": error_string}
|
|
195
|
+
|
|
196
|
+
- A dict with a "message" key whose value is a string, in which case the
|
|
197
|
+
dict will be returned unchanged
|
|
198
|
+
|
|
199
|
+
:param error: the error object to validate
|
|
200
|
+
|
|
201
|
+
:raises InvalidErrorObjectError: If the error object doesn't match any of
|
|
202
|
+
the allowed types
|
|
203
|
+
|
|
204
|
+
"""
|
|
205
|
+
if error is None:
|
|
206
|
+
return None
|
|
207
|
+
elif isinstance(error, six.string_types):
|
|
208
|
+
return {"message": error}
|
|
209
|
+
else:
|
|
210
|
+
try:
|
|
211
|
+
message = error["message"]
|
|
212
|
+
if isinstance(message, six.string_types):
|
|
213
|
+
return error
|
|
214
|
+
else:
|
|
215
|
+
raise InvalidErrorObjectError("error['message'] must be a string")
|
|
216
|
+
except (TypeError, KeyError):
|
|
217
|
+
raise InvalidErrorObjectError(
|
|
218
|
+
"error must be either a string or a dict with a message key"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _update_job(job_id, job_dict):
|
|
223
|
+
"""Update the database row for the given job_id with the given job_dict."""
|
|
224
|
+
if job_id:
|
|
225
|
+
job_id = six.text_type(job_id)
|
|
226
|
+
|
|
227
|
+
if "error" in job_dict:
|
|
228
|
+
job_dict["error"] = json.dumps(_validate_error(job_dict["error"]))
|
|
229
|
+
job_dict["error"] = six.text_type(job_dict["error"])
|
|
230
|
+
|
|
231
|
+
if "data" in job_dict:
|
|
232
|
+
job_dict["data"] = six.text_type(job_dict["data"])
|
|
233
|
+
|
|
234
|
+
with ENGINE.connect() as conn:
|
|
235
|
+
conn.execute(
|
|
236
|
+
JOBS_TABLE.update().where(JOBS_TABLE.c.job_id == job_id).values(**job_dict)
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def mark_job_as_completed(job_id, data=None):
|
|
241
|
+
"""Mark a job as completed successfully.
|
|
242
|
+
|
|
243
|
+
:param job_id: the job_id of the job to be updated
|
|
244
|
+
:type job_id: unicode
|
|
245
|
+
|
|
246
|
+
:param data: the output data returned by the job
|
|
247
|
+
:type data: any JSON-serializable type (including None)
|
|
248
|
+
|
|
249
|
+
"""
|
|
250
|
+
update_dict = {
|
|
251
|
+
"status": "complete",
|
|
252
|
+
"data": json.dumps(data),
|
|
253
|
+
"finished_timestamp": datetime.datetime.utcnow(),
|
|
254
|
+
}
|
|
255
|
+
_update_job(job_id, update_dict)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def mark_job_as_missed(job_id):
|
|
259
|
+
"""Mark a job as missed because it was in the queue for too long.
|
|
260
|
+
|
|
261
|
+
:param job_id: the job_id of the job to be updated
|
|
262
|
+
:type job_id: unicode
|
|
263
|
+
|
|
264
|
+
"""
|
|
265
|
+
update_dict = {
|
|
266
|
+
"status": "error",
|
|
267
|
+
"error": "Job delayed too long, service full",
|
|
268
|
+
"finished_timestamp": datetime.datetime.utcnow(),
|
|
269
|
+
}
|
|
270
|
+
_update_job(job_id, update_dict)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def mark_job_as_errored(job_id, error_object):
|
|
274
|
+
"""Mark a job as failed with an error.
|
|
275
|
+
|
|
276
|
+
:param job_id: the job_id of the job to be updated
|
|
277
|
+
:type job_id: unicode
|
|
278
|
+
|
|
279
|
+
:param error_object: the error returned by the job
|
|
280
|
+
:type error_object: either a string or a dict with a "message" key whose
|
|
281
|
+
value is a string
|
|
282
|
+
|
|
283
|
+
"""
|
|
284
|
+
update_dict = {
|
|
285
|
+
"status": "error",
|
|
286
|
+
"error": error_object,
|
|
287
|
+
"finished_timestamp": datetime.datetime.utcnow(),
|
|
288
|
+
}
|
|
289
|
+
_update_job(job_id, update_dict)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def mark_job_as_failed_to_post_result(job_id):
|
|
293
|
+
"""Mark a job as 'failed to post result'.
|
|
294
|
+
|
|
295
|
+
This happens when a job completes (either successfully or with an error)
|
|
296
|
+
then trying to post the job result back to the job's callback URL fails.
|
|
297
|
+
|
|
298
|
+
FIXME: This overwrites any error from the job itself!
|
|
299
|
+
|
|
300
|
+
:param job_id: the job_id of the job to be updated
|
|
301
|
+
:type job_id: unicode
|
|
302
|
+
|
|
303
|
+
"""
|
|
304
|
+
update_dict = {
|
|
305
|
+
"error": "Process completed but unable to post to result_url",
|
|
306
|
+
}
|
|
307
|
+
_update_job(job_id, update_dict)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _init_jobs_table():
|
|
311
|
+
"""Initialise the "jobs" table in the db."""
|
|
312
|
+
_jobs_table = sqlalchemy.Table(
|
|
313
|
+
"jobs",
|
|
314
|
+
_METADATA,
|
|
315
|
+
sqlalchemy.Column("job_id", sqlalchemy.UnicodeText, primary_key=True),
|
|
316
|
+
sqlalchemy.Column("job_type", sqlalchemy.UnicodeText),
|
|
317
|
+
sqlalchemy.Column("status", sqlalchemy.UnicodeText, index=True),
|
|
318
|
+
sqlalchemy.Column("data", sqlalchemy.UnicodeText),
|
|
319
|
+
sqlalchemy.Column("error", sqlalchemy.UnicodeText),
|
|
320
|
+
sqlalchemy.Column("requested_timestamp", sqlalchemy.DateTime),
|
|
321
|
+
sqlalchemy.Column("finished_timestamp", sqlalchemy.DateTime),
|
|
322
|
+
sqlalchemy.Column("sent_data", sqlalchemy.UnicodeText),
|
|
323
|
+
# Callback URL:
|
|
324
|
+
sqlalchemy.Column("result_url", sqlalchemy.UnicodeText),
|
|
325
|
+
)
|
|
326
|
+
return _jobs_table
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _init_metadata_table():
|
|
330
|
+
"""Initialise the "metadata" table in the db."""
|
|
331
|
+
_metadata_table = sqlalchemy.Table(
|
|
332
|
+
"metadata",
|
|
333
|
+
_METADATA,
|
|
334
|
+
sqlalchemy.Column(
|
|
335
|
+
"job_id",
|
|
336
|
+
sqlalchemy.ForeignKey("jobs.job_id", ondelete="CASCADE"),
|
|
337
|
+
nullable=False,
|
|
338
|
+
primary_key=True,
|
|
339
|
+
),
|
|
340
|
+
sqlalchemy.Column("key", sqlalchemy.UnicodeText, primary_key=True),
|
|
341
|
+
sqlalchemy.Column("value", sqlalchemy.UnicodeText, index=True),
|
|
342
|
+
sqlalchemy.Column("type", sqlalchemy.UnicodeText),
|
|
343
|
+
)
|
|
344
|
+
return _metadata_table
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _init_logs_table():
|
|
348
|
+
"""Initialise the "logs" table in the db."""
|
|
349
|
+
_logs_table = sqlalchemy.Table(
|
|
350
|
+
"logs",
|
|
351
|
+
_METADATA,
|
|
352
|
+
sqlalchemy.Column(
|
|
353
|
+
"job_id",
|
|
354
|
+
sqlalchemy.ForeignKey("jobs.job_id", ondelete="CASCADE"),
|
|
355
|
+
nullable=False,
|
|
356
|
+
),
|
|
357
|
+
sqlalchemy.Column("timestamp", sqlalchemy.DateTime),
|
|
358
|
+
sqlalchemy.Column("message", sqlalchemy.UnicodeText),
|
|
359
|
+
sqlalchemy.Column("level", sqlalchemy.UnicodeText),
|
|
360
|
+
sqlalchemy.Column("module", sqlalchemy.UnicodeText),
|
|
361
|
+
sqlalchemy.Column("funcName", sqlalchemy.UnicodeText),
|
|
362
|
+
sqlalchemy.Column("lineno", sqlalchemy.Integer),
|
|
363
|
+
)
|
|
364
|
+
return _logs_table
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _get_metadata(job_id):
|
|
368
|
+
"""Return any metadata for the given job_id from the metadata table."""
|
|
369
|
+
job_id = six.text_type(job_id)
|
|
370
|
+
|
|
371
|
+
with ENGINE.connect() as conn:
|
|
372
|
+
results = conn.execute(
|
|
373
|
+
METADATA_TABLE.select().where(METADATA_TABLE.c.job_id == job_id)
|
|
374
|
+
).fetchall()
|
|
375
|
+
|
|
376
|
+
metadata = {
|
|
377
|
+
row["key"]: json.loads(row["value"]) if row["type"] == "json" else row["value"]
|
|
378
|
+
for row in results
|
|
379
|
+
}
|
|
380
|
+
return metadata
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _get_logs(job_id):
|
|
384
|
+
"""Return any logs for the given job_id from the logs table."""
|
|
385
|
+
job_id = six.text_type(job_id)
|
|
386
|
+
|
|
387
|
+
with ENGINE.connect() as conn:
|
|
388
|
+
results = conn.execute(
|
|
389
|
+
LOGS_TABLE.select().where(LOGS_TABLE.c.job_id == job_id)
|
|
390
|
+
).fetchall()
|
|
391
|
+
|
|
392
|
+
results = [dict(result) for result in results]
|
|
393
|
+
|
|
394
|
+
for result in results:
|
|
395
|
+
result.pop("job_id")
|
|
396
|
+
|
|
397
|
+
return results
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import ckan.plugins.toolkit as toolkit
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
log = __import__("logging").getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def csvwmapandtransform__status_description(status: dict[str, Any]):
|
|
13
|
+
_ = toolkit._
|
|
14
|
+
|
|
15
|
+
if status.get("status"):
|
|
16
|
+
captions = {
|
|
17
|
+
"complete": _("Complete"),
|
|
18
|
+
"pending": _("Pending"),
|
|
19
|
+
"submitting": _("Submitting"),
|
|
20
|
+
"error": _("Error"),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return captions.get(status["status"], status["status"].capitalize())
|
|
24
|
+
else:
|
|
25
|
+
return _("Not Uploaded Yet")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def common_member(a, b):
|
|
29
|
+
return any(i in b for i in a)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def csvwmapandtransform_show_tools(resource):
|
|
33
|
+
formats = toolkit.config.get("ckanext.csvwmapandtransform.formats")
|
|
34
|
+
|
|
35
|
+
format_parts = re.split("/|;", resource["format"].lower().replace(" ", ""))
|
|
36
|
+
if common_member(format_parts, formats):
|
|
37
|
+
return True
|
|
38
|
+
else:
|
|
39
|
+
False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def csvwmapandtransform_service_available():
|
|
43
|
+
url = toolkit.config.get("ckanext.csvwmapandtransform.maptomethod_url")
|
|
44
|
+
ssl_verify = toolkit.config.get("ckanext.csvwmapandtransform.ssl_verify")
|
|
45
|
+
# log.debug(f"mapomethodurl: {url} {bool(url)}")
|
|
46
|
+
if not url:
|
|
47
|
+
return False # If EXTRACT_URL is not set, return False
|
|
48
|
+
try:
|
|
49
|
+
# Perform a HEAD request (lightweight check) to see if the service responds
|
|
50
|
+
response = requests.head(url, timeout=5, verify=ssl_verify)
|
|
51
|
+
# log.debug(f"reponse: {response}")
|
|
52
|
+
if (200 <= response.status_code < 400) or response.status_code == 405:
|
|
53
|
+
return True # URL is reachable and returns a valid status code
|
|
54
|
+
else:
|
|
55
|
+
return False # URL is reachable but response status is not valid
|
|
56
|
+
except requests.RequestException as e:
|
|
57
|
+
# If there's any issue (timeout, connection error, etc.)
|
|
58
|
+
# log.debug(e)
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_helpers():
|
|
63
|
+
return {
|
|
64
|
+
"csvwmapandtransform__status_description": csvwmapandtransform__status_description,
|
|
65
|
+
"csvwmapandtransform_show_tools": csvwmapandtransform_show_tools,
|
|
66
|
+
"csvwmapandtransform_service_available": csvwmapandtransform_service_available,
|
|
67
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import ckan.plugins.toolkit as toolkit
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
log = __import__("logging").getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def post_request(url, headers, data, files=None):
|
|
10
|
+
ssl_verify = toolkit.config.get("ckanext.csvwmapandtransform.ssl_verify")
|
|
11
|
+
if not ssl_verify:
|
|
12
|
+
requests.packages.urllib3.disable_warnings()
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
if files:
|
|
16
|
+
# should crate a multipart form upload
|
|
17
|
+
response = requests.post(
|
|
18
|
+
url, data=data, headers=headers, files=files, verify=ssl_verify
|
|
19
|
+
)
|
|
20
|
+
else:
|
|
21
|
+
# a application json post request
|
|
22
|
+
response = requests.post(
|
|
23
|
+
url, data=json.dumps(data), headers=headers, verify=ssl_verify
|
|
24
|
+
)
|
|
25
|
+
response.raise_for_status()
|
|
26
|
+
|
|
27
|
+
except Exception as e:
|
|
28
|
+
# placeholder for save file / clean-up
|
|
29
|
+
log.error(e)
|
|
30
|
+
return None
|
|
31
|
+
# raise SystemExit(e) from None
|
|
32
|
+
return response
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def check_mapping(map_url: str, data_url: str, authorization: None):
|
|
36
|
+
rdfconverter_url = toolkit.config.get(
|
|
37
|
+
"ckanext.csvwmapandtransform.rdfconverter_url"
|
|
38
|
+
)
|
|
39
|
+
log.debug("checking mapping at: {} with data url: {}".format(map_url, data_url))
|
|
40
|
+
# curl -X 'POST' 'http://docker-dev.iwm.fraunhofer.de:5003/api/checkmapping' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"data_url": "https://raw.githubusercontent.com/Mat-O-Lab/CSVToCSVW/main/examples/example-metadata.json", "mapping_url": "https://github.com/Mat-O-Lab/MapToMethod/raw/main/examples/example-map.yaml"}'
|
|
41
|
+
url = rdfconverter_url + "/api/checkmapping"
|
|
42
|
+
log.debug("rdf converter api call: {}".format(url))
|
|
43
|
+
data = {"mapping_url": map_url, "data_url": data_url}
|
|
44
|
+
headers = {"Content-Type": "application/json"}
|
|
45
|
+
if authorization:
|
|
46
|
+
headers["Authorization"] = authorization
|
|
47
|
+
r = post_request(url, headers, data)
|
|
48
|
+
# r=requests.get(rdfconverter_url+"/info")
|
|
49
|
+
# log.debug(r)
|
|
50
|
+
if r and r.status_code == 200:
|
|
51
|
+
res = r.json()
|
|
52
|
+
log.debug("map check results: {}".format(res))
|
|
53
|
+
return res
|
|
54
|
+
else:
|
|
55
|
+
log.debug("map check error: {}".format(r))
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_joined_rdf(map_url: str, data_url: str, authorization: None):
|
|
60
|
+
log.debug("createing joined rdf: {} with data url: {}".format(map_url, data_url))
|
|
61
|
+
rdfconverter_url = toolkit.config.get(
|
|
62
|
+
"ckanext.csvwmapandtransform.rdfconverter_url"
|
|
63
|
+
)
|
|
64
|
+
url = rdfconverter_url + "/api/createrdf?return_type=turtle"
|
|
65
|
+
data = {"mapping_url": map_url, "data_url": data_url}
|
|
66
|
+
headers = {"Content-type": "application/json", "Accept": "application/json"}
|
|
67
|
+
if authorization:
|
|
68
|
+
headers["Authorization"] = authorization
|
|
69
|
+
log.debug(f"headers: {headers}")
|
|
70
|
+
|
|
71
|
+
r = post_request(url, headers, data)
|
|
72
|
+
if r and r.status_code == 200:
|
|
73
|
+
r = r.json()
|
|
74
|
+
filename = r["filename"]
|
|
75
|
+
print(
|
|
76
|
+
"applied {} mapping rules and skipped {}".format(
|
|
77
|
+
r["num_mappings_applied"], r["num_mappings_skipped"]
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
return (
|
|
81
|
+
filename,
|
|
82
|
+
r["graph"],
|
|
83
|
+
r["num_mappings_applied"],
|
|
84
|
+
r["num_mappings_skipped"],
|
|
85
|
+
)
|
|
86
|
+
else:
|
|
87
|
+
return (
|
|
88
|
+
None,
|
|
89
|
+
None,
|
|
90
|
+
None,
|
|
91
|
+
None,
|
|
92
|
+
)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
import ckan.plugins as plugins
|
|
5
|
+
import ckan.plugins.toolkit as toolkit
|
|
6
|
+
from ckan import model
|
|
7
|
+
from ckan.config.declaration import Declaration, Key
|
|
8
|
+
from ckan.lib.plugins import DefaultTranslation
|
|
9
|
+
|
|
10
|
+
if toolkit.check_ckan_version("2.10"):
|
|
11
|
+
from ckan.types import Context
|
|
12
|
+
else:
|
|
13
|
+
|
|
14
|
+
class Context(dict):
|
|
15
|
+
def __init__(self, **kwargs):
|
|
16
|
+
super().__init__(**kwargs)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from ckanext.csvwmapandtransform import action, auth, helpers, views
|
|
22
|
+
|
|
23
|
+
log = __import__("logging").getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CsvwMapAndTransformPlugin(plugins.SingletonPlugin, DefaultTranslation):
|
|
27
|
+
plugins.implements(plugins.ITranslation)
|
|
28
|
+
plugins.implements(plugins.IConfigurer)
|
|
29
|
+
plugins.implements(plugins.IConfigDeclaration)
|
|
30
|
+
plugins.implements(plugins.ITemplateHelpers)
|
|
31
|
+
plugins.implements(plugins.IResourceUrlChange)
|
|
32
|
+
plugins.implements(plugins.IResourceController, inherit=True)
|
|
33
|
+
plugins.implements(plugins.IActions)
|
|
34
|
+
plugins.implements(plugins.IAuthFunctions)
|
|
35
|
+
plugins.implements(plugins.IBlueprint)
|
|
36
|
+
|
|
37
|
+
# IConfigurer
|
|
38
|
+
|
|
39
|
+
def update_config(self, config_):
|
|
40
|
+
toolkit.add_template_directory(config_, "templates")
|
|
41
|
+
toolkit.add_public_directory(config_, "public")
|
|
42
|
+
toolkit.add_resource("assets", "csvwmapandtransform")
|
|
43
|
+
|
|
44
|
+
# IConfigDeclaration
|
|
45
|
+
|
|
46
|
+
def declare_config_options(self, declaration: Declaration, key: Key):
|
|
47
|
+
|
|
48
|
+
declaration.annotate("csvwmapandtransform")
|
|
49
|
+
group = key.ckanext.csvwmapandtransform
|
|
50
|
+
declaration.declare_bool(group.ssl_verify, True)
|
|
51
|
+
declaration.declare(group.db_url, plugins.toolkit.config.get("sqlalchemy.url"))
|
|
52
|
+
declaration.declare(group.maptomethod_url, "https://maptomethod.matolab.org")
|
|
53
|
+
declaration.declare(group.rdfconverter_url, "https://rdfconverter.matolab.org")
|
|
54
|
+
declaration.declare(group.ckan_token, "")
|
|
55
|
+
declaration.declare(
|
|
56
|
+
group.formats, "json json-ld turtle n3 nt hext trig longturtle xml ld+json"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# IResourceUrlChange
|
|
60
|
+
|
|
61
|
+
def notify(self, resource: model.Resource):
|
|
62
|
+
context: Context = {"ignore_auth": True}
|
|
63
|
+
resource_dict = toolkit.get_action("resource_show")(
|
|
64
|
+
context,
|
|
65
|
+
{
|
|
66
|
+
"id": resource.id,
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
self._sumbit_transform(resource_dict)
|
|
70
|
+
|
|
71
|
+
# IResourceController
|
|
72
|
+
|
|
73
|
+
if not toolkit.check_ckan_version("2.10") or toolkit.check_ckan_version("2.11"):
|
|
74
|
+
|
|
75
|
+
def after_create(self, context, resource_dict):
|
|
76
|
+
self.after_resource_create(context, resource_dict)
|
|
77
|
+
|
|
78
|
+
# def before_show(self, resource_dict):
|
|
79
|
+
# self.before_resource_show(resource_dict)
|
|
80
|
+
|
|
81
|
+
def after_update(self, context: Context, resource_dict: dict[str, Any]):
|
|
82
|
+
self._sumbit_transform(resource_dict)
|
|
83
|
+
|
|
84
|
+
def after_resource_create(self, context: Context, resource_dict: dict[str, Any]):
|
|
85
|
+
self._sumbit_transform(resource_dict)
|
|
86
|
+
|
|
87
|
+
def _sumbit_transform(self, resource_dict: dict[str, Any]):
|
|
88
|
+
context = {"model": model, "ignore_auth": True, "defer_commit": True}
|
|
89
|
+
formats = toolkit.config.get("ckanext.csvwmapandtransform.formats")
|
|
90
|
+
format = resource_dict.get("format", None)
|
|
91
|
+
submit = (
|
|
92
|
+
format
|
|
93
|
+
and format.lower() in formats
|
|
94
|
+
and "-joined" not in resource_dict["url"]
|
|
95
|
+
)
|
|
96
|
+
log.debug(
|
|
97
|
+
"Submitting resource {0} with format {1}".format(
|
|
98
|
+
resource_dict["id"], format
|
|
99
|
+
)
|
|
100
|
+
+ " to csvwmapandtransform_transform"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if not submit:
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
log.debug(
|
|
108
|
+
"Submitting resource {0}".format(resource_dict["id"])
|
|
109
|
+
+ " to csvwmapandtransform_transform"
|
|
110
|
+
)
|
|
111
|
+
toolkit.get_action("csvwmapandtransform_transform")(
|
|
112
|
+
context, {"id": resource_dict["id"]}
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
except toolkit.ValidationError as e:
|
|
116
|
+
# If RDFConverter is offline want to catch error instead
|
|
117
|
+
# of raising otherwise resource save will fail with 500
|
|
118
|
+
log.critical(e)
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
# ITemplateHelpers
|
|
122
|
+
|
|
123
|
+
def get_helpers(self):
|
|
124
|
+
return helpers.get_helpers()
|
|
125
|
+
|
|
126
|
+
# IActions
|
|
127
|
+
|
|
128
|
+
def get_actions(self):
|
|
129
|
+
actions = action.get_actions()
|
|
130
|
+
return actions
|
|
131
|
+
|
|
132
|
+
# IBlueprint
|
|
133
|
+
|
|
134
|
+
def get_blueprint(self):
|
|
135
|
+
return views.get_blueprint()
|
|
136
|
+
|
|
137
|
+
# IAuthFunctions
|
|
138
|
+
|
|
139
|
+
def get_auth_functions(self):
|
|
140
|
+
return auth.get_auth_functions()
|