2013-06-20 16:48:40 +02:00
|
|
|
import sys
|
|
|
|
import logging
|
|
|
|
import datetime
|
|
|
|
import io
|
2021-05-28 13:27:12 +02:00
|
|
|
import os
|
|
|
|
import argparse
|
|
|
|
from six.moves.configparser import SafeConfigParser
|
2013-06-20 16:48:40 +02:00
|
|
|
|
|
|
|
import requests
|
|
|
|
from lxml import etree
|
|
|
|
|
2019-10-17 14:29:33 +02:00
|
|
|
from pycsw.core import metadata, repository, util
|
|
|
|
import pycsw.core.config
|
|
|
|
import pycsw.core.admin
|
2013-06-20 16:48:40 +02:00
|
|
|
|
2021-05-28 13:27:12 +02:00
|
|
|
logging.basicConfig(format="%(message)s", level=logging.INFO)
|
2013-06-20 16:48:40 +02:00
|
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
2021-05-28 13:27:12 +02:00
|
|
|
|
2013-06-20 16:48:40 +02:00
|
|
|
def setup_db(pycsw_config):
|
|
|
|
"""Setup database tables and indexes"""
|
2013-08-21 18:51:38 +02:00
|
|
|
|
|
|
|
from sqlalchemy import Column, Text
|
2013-06-20 16:48:40 +02:00
|
|
|
|
2021-05-28 13:27:12 +02:00
|
|
|
database = pycsw_config.get("repository", "database")
|
|
|
|
table_name = pycsw_config.get("repository", "table", "records")
|
2013-06-20 16:48:40 +02:00
|
|
|
|
2013-08-21 18:51:38 +02:00
|
|
|
ckan_columns = [
|
2021-05-28 13:27:12 +02:00
|
|
|
Column("ckan_id", Text, index=True),
|
|
|
|
Column("ckan_modified", Text),
|
2013-08-21 18:51:38 +02:00
|
|
|
]
|
|
|
|
|
2021-05-28 13:27:12 +02:00
|
|
|
pycsw.core.admin.setup_db(
|
|
|
|
database,
|
|
|
|
table_name,
|
|
|
|
"",
|
2013-08-21 18:51:38 +02:00
|
|
|
create_plpythonu_functions=False,
|
2021-05-28 13:27:12 +02:00
|
|
|
extra_columns=ckan_columns,
|
|
|
|
)
|
2013-06-20 16:48:40 +02:00
|
|
|
|
|
|
|
|
2013-11-18 22:32:23 +01:00
|
|
|
def set_keywords(pycsw_config_file, pycsw_config, ckan_url, limit=20):
|
2013-11-22 14:01:00 +01:00
|
|
|
"""set pycsw service metadata keywords from top limit CKAN tags"""
|
2013-11-18 22:32:23 +01:00
|
|
|
|
2021-05-28 13:27:12 +02:00
|
|
|
log.info("Fetching tags from %s", ckan_url)
|
|
|
|
url = ckan_url + "api/tag_counts"
|
2013-11-18 22:32:23 +01:00
|
|
|
response = requests.get(url)
|
|
|
|
tags = response.json()
|
|
|
|
|
2021-05-28 13:27:12 +02:00
|
|
|
log.info("Deriving top %d tags", limit)
|
2013-11-18 22:32:23 +01:00
|
|
|
# uniquify and sort by top limit
|
|
|
|
tags_unique = [list(x) for x in set(tuple(x) for x in tags)]
|
|
|
|
tags_sorted = sorted(tags_unique, key=lambda x: x[1], reverse=1)[0:limit]
|
2021-05-28 13:27:12 +02:00
|
|
|
keywords = ",".join("%s" % tn[0] for tn in tags_sorted)
|
2013-11-18 22:32:23 +01:00
|
|
|
|
2021-05-28 13:27:12 +02:00
|
|
|
log.info("Setting tags in pycsw configuration file %s", pycsw_config_file)
|
|
|
|
pycsw_config.set("metadata:main", "identification_keywords", keywords)
|
|
|
|
with open(pycsw_config_file, "wb") as configfile:
|
2013-11-18 22:32:23 +01:00
|
|
|
pycsw_config.write(configfile)
|
|
|
|
|
|
|
|
|
2013-06-20 16:48:40 +02:00
|
|
|
def load(pycsw_config, ckan_url):
|
|
|
|
|
2021-05-28 13:27:12 +02:00
|
|
|
database = pycsw_config.get("repository", "database")
|
|
|
|
table_name = pycsw_config.get("repository", "table", "records")
|
2013-06-20 16:48:40 +02:00
|
|
|
|
2019-10-17 14:29:33 +02:00
|
|
|
context = pycsw.core.config.StaticContext()
|
2013-06-20 16:48:40 +02:00
|
|
|
repo = repository.Repository(database, context, table=table_name)
|
|
|
|
|
2021-05-28 13:27:12 +02:00
|
|
|
log.info(
|
|
|
|
"Started gathering CKAN datasets identifiers: {0}".format(
|
|
|
|
str(datetime.datetime.now())
|
|
|
|
)
|
|
|
|
)
|
2013-06-20 16:48:40 +02:00
|
|
|
|
2013-06-21 12:59:04 +02:00
|
|
|
query = 'api/search/dataset?qjson={"fl":"id,metadata_modified,extras_harvest_object_id,extras_metadata_source", "q":"harvest_object_id:[\\"\\" TO *]", "limit":1000, "start":%s}'
|
2013-06-20 16:48:40 +02:00
|
|
|
|
|
|
|
start = 0
|
|
|
|
|
|
|
|
gathered_records = {}
|
|
|
|
|
|
|
|
while True:
|
|
|
|
url = ckan_url + query % start
|
|
|
|
|
|
|
|
response = requests.get(url)
|
|
|
|
listing = response.json()
|
2013-06-21 12:46:50 +02:00
|
|
|
if not isinstance(listing, dict):
|
2021-05-28 13:27:12 +02:00
|
|
|
raise RuntimeError("Wrong API response: %s" % listing)
|
|
|
|
results = listing.get("results")
|
2013-06-20 16:48:40 +02:00
|
|
|
if not results:
|
|
|
|
break
|
|
|
|
for result in results:
|
2021-05-28 13:27:12 +02:00
|
|
|
gathered_records[result["id"]] = {
|
|
|
|
"metadata_modified": result["metadata_modified"],
|
|
|
|
"harvest_object_id": result["extras"]["harvest_object_id"],
|
|
|
|
"source": result["extras"].get("metadata_source"),
|
2013-06-20 16:48:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
start = start + 1000
|
2021-05-28 13:27:12 +02:00
|
|
|
log.debug("Gathered %s" % start)
|
2013-06-20 16:48:40 +02:00
|
|
|
|
2021-05-28 13:27:12 +02:00
|
|
|
log.info(
|
|
|
|
"Gather finished ({0} datasets): {1}".format(
|
|
|
|
len(gathered_records.keys()), str(datetime.datetime.now())
|
|
|
|
)
|
|
|
|
)
|
2013-06-20 16:48:40 +02:00
|
|
|
|
|
|
|
existing_records = {}
|
|
|
|
|
|
|
|
query = repo.session.query(repo.dataset.ckan_id, repo.dataset.ckan_modified)
|
|
|
|
for row in query:
|
|
|
|
existing_records[row[0]] = row[1]
|
|
|
|
repo.session.close()
|
|
|
|
|
|
|
|
new = set(gathered_records) - set(existing_records)
|
|
|
|
deleted = set(existing_records) - set(gathered_records)
|
|
|
|
changed = set()
|
|
|
|
|
|
|
|
for key in set(gathered_records) & set(existing_records):
|
2021-05-28 13:27:12 +02:00
|
|
|
if gathered_records[key]["metadata_modified"] > existing_records[key]:
|
2013-06-20 16:48:40 +02:00
|
|
|
changed.add(key)
|
|
|
|
|
|
|
|
for ckan_id in deleted:
|
|
|
|
try:
|
|
|
|
repo.session.begin()
|
2021-05-28 13:27:12 +02:00
|
|
|
repo.session.query(repo.dataset.ckan_id).filter_by(ckan_id=ckan_id).delete()
|
|
|
|
log.info("Deleted %s" % ckan_id)
|
2013-06-20 16:48:40 +02:00
|
|
|
repo.session.commit()
|
2021-05-28 13:27:12 +02:00
|
|
|
except Exception:
|
2013-06-20 16:48:40 +02:00
|
|
|
repo.session.rollback()
|
|
|
|
raise
|
|
|
|
|
|
|
|
for ckan_id in new:
|
|
|
|
ckan_info = gathered_records[ckan_id]
|
|
|
|
record = get_record(context, repo, ckan_url, ckan_id, ckan_info)
|
|
|
|
if not record:
|
2021-05-28 13:27:12 +02:00
|
|
|
log.info("Skipped record %s" % ckan_id)
|
2013-06-20 16:48:40 +02:00
|
|
|
continue
|
|
|
|
try:
|
2021-05-28 13:27:12 +02:00
|
|
|
repo.insert(record, "local", util.get_today_and_now())
|
|
|
|
log.info("Inserted %s" % ckan_id)
|
2021-01-15 06:15:22 +01:00
|
|
|
except Exception as err:
|
2021-05-28 13:27:12 +02:00
|
|
|
log.error("ERROR: not inserted %s Error:%s" % (ckan_id, err))
|
2013-06-20 16:48:40 +02:00
|
|
|
|
|
|
|
for ckan_id in changed:
|
|
|
|
ckan_info = gathered_records[ckan_id]
|
|
|
|
record = get_record(context, repo, ckan_url, ckan_id, ckan_info)
|
|
|
|
if not record:
|
|
|
|
continue
|
2021-05-28 13:27:12 +02:00
|
|
|
update_dict = dict(
|
|
|
|
[
|
|
|
|
(getattr(repo.dataset, key), getattr(record, key))
|
|
|
|
for key in record.__dict__.keys()
|
|
|
|
if key != "_sa_instance_state"
|
|
|
|
]
|
|
|
|
)
|
2013-06-20 16:48:40 +02:00
|
|
|
try:
|
|
|
|
repo.session.begin()
|
2021-05-28 13:27:12 +02:00
|
|
|
repo.session.query(repo.dataset).filter_by(ckan_id=ckan_id).update(
|
|
|
|
update_dict
|
|
|
|
)
|
2013-06-20 16:48:40 +02:00
|
|
|
repo.session.commit()
|
2021-05-28 13:27:12 +02:00
|
|
|
log.info("Changed %s" % ckan_id)
|
2021-01-15 06:15:22 +01:00
|
|
|
except Exception as err:
|
2013-06-20 16:48:40 +02:00
|
|
|
repo.session.rollback()
|
2021-05-28 13:27:12 +02:00
|
|
|
raise RuntimeError("ERROR: %s" % str(err))
|
2013-06-20 16:48:40 +02:00
|
|
|
|
|
|
|
|
2013-06-20 17:45:37 +02:00
|
|
|
def clear(pycsw_config):
|
|
|
|
|
|
|
|
from sqlalchemy import create_engine, MetaData, Table
|
|
|
|
|
2021-05-28 13:27:12 +02:00
|
|
|
database = pycsw_config.get("repository", "database")
|
|
|
|
table_name = pycsw_config.get("repository", "table", "records")
|
2013-06-20 17:45:37 +02:00
|
|
|
|
2021-05-28 13:27:12 +02:00
|
|
|
log.debug("Creating engine")
|
2013-06-20 17:45:37 +02:00
|
|
|
engine = create_engine(database)
|
|
|
|
records = Table(table_name, MetaData(engine))
|
|
|
|
records.delete().execute()
|
2021-05-28 13:27:12 +02:00
|
|
|
log.info("Table cleared")
|
2013-06-20 17:45:37 +02:00
|
|
|
|
|
|
|
|
2013-06-20 16:48:40 +02:00
|
|
|
def get_record(context, repo, ckan_url, ckan_id, ckan_info):
|
2021-05-28 13:27:12 +02:00
|
|
|
query = ckan_url + "harvest/object/%s"
|
|
|
|
url = query % ckan_info["harvest_object_id"]
|
2013-06-20 16:48:40 +02:00
|
|
|
response = requests.get(url)
|
|
|
|
|
2021-05-28 13:27:12 +02:00
|
|
|
if ckan_info["source"] == "arcgis":
|
2013-06-20 16:48:40 +02:00
|
|
|
return
|
|
|
|
|
|
|
|
try:
|
|
|
|
xml = etree.parse(io.BytesIO(response.content))
|
2021-01-15 06:15:22 +01:00
|
|
|
except Exception as err:
|
2021-05-28 13:27:12 +02:00
|
|
|
log.error("Could not pass xml doc from %s, Error: %s" % (ckan_id, err))
|
2013-06-20 16:48:40 +02:00
|
|
|
return
|
|
|
|
|
|
|
|
try:
|
|
|
|
record = metadata.parse_record(context, xml, repo)[0]
|
2021-01-15 06:15:22 +01:00
|
|
|
except Exception as err:
|
2021-05-28 13:27:12 +02:00
|
|
|
log.error("Could not extract metadata from %s, Error: %s" % (ckan_id, err))
|
2013-06-20 16:48:40 +02:00
|
|
|
return
|
|
|
|
|
|
|
|
if not record.identifier:
|
|
|
|
record.identifier = ckan_id
|
|
|
|
record.ckan_id = ckan_id
|
2021-05-28 13:27:12 +02:00
|
|
|
record.ckan_modified = ckan_info["metadata_modified"]
|
2013-06-20 16:48:40 +02:00
|
|
|
|
|
|
|
return record
|
|
|
|
|
|
|
|
|
2021-05-28 13:27:12 +02:00
|
|
|
usage = """
|
2013-06-20 16:48:40 +02:00
|
|
|
Manages the CKAN-pycsw integration
|
|
|
|
|
|
|
|
python ckan-pycsw.py setup [-p]
|
|
|
|
Setups the necessary pycsw table on the db.
|
|
|
|
|
2013-11-18 22:32:23 +01:00
|
|
|
python ckan-pycsw.py set_keywords [-p] -u
|
|
|
|
Sets pycsw server metadata keywords from CKAN site tag list.
|
|
|
|
|
2013-06-20 16:48:40 +02:00
|
|
|
python ckan-pycsw.py load [-p] -u
|
|
|
|
Loads CKAN datasets as records into the pycsw db.
|
|
|
|
|
2013-06-21 14:21:30 +02:00
|
|
|
python ckan-pycsw.py clear [-p]
|
|
|
|
Removes all records from the pycsw table.
|
|
|
|
|
2013-06-20 16:48:40 +02:00
|
|
|
All commands require the pycsw configuration file. By default it will try
|
|
|
|
to find a file called 'default.cfg' in the same directory, but you'll
|
|
|
|
probably need to provide the actual location via the -p option:
|
|
|
|
|
2021-05-28 13:27:12 +02:00
|
|
|
python ckan_pycsw.py setup -p /etc/ckan/default/pycsw.cfg
|
2013-06-20 16:48:40 +02:00
|
|
|
|
|
|
|
The load command requires a CKAN URL from where the datasets will be pulled:
|
|
|
|
|
2021-05-28 13:27:12 +02:00
|
|
|
python ckan_pycsw.py load -p /etc/ckan/default/pycsw.cfg -u http://localhost
|
|
|
|
|
|
|
|
"""
|
2013-06-20 16:48:40 +02:00
|
|
|
|
|
|
|
|
|
|
|
def _load_config(file_path):
|
|
|
|
abs_path = os.path.abspath(file_path)
|
|
|
|
if not os.path.exists(abs_path):
|
2021-05-28 13:27:12 +02:00
|
|
|
raise AssertionError("pycsw config file {0} does not exist.".format(abs_path))
|
2013-06-20 16:48:40 +02:00
|
|
|
|
|
|
|
config = SafeConfigParser()
|
|
|
|
config.read(abs_path)
|
|
|
|
|
|
|
|
return config
|
|
|
|
|
|
|
|
|
2021-05-28 13:27:12 +02:00
|
|
|
if __name__ == "__main__":
|
|
|
|
parser = argparse.ArgumentParser(description="\n".split(usage)[0], usage=usage)
|
|
|
|
parser.add_argument("command", help="Command to perform")
|
2013-06-20 16:48:40 +02:00
|
|
|
|
2021-05-28 13:27:12 +02:00
|
|
|
parser.add_argument(
|
|
|
|
"-p",
|
|
|
|
"--pycsw_config",
|
|
|
|
action="store",
|
|
|
|
default="default.cfg",
|
|
|
|
help="pycsw config file to use.",
|
|
|
|
)
|
2013-06-20 16:48:40 +02:00
|
|
|
|
2021-05-28 13:27:12 +02:00
|
|
|
parser.add_argument(
|
|
|
|
"-u",
|
|
|
|
"--ckan_url",
|
|
|
|
action="store",
|
|
|
|
help="CKAN instance to import the datasets from.",
|
|
|
|
)
|
2013-06-20 16:48:40 +02:00
|
|
|
|
|
|
|
if len(sys.argv) <= 1:
|
|
|
|
parser.print_usage()
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
arg = parser.parse_args()
|
|
|
|
pycsw_config = _load_config(arg.pycsw_config)
|
|
|
|
|
2021-05-28 13:27:12 +02:00
|
|
|
if arg.command == "setup":
|
2013-06-20 16:48:40 +02:00
|
|
|
setup_db(pycsw_config)
|
2021-05-28 13:27:12 +02:00
|
|
|
elif arg.command in ["load", "set_keywords"]:
|
2013-06-20 16:48:40 +02:00
|
|
|
if not arg.ckan_url:
|
2021-05-28 13:27:12 +02:00
|
|
|
raise AssertionError("You need to provide a CKAN URL with -u or --ckan_url")
|
|
|
|
ckan_url = arg.ckan_url.rstrip("/") + "/"
|
|
|
|
if arg.command == "load":
|
2013-11-18 22:32:23 +01:00
|
|
|
load(pycsw_config, ckan_url)
|
|
|
|
else:
|
|
|
|
set_keywords(arg.pycsw_config, pycsw_config, ckan_url)
|
2021-05-28 13:27:12 +02:00
|
|
|
elif arg.command == "clear":
|
2013-06-20 17:45:37 +02:00
|
|
|
clear(pycsw_config)
|
2013-06-20 16:48:40 +02:00
|
|
|
else:
|
2021-05-28 13:27:12 +02:00
|
|
|
print("Unknown command {0}".format(arg.command))
|
2013-06-20 16:48:40 +02:00
|
|
|
sys.exit(1)
|