Changeset View
Changeset View
Standalone View
Standalone View
swh/deposit/cli/client.py
Show First 20 Lines • Show All 63 Lines • ▼ Show 20 Lines | def _url(url: str) -> str: | ||||
""" | """ | ||||
if not url.endswith("/1"): | if not url.endswith("/1"): | ||||
url = "%s/1" % url | url = "%s/1" % url | ||||
return url | return url | ||||
def generate_metadata( | def generate_metadata( | ||||
deposit_client: str, name: str, external_id: Optional[str], authors: List[str] | deposit_client: str, | ||||
name: str, | |||||
authors: List[str], | |||||
external_id: Optional[str] = None, | |||||
create_origin: Optional[str] = None, | |||||
) -> str: | ) -> str: | ||||
"""Generate sword compliant xml metadata with the minimum required metadata. | """Generate sword compliant xml metadata with the minimum required metadata. | ||||
The Atom spec, https://tools.ietf.org/html/rfc4287, says that: | The Atom spec, https://tools.ietf.org/html/rfc4287, says that: | ||||
- atom:entry elements MUST contain one or more atom:author elements | - atom:entry elements MUST contain one or more atom:author elements | ||||
- atom:entry elements MUST contain exactly one atom:title element. | - atom:entry elements MUST contain exactly one atom:title element. | ||||
- atom:entry elements MUST contain exactly one atom:updated element. | - atom:entry elements MUST contain exactly one atom:updated element. | ||||
However, we are also using CodeMeta, so we want some basic information to be | However, we are also using CodeMeta, so we want some basic information to be | ||||
mandatory. | mandatory. | ||||
Therefore, we generate the following mandatory fields: | Therefore, we generate the following mandatory fields: | ||||
- http://www.w3.org/2005/Atom#updated | - http://www.w3.org/2005/Atom#updated | ||||
- http://www.w3.org/2005/Atom#author | - http://www.w3.org/2005/Atom#author | ||||
- http://www.w3.org/2005/Atom#title | - http://www.w3.org/2005/Atom#title | ||||
- https://doi.org/10.5063/SCHEMA/CODEMETA-2.0#name (yes, in addition to | - https://doi.org/10.5063/SCHEMA/CODEMETA-2.0#name (yes, in addition to | ||||
http://www.w3.org/2005/Atom#title, even if they have somewhat the same meaning) | http://www.w3.org/2005/Atom#title, even if they have somewhat the same meaning) | ||||
- https://doi.org/10.5063/SCHEMA/CODEMETA-2.0#author | - https://doi.org/10.5063/SCHEMA/CODEMETA-2.0#author | ||||
Args: | Args: | ||||
deposit_client: Deposit client username, | deposit_client: Deposit client username, | ||||
name: Software name | name: Software name | ||||
external_id: External identifier (slug) or generated one | |||||
authors: List of author names | authors: List of author names | ||||
create_origin: Origin concerned by the deposit | |||||
Returns: | Returns: | ||||
metadata xml string | metadata xml string | ||||
""" | """ | ||||
import xmltodict | import xmltodict | ||||
# generate a metadata file with the minimum required metadata | # generate a metadata file with the minimum required metadata | ||||
document = { | document = { | ||||
"atom:entry": { | "atom:entry": { | ||||
"@xmlns:atom": "http://www.w3.org/2005/Atom", | "@xmlns:atom": "http://www.w3.org/2005/Atom", | ||||
"@xmlns:codemeta": "https://doi.org/10.5063/SCHEMA/CODEMETA-2.0", | "@xmlns:codemeta": "https://doi.org/10.5063/SCHEMA/CODEMETA-2.0", | ||||
vlorentz: ? | |||||
Done Inline Actionsindeed, fixed. ardumont: indeed, fixed. | |||||
"atom:updated": datetime.now(tz=timezone.utc), # mandatory, cf. docstring | "atom:updated": datetime.now(tz=timezone.utc), # mandatory, cf. docstring | ||||
"atom:author": deposit_client, # mandatory, cf. docstring | "atom:author": deposit_client, # mandatory, cf. docstring | ||||
"atom:title": name, # mandatory, cf. docstring | "atom:title": name, # mandatory, cf. docstring | ||||
"codemeta:name": name, # mandatory, cf. docstring | "codemeta:name": name, # mandatory, cf. docstring | ||||
"codemeta:author": [ # mandatory, cf. docstring | "codemeta:author": [ # mandatory, cf. docstring | ||||
{"codemeta:name": author_name} for author_name in authors | {"codemeta:name": author_name} for author_name in authors | ||||
], | ], | ||||
}, | }, | ||||
} | } | ||||
if external_id: | if external_id: | ||||
document["atom:entry"]["codemeta:identifier"] = external_id | document["atom:entry"]["codemeta:identifier"] = external_id | ||||
if create_origin: | |||||
document["atom:entry"][ | |||||
"@xmlns:swh" | |||||
] = "https://www.softwareheritage.org/schema/2018/deposit" | |||||
document["atom:entry"]["swh:create_origin"] = { | |||||
"swh:origin": {"@url": create_origin} | |||||
} | |||||
logging.debug("Atom entry dict to generate as xml: %s", document) | logging.debug("Atom entry dict to generate as xml: %s", document) | ||||
return xmltodict.unparse(document, pretty=True) | return xmltodict.unparse(document, pretty=True) | ||||
def _collection(client: PublicApiDepositClient) -> str: | def _collection(client: PublicApiDepositClient) -> str: | ||||
"""Retrieve the client's collection | """Retrieve the client's collection | ||||
""" | """ | ||||
Show All 9 Lines | |||||
def client_command_parse_input( | def client_command_parse_input( | ||||
client, | client, | ||||
username: str, | username: str, | ||||
archive: Optional[str], | archive: Optional[str], | ||||
metadata: Optional[str], | metadata: Optional[str], | ||||
collection: Optional[str], | collection: Optional[str], | ||||
slug: Optional[str], | slug: Optional[str], | ||||
create_origin: Optional[str], | |||||
partial: bool, | partial: bool, | ||||
deposit_id: Optional[int], | deposit_id: Optional[int], | ||||
swhid: Optional[str], | swhid: Optional[str], | ||||
replace: bool, | replace: bool, | ||||
url: str, | url: str, | ||||
name: Optional[str], | name: Optional[str], | ||||
authors: List[str], | authors: List[str], | ||||
temp_dir: str, | temp_dir: str, | ||||
Show All 27 Lines | ) -> Dict[str, Any]: | ||||
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 | ||||
"metadata": the metadata file to deposit | "metadata": the metadata file to deposit | ||||
"collection": the user's collection under which to put the deposit | "collection": the user's collection under which to put the deposit | ||||
"slug": the slug or external id identifying the deposit to make | "create_origin": the origin concerned by the deposit | ||||
"in_progress": if the deposit is partial or not | "in_progress": if the deposit is partial or not | ||||
"url": deposit's server main entry point | "url": deposit's server main entry point | ||||
"deposit_id": optional deposit identifier | "deposit_id": optional deposit identifier | ||||
"swhid": optional deposit swhid | "swhid": optional deposit swhid | ||||
"replace": whether the given deposit is to be replaced or not | "replace": whether the given deposit is to be replaced or not | ||||
""" | """ | ||||
if not metadata: | if not metadata: | ||||
if name and authors: | if name and authors: | ||||
metadata_path = os.path.join(temp_dir, "metadata.xml") | metadata_path = os.path.join(temp_dir, "metadata.xml") | ||||
logging.debug("Temporary file: %s", metadata_path) | logging.debug("Temporary file: %s", metadata_path) | ||||
metadata_xml = generate_metadata(username, name, slug, authors) | metadata_xml = generate_metadata( | ||||
username, name, authors, external_id=slug, create_origin=create_origin | |||||
) | |||||
logging.debug("Metadata xml generated: %s", metadata_xml) | logging.debug("Metadata xml generated: %s", metadata_xml) | ||||
with open(metadata_path, "w") as f: | with open(metadata_path, "w") as f: | ||||
f.write(metadata_xml) | f.write(metadata_xml) | ||||
metadata = metadata_path | metadata = metadata_path | ||||
elif archive is not None and not partial and not deposit_id: | elif archive is not None and not partial and not deposit_id: | ||||
# If we meet all the following conditions: | # If we meet all the following conditions: | ||||
# * this is not an archive-only deposit request | # * this is not an archive-only deposit request | ||||
# * it is not part of a multipart deposit (either create/update | # * it is not part of a multipart deposit (either create/update | ||||
Show All 9 Lines | if not metadata: | ||||
raise InputError( | raise InputError( | ||||
"For metadata deposit request, either a metadata file with " | "For metadata deposit request, either a metadata file with " | ||||
"--metadata or both --author and --name must be provided." | "--metadata or both --author and --name must be provided." | ||||
) | ) | ||||
else: | else: | ||||
# TODO: this is a multipart deposit, we might want to check that | # TODO: this is a multipart deposit, we might want to check that | ||||
# metadata are deposited at some point | # metadata are deposited at some point | ||||
pass | pass | ||||
elif name or authors: | elif name or authors or create_origin: | ||||
raise InputError( | raise InputError( | ||||
"Using --metadata flag is incompatible with both " | "Using --metadata flag is incompatible with " | ||||
"--author and --name (Those are used to generate one metadata file)." | "--author and --name and --create-origin (those are used to generate one " | ||||
"metadata file)." | |||||
) | ) | ||||
if not archive and not metadata: | if not archive and not metadata: | ||||
raise InputError( | raise InputError( | ||||
"Please provide an actionable command. See --help for more information" | "Please provide an actionable command. See --help for more information" | ||||
) | ) | ||||
if replace and not deposit_id: | if replace and not deposit_id: | ||||
▲ Show 20 Lines • Show All 81 Lines • ▼ Show 20 Lines | |||||
) | ) | ||||
@click.option( | @click.option( | ||||
"--collection", | "--collection", | ||||
help="(Optional) User's collection. If not provided, this will be fetched.", | help="(Optional) User's collection. If not provided, this will be fetched.", | ||||
) | ) | ||||
@click.option( | @click.option( | ||||
"--slug", | "--slug", | ||||
help=( | help=( | ||||
"(Optional) External system information identifier. " | "(Deprecated) (Optional) External system information identifier. " | ||||
"If not provided, it will be generated" | "If not provided, it will be generated" | ||||
), | ), | ||||
) | ) | ||||
@click.option( | @click.option( | ||||
"--create-origin", | |||||
help=( | |||||
"(Optional) Origin url to attach information to. To be used alongside " | |||||
"--name and --author. This will be generated alongside the metadata to " | |||||
"provide to the deposit server." | |||||
), | |||||
) | |||||
@click.option( | |||||
"--partial/--no-partial", | "--partial/--no-partial", | ||||
default=False, | default=False, | ||||
help=( | help=( | ||||
"(Optional) The deposit will be partial, other deposits " | "(Optional) The deposit will be partial, other deposits " | ||||
"will have to take place to finalize it." | "will have to take place to finalize it." | ||||
), | ), | ||||
) | ) | ||||
@click.option( | @click.option( | ||||
Show All 26 Lines | def upload( | ||||
username: str, | username: str, | ||||
password: str, | password: str, | ||||
archive: Optional[str], | archive: Optional[str], | ||||
metadata: Optional[str], | metadata: Optional[str], | ||||
archive_deposit: bool, | archive_deposit: bool, | ||||
metadata_deposit: bool, | metadata_deposit: bool, | ||||
collection: Optional[str], | collection: Optional[str], | ||||
slug: Optional[str], | slug: Optional[str], | ||||
create_origin: Optional[str], | |||||
partial: bool, | partial: bool, | ||||
deposit_id: Optional[int], | deposit_id: Optional[int], | ||||
swhid: Optional[str], | swhid: Optional[str], | ||||
replace: bool, | replace: bool, | ||||
url: str, | url: str, | ||||
verbose: bool, | verbose: bool, | ||||
name: Optional[str], | name: Optional[str], | ||||
author: List[str], | author: List[str], | ||||
Show All 15 Lines | if archive_deposit or metadata_deposit: | ||||
warnings.warn( | warnings.warn( | ||||
'"archive_deposit" and "metadata_deposit" option arguments are ' | '"archive_deposit" and "metadata_deposit" option arguments are ' | ||||
"deprecated and have no effect; simply do not provide the archive " | "deprecated and have no effect; simply do not provide the archive " | ||||
"for a metadata-only deposit, and do not provide a metadata for a" | "for a metadata-only deposit, and do not provide a metadata for a" | ||||
"archive-only deposit.", | "archive-only deposit.", | ||||
DeprecationWarning, | DeprecationWarning, | ||||
) | ) | ||||
if slug: | |||||
if create_origin and slug != create_origin: | |||||
raise InputError( | |||||
'"--slug" flag has been deprecated in favor of "--create-origin" flag. ' | |||||
"You mentioned both with different values, please only " | |||||
'use "--create-origin".' | |||||
Not Done Inline Actionsthis one should be an error vlorentz: this one should be an error | |||||
) | |||||
warnings.warn( | |||||
'"--slug" flag has been deprecated in favor of "--create-origin" flag. ' | |||||
'Please, start using "--create-origin" instead of "--slug"', | |||||
DeprecationWarning, | |||||
) | |||||
url = _url(url) | url = _url(url) | ||||
client = PublicApiDepositClient(url=url, auth=(username, password)) | client = PublicApiDepositClient(url=url, auth=(username, password)) | ||||
with tempfile.TemporaryDirectory() as temp_dir: | with tempfile.TemporaryDirectory() as temp_dir: | ||||
with trap_and_report_exceptions(): | with trap_and_report_exceptions(): | ||||
logger.debug("Parsing cli options") | logger.debug("Parsing cli options") | ||||
config = client_command_parse_input( | config = client_command_parse_input( | ||||
client, | client, | ||||
username, | username, | ||||
archive, | archive, | ||||
metadata, | metadata, | ||||
collection, | collection, | ||||
slug, | slug, | ||||
create_origin, | |||||
partial, | partial, | ||||
deposit_id, | deposit_id, | ||||
swhid, | swhid, | ||||
replace, | replace, | ||||
url, | url, | ||||
name, | name, | ||||
author, | author, | ||||
temp_dir, | temp_dir, | ||||
) | ) | ||||
if verbose: | if verbose: | ||||
logger.info("Parsed configuration: %s", config) | logger.info("Parsed configuration: %s", config) | ||||
keys = ["archive", "collection", "in_progress", "metadata", "slug"] | keys = [ | ||||
"archive", | |||||
"collection", | |||||
"in_progress", | |||||
"metadata", | |||||
"slug", | |||||
] | |||||
if config["deposit_id"]: | if config["deposit_id"]: | ||||
keys += ["deposit_id", "replace", "swhid"] | keys += ["deposit_id", "replace", "swhid"] | ||||
data = client.deposit_update(**_subdict(config, keys)) | data = client.deposit_update(**_subdict(config, keys)) | ||||
else: | else: | ||||
data = client.deposit_create(**_subdict(config, keys)) | data = client.deposit_create(**_subdict(config, keys)) | ||||
print_result(data, output_format) | print_result(data, output_format) | ||||
▲ Show 20 Lines • Show All 72 Lines • Show Last 20 Lines |
?