Changeset View
Changeset View
Standalone View
Standalone View
swh/deposit/cli/client.py
Show First 20 Lines • Show All 84 Lines • ▼ Show 20 Lines | def _cleanup_tempfile(config): | ||||
""" | """ | ||||
if config['cleanup_tempfile']: | if config['cleanup_tempfile']: | ||||
path = config['metadata'] | path = config['metadata'] | ||||
if os.path.exists(path): | if os.path.exists(path): | ||||
os.unlink(path) | os.unlink(path) | ||||
def _client(url, username, password): | |||||
"""Instantiate a client to access the deposit api server | |||||
Args: | |||||
url (str): Deposit api server | |||||
username (str): User | |||||
password (str): User's password | |||||
""" | |||||
client = PublicApiDepositClient({ | |||||
'url': url, | |||||
'auth': { | |||||
'username': username, | |||||
'password': password | |||||
}, | |||||
}) | |||||
return client | |||||
def _collection(client): | |||||
"""Retrieve the client's collection | |||||
""" | |||||
# retrieve user's collection | |||||
sd_content = client.service_document() | |||||
if 'error' in sd_content: | |||||
raise InputError('Service document retrieval: %s' % ( | |||||
sd_content['error'], )) | |||||
collection = sd_content[ | |||||
'service']['workspace']['collection']['sword:name'] | |||||
return collection | |||||
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, name, authors): | url, 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 | ||||
existing software archive and an existing metadata file | software archive and an existing metadata file (or at least | ||||
authors and name provided) | |||||
moranegg: needs=> requires
```
- A multipart deposit (create or update) requires:
-an… | |||||
- A binary deposit (create/update) needs an existing | - A binary deposit (create/update) needs an existing | ||||
Done Inline Actionsrequires moranegg: requires | |||||
software archive | software archive | ||||
- A metadata deposit (create/update) needs an existing | - A metadata deposit (create/update) needs an existing | ||||
Done Inline Actionsrequires moranegg: requires | |||||
metadata file | metadata file (or at least authors and name provided) | ||||
Done Inline Actionswithout ( ) metadata file or the authors and software artifact name provided in params moranegg: without ( )
```
metadata file or the authors and software artifact name provided in params
``` | |||||
- A deposit update needs a deposit_id to be provided | - A deposit update needs a deposit_id to be provided | ||||
Done Inline Actionsrequires A deposit update requires a deposit_id moranegg: requires
and erase to be provided
```
A deposit update requires a deposit_id
``` | |||||
This won't prevent all failure cases though. The remaining | This will not prevent all failure cases though. The remaining | ||||
errors are already dealt with the underlying api client. | errors are already dealt with by the underlying api client. | ||||
Raises: | Raises: | ||||
InputError explaining the issue | InputError explaining the issue | ||||
Returns: | Returns: | ||||
dict with the following keys: | dict with the following keys: | ||||
'archive': the software archive to deposit | 'archive': the software archive to deposit | ||||
'username': username | 'username': username | ||||
'password': associated password | 'password': associated password | ||||
'metadata': the metadata file to deposit | 'metadata': the metadata file to deposit | ||||
'collection': the username's associated client | 'collection': the username's associated client | ||||
'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 | cleanup_tempfile = False | ||||
try: | try: | ||||
if status and not deposit_id: | |||||
raise InputError("Deposit id must be provided for status check") | |||||
if status and deposit_id: # status is higher priority over deposit | |||||
archive_deposit = False | |||||
metadata_deposit = False | |||||
archive = 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): | |||||
raise InputError('Software Archive %s must exist!' % archive) | |||||
if not slug: # generate one as this is mandatory | if not slug: # generate one as this is mandatory | ||||
slug = generate_slug() | slug = generate_slug() | ||||
if archive and not metadata: # we need to have the metadata | if not metadata and name and authors: | ||||
if name and authors: | |||||
metadata = generate_metadata_file(name, slug, authors) | metadata = generate_metadata_file(name, slug, authors) | ||||
cleanup_tempfile = True | 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 " | "Metadata deposit must be provided for metadata " | ||||
"deposit") | "deposit (either a filepath or --name and --author)") | ||||
if metadata and not os.path.exists(metadata): | if not archive and not metadata: | ||||
raise InputError('Software Archive metadata %s must exist!' % ( | |||||
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 = _client(url, username, password) | ||||
'url': url, | |||||
'auth': { | |||||
'username': username, | |||||
'password': password | |||||
}, | |||||
}) | |||||
if not collection: | if not collection: | ||||
# retrieve user's collection | collection = _collection(client) | ||||
sd_content = client.service_document() | |||||
if 'error' in sd_content: | |||||
raise InputError('Service document retrieval: %s' % ( | |||||
sd_content['error'], )) | |||||
collection = sd_content[ | |||||
'service']['workspace']['collection']['sword:name'] | |||||
return { | return { | ||||
'archive': archive, | 'archive': archive, | ||||
'username': username, | 'username': username, | ||||
'password': password, | 'password': password, | ||||
'metadata': metadata, | 'metadata': metadata, | ||||
'cleanup_tempfile': cleanup_tempfile, | 'cleanup_tempfile': cleanup_tempfile, | ||||
'collection': collection, | 'collection': collection, | ||||
Show All 12 Lines | except Exception: # to be clean, cleanup prior to raise | ||||
raise | 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): | |||||
logger.debug('Status deposit') | |||||
keys = ('collection', 'deposit_id') | |||||
client = config['client'] | |||||
return client.deposit_status( | |||||
**_subdict(config, keys)) | |||||
def deposit_create(config, logger): | def deposit_create(config, logger): | ||||
"""Delegate the actual deposit to the deposit client. | """Delegate the actual deposit to the deposit client. | ||||
""" | """ | ||||
logger.debug('Create deposit') | logger.debug('Create deposit') | ||||
client = config['client'] | client = config['client'] | ||||
keys = ('collection', 'archive', 'metadata', 'slug', 'in_progress') | keys = ('collection', 'archive', 'metadata', 'slug', 'in_progress') | ||||
Show All 14 Lines | return client.deposit_update( | ||||
**_subdict(config, keys)) | **_subdict(config, keys)) | ||||
@deposit.command() | @deposit.command() | ||||
@click.option('--username', required=1, | @click.option('--username', required=1, | ||||
help="(Mandatory) User's name") | help="(Mandatory) User's name") | ||||
@click.option('--password', required=1, | @click.option('--password', required=1, | ||||
help="(Mandatory) User's associated password") | help="(Mandatory) User's associated password") | ||||
@click.option('--archive', | @click.option('--archive', type=click.Path(exists=True), | ||||
help='(Optional) Software archive to deposit') | help='(Optional) Software archive to deposit') | ||||
@click.option('--metadata', | @click.option('--metadata', type=click.Path(exists=True), | ||||
help="(Optional) Path to xml metadata file. If not provided, this will use a file named <archive>.metadata.xml") # noqa | help="(Optional) Path to xml metadata file. If not provided, this will use a file named <archive>.metadata.xml") # noqa | ||||
@click.option('--archive-deposit/--no-archive-deposit', default=False, | @click.option('--archive-deposit/--no-archive-deposit', default=False, | ||||
help='(Optional) Software archive only deposit') | help='(Optional) Software archive only deposit') | ||||
@click.option('--metadata-deposit/--no-metadata-deposit', default=False, | @click.option('--metadata-deposit/--no-metadata-deposit', default=False, | ||||
help='(Optional) Metadata only deposit') | help='(Optional) Metadata only deposit') | ||||
@click.option('--collection', | @click.option('--collection', | ||||
help="(Optional) User's collection. If not provided, this will be fetched.") # noqa | help="(Optional) User's collection. If not provided, this will be fetched.") # noqa | ||||
@click.option('--slug', | @click.option('--slug', | ||||
help="""(Optional) External system information identifier. If not provided, it will be generated""") # noqa | help="""(Optional) External system information identifier. If not provided, it will be generated""") # noqa | ||||
@click.option('--partial/--no-partial', default=False, | @click.option('--partial/--no-partial', default=False, | ||||
help='(Optional) The deposit will be partial, other deposits will have to take place to finalize it.') # noqa | help='(Optional) The deposit will be partial, other deposits will have to take place to finalize it.') # noqa | ||||
@click.option('--deposit-id', default=None, | @click.option('--deposit-id', default=None, | ||||
help='(Optional) Update an existing partial deposit with its identifier') # noqa | help='(Optional) Update an existing partial deposit with its identifier') # noqa | ||||
@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, | |||||
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', | @click.option('--name', | ||||
help='Software name') | help='Software name') | ||||
@click.option('--author', multiple=True, | @click.option('--author', multiple=True, | ||||
help='Software author(s), this can be repeated as many times' | help='Software author(s), this can be repeated as many times' | ||||
' as there are authors') | ' as there are authors') | ||||
@click.pass_context | @click.pass_context | ||||
def upload(ctx, | def upload(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, | ||||
url='https://deposit.softwareheritage.org/1', | url='https://deposit.softwareheritage.org/1', | ||||
verbose=False, name=None, author=None): | 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. | ||||
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, name, author) | replace, url, 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: | 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 deposit_id: | ||||
r = deposit_status(config, logger) | |||||
elif not status and deposit_id: | |||||
r = deposit_update(config, logger) | r = deposit_update(config, logger) | ||||
elif not status and not deposit_id: | else: | ||||
r = deposit_create(config, logger) | r = deposit_create(config, logger) | ||||
logger.info(r) | logger.info(r) | ||||
Done Inline ActionsI'm wondering if print would not be better here... INFO:swh.deposit.cli.client:{'deposit-id...} whereas a bare dict outputed (with no noise) would be simpler to use for a client wanted to do something with the json-like output. I guess there is also the formatter configuration route... What do you think, could print be ok here? ardumont: I'm wondering if `print` would not be better here...
As the output is a dict, today, that gives… | |||||
finally: | finally: | ||||
_cleanup_tempfile(config) | _cleanup_tempfile(config) | ||||
@deposit.command() | |||||
@click.option('--url', default='https://deposit.softwareheritage.org/1', | |||||
help="(Optional) Deposit server api endpoint. By default, " | |||||
"https://deposit.softwareheritage.org/1") | |||||
@click.option('--username', required=1, | |||||
help="(Mandatory) User's name") | |||||
@click.option('--password', required=1, | |||||
help="(Mandatory) User's associated password") | |||||
@click.option('--deposit-id', default=None, | |||||
required=1, | |||||
help="Deposit identifier.") | |||||
@click.pass_context | |||||
def status(ctx, url, username, password, deposit_id): | |||||
"""Deposit's status | |||||
""" | |||||
logger.debug('Status deposit') | |||||
try: | |||||
client = _client(url, username, password) | |||||
collection = _collection(client) | |||||
except InputError as e: | |||||
msg = 'Problem during parsing options: %s' % e | |||||
r = { | |||||
'error': msg, | |||||
} | |||||
logger.info(r) | |||||
return 1 | |||||
r = client.deposit_status( | |||||
collection=collection, deposit_id=deposit_id) | |||||
logger.info(r) |
needs=> requires