Changeset View
Changeset View
Standalone View
Standalone View
swh/deposit/cli/client.py
# Copyright (C) 2017-2020 The Software Heritage developers | # Copyright (C) 2017-2020 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 | ||||
from __future__ import annotations | from __future__ import annotations | ||||
from datetime import datetime, timezone | |||||
import logging | import logging | ||||
# WARNING: do not import unnecessary things here to keep cli startup time under | # WARNING: do not import unnecessary things here to keep cli startup time under | ||||
# control | # control | ||||
import os | import os | ||||
import sys | import sys | ||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple | from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple | ||||
Show All 36 Lines | Returns: | ||||
Top level api url to actually request | Top level api url to actually request | ||||
""" | """ | ||||
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_file( | def generate_metadata( | ||||
name: str, external_id: str, authors: List[str], temp_dir: str | deposit_client: str, name: str, external_id: str, authors: List[str] | ||||
) -> str: | ) -> str: | ||||
"""Generate a temporary metadata file with the minimum required metadata | """Generate sword compliant xml metadata with the minimum required metadata. | ||||
This generates a xml file in a temporary location and returns the | The Atom spec, https://tools.ietf.org/html/rfc4287, says that: | ||||
path to that file. | |||||
This is up to the client of that function to clean up the | - atom:entry elements MUST contain one or more atom:author elements | ||||
temporary file. | - atom:entry elements MUST contain exactly one atom:title 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 | |||||
mandatory. | |||||
Therefore, we generate the following mandatory fields: | |||||
- http://www.w3.org/2005/Atom#updated | |||||
- http://www.w3.org/2005/Atom#author | |||||
- http://www.w3.org/2005/Atom#title | |||||
- 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) | |||||
- https://doi.org/10.5063/SCHEMA/CODEMETA-2.0#author | |||||
Args: | Args: | ||||
name: Software's name | deposit_client: Deposit client username, | ||||
name: Software name | |||||
external_id: External identifier (slug) or generated one | external_id: External identifier (slug) or generated one | ||||
authors: List of author names | authors: List of author names | ||||
Returns: | Returns: | ||||
Filepath to the metadata generated file | metadata xml string | ||||
""" | """ | ||||
import xmltodict | import xmltodict | ||||
path = os.path.join(temp_dir, "metadata.xml") | |||||
# generate a metadata file with the minimum required metadata | # generate a metadata file with the minimum required metadata | ||||
codemetadata = { | codemetadata = { | ||||
"entry": { | "entry": { | ||||
"@xmlns": "http://www.w3.org/2005/Atom", | "@xmlns:atom": "http://www.w3.org/2005/Atom", | ||||
vlorentz: yay, I like it when we avoid default namespaces | |||||
Done Inline Actionsyes, i don't like it either, I just did not understand how to properly use those before. ardumont: yes, i don't like it either, I just did not understand how to properly use those before. | |||||
"@xmlns:codemeta": "https://doi.org/10.5063/SCHEMA/CODEMETA-2.0", | "@xmlns:codemeta": "https://doi.org/10.5063/SCHEMA/CODEMETA-2.0", | ||||
"codemeta:name": name, | |||||
"codemeta:identifier": external_id, | "codemeta:identifier": external_id, | ||||
"codemeta:author": [ | "atom:updated": datetime.now(tz=timezone.utc), # mandatory, cf. docstring | ||||
"atom:author": deposit_client, # mandatory, cf. docstring | |||||
"atom:title": name, # mandatory, cf. docstring | |||||
"codemeta:name": name, # 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 | ||||
], | ], | ||||
}, | }, | ||||
} | } | ||||
logging.debug("Temporary file: %s", path) | |||||
logging.debug("Metadata dict to generate as xml: %s", codemetadata) | logging.debug("Metadata dict to generate as xml: %s", codemetadata) | ||||
Not Done Inline Actionscould you add a comment explaining which fields are mandatory (and why the duplicates, etc.) You can probably just copy-paste my comment on T2701 vlorentz: could you add a comment explaining which fields are mandatory (and why the duplicates, etc.)… | |||||
Done Inline Actionsok ardumont: ok | |||||
s = xmltodict.unparse(codemetadata, pretty=True) | return xmltodict.unparse(codemetadata, pretty=True) | ||||
logging.debug("Metadata dict as xml generated: %s", s) | |||||
with open(path, "w") as fp: | |||||
fp.write(s) | |||||
return path | |||||
def _client(url: str, username: str, password: str) -> PublicApiDepositClient: | def _client(url: str, username: str, password: str) -> PublicApiDepositClient: | ||||
"""Instantiate a client to access the deposit api server | """Instantiate a client to access the deposit api server | ||||
Args: | Args: | ||||
url (str): Deposit api server | url (str): Deposit api server | ||||
username (str): User | username (str): User | ||||
▲ Show 20 Lines • Show All 91 Lines • ▼ Show 20 Lines | ) -> Dict[str, Any]: | ||||
if not metadata: | if not metadata: | ||||
if metadata_deposit: | if metadata_deposit: | ||||
raise InputError( | raise InputError( | ||||
"Metadata deposit must be provided for metadata " | "Metadata deposit must be provided for metadata " | ||||
"deposit, either a filepath with --metadata or --name and --author" | "deposit, either a filepath with --metadata or --name and --author" | ||||
) | ) | ||||
if name and authors: | if name and authors: | ||||
metadata = generate_metadata_file(name, slug, authors, temp_dir) | metadata_path = os.path.join(temp_dir, "metadata.xml") | ||||
logging.debug("Temporary file: %s", metadata_path) | |||||
metadata_xml = generate_metadata(username, name, slug, authors) | |||||
logging.debug("Metadata xml generated: %s", metadata_xml) | |||||
with open(metadata_path, "w") as f: | |||||
f.write(metadata_xml) | |||||
metadata = metadata_path | |||||
elif not archive_deposit and not partial and not deposit_id: | elif not archive_deposit 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 | ||||
# or finish) | # or finish) | ||||
# * it misses either name or authors | # * it misses either name or authors | ||||
raise InputError( | raise InputError( | ||||
"For metadata deposit request, either a metadata file with " | "For metadata deposit request, either a metadata file with " | ||||
▲ Show 20 Lines • Show All 312 Lines • Show Last 20 Lines |
yay, I like it when we avoid default namespaces