Changeset View
Changeset View
Standalone View
Standalone View
swh/deposit/cli/deposit.py
# Copyright (C) 2017-2019 The Software Heritage developers | # Copyright (C) 2017-2019 The Software Heritage developers | ||||
# See the AUTHORS file at the top-level directory of this distribution | # See the AUTHORS file at the top-level directory of this distribution | ||||
# License: GNU General Public License version 3, or any later version | # License: GNU General Public License version 3, or any later version | ||||
# See top-level LICENSE file for more information | # See top-level LICENSE file for more information | ||||
import logging | |||||
import os | import os | ||||
import logging | |||||
import tempfile | |||||
import uuid | import uuid | ||||
import click | import click | ||||
import xmltodict | |||||
from swh.deposit.client import PublicApiDepositClient | from swh.deposit.client import PublicApiDepositClient | ||||
from swh.deposit.cli import cli | from swh.deposit.cli import cli | ||||
logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||
class InputError(ValueError): | class InputError(ValueError): | ||||
"""Input script error | """Input script error | ||||
""" | """ | ||||
pass | pass | ||||
def generate_slug(): | def generate_slug(): | ||||
"""Generate a slug (sample purposes). | """Generate a slug (sample purposes). | ||||
""" | """ | ||||
return str(uuid.uuid4()) | return str(uuid.uuid4()) | ||||
def generate_metadata_file(name, authors): | |||||
"""Generate a temporary metadata file with the minimum required metadata | |||||
This generates a xml file in a temporary location and returns the | |||||
path to that file. | |||||
This is up to the client of that function to clean up the | |||||
temporary file. | |||||
""" | |||||
_, tmpfile = tempfile.mkstemp(prefix='swh.deposit.cli.') | |||||
# generate a metadata file with the minimum required metadata | |||||
codemetadata = { | |||||
'entry': { | |||||
'@xmlns': "http://www.w3.org/2005/Atom", | |||||
'@xmlns:codemeta': "https://doi.org/10.5063/SCHEMA/CODEMETA-2.0", | |||||
'codemeta:name': name, | |||||
'codemeta:author': [{ | |||||
'codemeta:name': author_name | |||||
} for author_name in authors], | |||||
}, | |||||
moranegg: I would add the slug or the made-up slug to this file (if possible) with:
<codemeta:identifier>… | |||||
} | |||||
logging.debug('Temporary file: %s', tmpfile) | |||||
logging.debug('Metadata dict to generate as xml: %s', codemetadata) | |||||
s = xmltodict.unparse(codemetadata, pretty=True) | |||||
logging.debug('Metadata dict as xml generated: %s', s) | |||||
with open(tmpfile, 'w') as fp: | |||||
fp.write(s) | |||||
return tmpfile | |||||
def _cleanup_tempfile(config): | |||||
"""Clean up the temporary metadata file generated. | |||||
Args: | |||||
config (Dict): A configuration dict with 2 important keys for | |||||
that routine, 'cleanup_tempfile' (bool) and 'metadata' (path | |||||
to eventually clean up) | |||||
""" | |||||
if config['cleanup_tempfile']: | |||||
path = config['metadata'] | |||||
if os.path.exists(path): | |||||
os.unlink(path) | |||||
def client_command_parse_input( | def client_command_parse_input( | ||||
username, password, archive, metadata, | username, password, archive, metadata, | ||||
archive_deposit, metadata_deposit, | archive_deposit, metadata_deposit, | ||||
collection, slug, partial, deposit_id, replace, | collection, slug, partial, deposit_id, replace, | ||||
url, status): | url, status, name, authors): | ||||
"""Parse the client subcommand options and make sure the combination | """Parse the client subcommand options and make sure the combination | ||||
is acceptable*. If not, an InputError exception is raised | is acceptable*. If not, an InputError exception is raised | ||||
explaining the issue. | explaining the issue. | ||||
By acceptable, we mean: | By acceptable, we mean: | ||||
- A multipart deposit (create or update) needs both an | - A multipart deposit (create or update) needs both an | ||||
existing software archive and an existing metadata file | existing software archive and an existing metadata file | ||||
Show All 23 Lines | Returns: | ||||
'slug': the slug or external id identifying the deposit to make | 'slug': the slug or external id identifying the deposit to make | ||||
'partial': if the deposit is partial or not | 'partial': if the deposit is partial or not | ||||
'client': instantiated class | 'client': instantiated class | ||||
'url': deposit's server main entry point | 'url': deposit's server main entry point | ||||
'deposit_type': deposit's type (binary, multipart, metadata) | 'deposit_type': deposit's type (binary, multipart, metadata) | ||||
'deposit_id': optional deposit identifier | 'deposit_id': optional deposit identifier | ||||
""" | """ | ||||
cleanup_tempfile = False | |||||
try: | |||||
if status and not deposit_id: | if status and not deposit_id: | ||||
raise InputError("Deposit id must be provided for status check") | raise InputError("Deposit id must be provided for status check") | ||||
if status and deposit_id: # status is higher priority over deposit | if status and deposit_id: # status is higher priority over deposit | ||||
archive_deposit = False | archive_deposit = False | ||||
metadata_deposit = False | metadata_deposit = False | ||||
archive = None | archive = None | ||||
metadata = None | metadata = None | ||||
if archive_deposit and metadata_deposit: | if archive_deposit and metadata_deposit: | ||||
# too many flags use, remove redundant ones (-> multipart deposit) | # too many flags use, remove redundant ones (-> multipart deposit) | ||||
archive_deposit = False | archive_deposit = False | ||||
metadata_deposit = False | metadata_deposit = False | ||||
if archive and not os.path.exists(archive): | if archive and not os.path.exists(archive): | ||||
raise InputError('Software Archive %s must exist!' % archive) | raise InputError('Software Archive %s must exist!' % archive) | ||||
if archive and not metadata: | if archive and not metadata: # we need to have the metadata | ||||
metadata = '%s.metadata.xml' % archive | if name and authors: | ||||
metadata = generate_metadata_file(name, authors) | |||||
cleanup_tempfile = True | |||||
else: | |||||
raise InputError('Either metadata deposit file or (`--name` ' | |||||
' and `--author`) fields must be provided') | |||||
if metadata_deposit: | if metadata_deposit: | ||||
archive = None | archive = None | ||||
if archive_deposit: | if archive_deposit: | ||||
metadata = None | metadata = None | ||||
if metadata_deposit and not metadata: | if metadata_deposit and not metadata: | ||||
raise InputError( | raise InputError( | ||||
"Metadata deposit filepath must be provided for metadata deposit") | "Metadata deposit filepath must be provided for metadata " | ||||
"deposit") | |||||
if metadata and not os.path.exists(metadata): | if metadata and not os.path.exists(metadata): | ||||
raise InputError('Software Archive metadata %s must exist!' % metadata) | raise InputError('Software Archive metadata %s must exist!' % ( | ||||
metadata, )) | |||||
if not status and not archive and not metadata: | if not status and not archive and not metadata: | ||||
raise InputError( | raise InputError( | ||||
'Please provide an actionable command. See --help for more ' | 'Please provide an actionable command. See --help for more ' | ||||
'information.') | 'information.') | ||||
if replace and not deposit_id: | if replace and not deposit_id: | ||||
raise InputError( | raise InputError( | ||||
'To update an existing deposit, you must provide its id') | 'To update an existing deposit, you must provide its id') | ||||
client = PublicApiDepositClient({ | client = PublicApiDepositClient({ | ||||
'url': url, | 'url': url, | ||||
'auth': { | 'auth': { | ||||
'username': username, | 'username': username, | ||||
'password': password | 'password': password | ||||
}, | }, | ||||
}) | }) | ||||
if not collection: | if not collection: | ||||
# retrieve user's collection | # retrieve user's collection | ||||
sd_content = client.service_document() | sd_content = client.service_document() | ||||
if 'error' in sd_content: | if 'error' in sd_content: | ||||
raise InputError('Service document retrieval: %s' % ( | raise InputError('Service document retrieval: %s' % ( | ||||
sd_content['error'], )) | sd_content['error'], )) | ||||
collection = sd_content[ | collection = sd_content[ | ||||
'service']['workspace']['collection']['sword:name'] | 'service']['workspace']['collection']['sword:name'] | ||||
if not slug: | if not slug: | ||||
# generate slug | # generate slug | ||||
slug = generate_slug() | slug = generate_slug() | ||||
return { | return { | ||||
'archive': archive, | 'archive': archive, | ||||
'username': username, | 'username': username, | ||||
'password': password, | 'password': password, | ||||
'metadata': metadata, | 'metadata': metadata, | ||||
'cleanup_tempfile': cleanup_tempfile, | |||||
'collection': collection, | 'collection': collection, | ||||
'slug': slug, | 'slug': slug, | ||||
'in_progress': partial, | 'in_progress': partial, | ||||
'client': client, | 'client': client, | ||||
'url': url, | 'url': url, | ||||
'deposit_id': deposit_id, | 'deposit_id': deposit_id, | ||||
'replace': replace, | 'replace': replace, | ||||
} | } | ||||
except Exception: # to be clean, cleanup prior to raise | |||||
_cleanup_tempfile({ | |||||
'cleanup_tempfile': cleanup_tempfile, | |||||
'metadata': metadata | |||||
}) | |||||
raise | |||||
def _subdict(d, keys): | def _subdict(d, keys): | ||||
'return a dict from d with only given keys' | 'return a dict from d with only given keys' | ||||
return {k: v for k, v in d.items() if k in keys} | return {k: v for k, v in d.items() if k in keys} | ||||
def deposit_status(config, logger): | def deposit_status(config, logger): | ||||
▲ Show 20 Lines • Show All 53 Lines • ▼ Show 20 Lines | |||||
@click.option('--replace/--no-replace', default=False, | @click.option('--replace/--no-replace', default=False, | ||||
help='(Optional) Update by replacing existing metadata to a deposit') # noqa | help='(Optional) Update by replacing existing metadata to a deposit') # noqa | ||||
@click.option('--url', default='https://deposit.softwareheritage.org/1', | @click.option('--url', default='https://deposit.softwareheritage.org/1', | ||||
help="(Optional) Deposit server api endpoint. By default, https://deposit.softwareheritage.org/1") # noqa | help="(Optional) Deposit server api endpoint. By default, https://deposit.softwareheritage.org/1") # noqa | ||||
@click.option('--status/--no-status', default=False, | @click.option('--status/--no-status', default=False, | ||||
help="(Optional) Deposit's status") | help="(Optional) Deposit's status") | ||||
@click.option('--verbose/--no-verbose', default=False, | @click.option('--verbose/--no-verbose', default=False, | ||||
help='Verbose mode') | help='Verbose mode') | ||||
@click.option('--name', | |||||
help='Software name') | |||||
@click.option('--author', multiple=True, | |||||
help='Software author(s), this can be repeated as many times' | |||||
' as there are authors') | |||||
@click.pass_context | @click.pass_context | ||||
def deposit(ctx, | def deposit(ctx, | ||||
username, password, archive=None, metadata=None, | username, password, archive=None, metadata=None, | ||||
archive_deposit=False, metadata_deposit=False, | archive_deposit=False, metadata_deposit=False, | ||||
collection=None, slug=None, partial=False, deposit_id=None, | collection=None, slug=None, partial=False, deposit_id=None, | ||||
replace=False, status=False, | replace=False, status=False, | ||||
url='https://deposit.softwareheritage.org/1', | url='https://deposit.softwareheritage.org/1', | ||||
moraneggUnsubmitted Not Done Inline ActionsIs this url used? or was it for the manadatory url entry? moranegg: Is this url used? or was it for the manadatory url entry? | |||||
ardumontAuthorUnsubmitted Done Inline Actions
yes, it is. Thus the need to override it in the sample i provided.
It is not. ardumont: > Is this url used?
yes, it is.
It is the real deposit server uri (that is not something new… | |||||
moraneggUnsubmitted Not Done Inline Actionsoh right, I thought it was the url of the deposit artifact. moranegg: oh right, I thought it was the url of the deposit artifact.
| |||||
verbose=False): | verbose=False, name=None, author=None): | ||||
"""Software Heritage Public Deposit Client | """Software Heritage Public Deposit Client | ||||
Create/Update deposit through the command line or access its | Create/Update deposit through the command line or access its | ||||
status. | status. | ||||
More documentation can be found at | More documentation can be found at | ||||
https://docs.softwareheritage.org/devel/swh-deposit/getting-started.html. | https://docs.softwareheritage.org/devel/swh-deposit/getting-started.html. | ||||
""" | """ | ||||
config = {} | config = {} | ||||
try: | try: | ||||
logger.debug('Parsing cli options') | logger.debug('Parsing cli options') | ||||
config = client_command_parse_input( | config = client_command_parse_input( | ||||
username, password, archive, metadata, archive_deposit, | username, password, archive, metadata, archive_deposit, | ||||
metadata_deposit, collection, slug, partial, deposit_id, | metadata_deposit, collection, slug, partial, deposit_id, | ||||
replace, url, status) | replace, url, status, name, author) | ||||
except InputError as e: | except InputError as e: | ||||
msg = 'Problem during parsing options: %s' % e | msg = 'Problem during parsing options: %s' % e | ||||
r = { | r = { | ||||
'error': msg, | 'error': msg, | ||||
} | } | ||||
logger.info(r) | logger.info(r) | ||||
return 1 | return 1 | ||||
try: | |||||
if verbose: | if verbose: | ||||
logger.info("Parsed configuration: %s" % ( | logger.info("Parsed configuration: %s" % ( | ||||
config, )) | config, )) | ||||
deposit_id = config['deposit_id'] | deposit_id = config['deposit_id'] | ||||
if status and deposit_id: | if status and deposit_id: | ||||
r = deposit_status(config, logger) | r = deposit_status(config, logger) | ||||
elif not status and deposit_id: | elif not status and deposit_id: | ||||
r = deposit_update(config, logger) | r = deposit_update(config, logger) | ||||
elif not status and not deposit_id: | elif not status and not deposit_id: | ||||
r = deposit_create(config, logger) | r = deposit_create(config, logger) | ||||
logger.info(r) | logger.info(r) | ||||
finally: | |||||
_cleanup_tempfile(config) |
I would add the slug or the made-up slug to this file (if possible) with:
<codemeta:identifier> slug <codemeta:identifier>