Skip to content

Commit 8cd4d5e

Browse files
committed
Create get_packaged_python_script.py
1 parent 0f1a4b5 commit 8cd4d5e

1 file changed

Lines changed: 118 additions & 0 deletions

File tree

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""Extract metadata from Python script."""
2+
3+
import re
4+
from typing import Any
5+
6+
from fair_mappings_schema.datamodel.fair_mappings_schema_pydantic import (
7+
MappingSpecification,
8+
Person,
9+
MappingSpecificationTypeEnum,
10+
)
11+
import click
12+
import requests
13+
from pystow.utils import model_dump_yaml
14+
import tomllib
15+
16+
__all__ = ["get_python_script"]
17+
18+
19+
def get_python_script(script_url: str) -> MappingSpecification:
20+
"""Get a mapping specification from a package."""
21+
owner, repo = _get_repository(script_url)
22+
data = _get_pyproject_toml(owner, repo)
23+
project = data["project"]
24+
urls = data.get("urls", {})
25+
documentation = urls.get("documentation") or urls.get("Documentation")
26+
xxx = dict(
27+
content_url=script_url,
28+
# TODO add explicit way of saying it's code
29+
type=MappingSpecificationTypeEnum.other,
30+
mapping_method="Python script",
31+
name=project["name"],
32+
version=project["version"],
33+
description=project.get("description"),
34+
license=project.get("license"),
35+
author=_get_person(project, "authors"),
36+
documentation=documentation,
37+
)
38+
xxx.update(_get_script_data(script_url))
39+
return MappingSpecification.model_validate(xxx)
40+
41+
42+
def _get_script_data(script_url: str) -> dict[str, Any]:
43+
"""Get a mapping specification from a package."""
44+
res = requests.get(script_url, timeout=5)
45+
res.raise_for_status()
46+
dd = extract_script_toml(res.text)
47+
return dd.get("tool", {}).get("fair-mappings")
48+
49+
50+
def _get_person(project: dict[str, Any], key: str) -> Person | None:
51+
people = project.get(key)
52+
if not people:
53+
return None
54+
person = people[0]
55+
name = person["name"]
56+
# TODO add email to person model
57+
# TODO see if we can recognize ORCiD here more officially
58+
orcid = person.get("orcid")
59+
if orcid:
60+
orcid = orcid.removeprefix("https://orcid.com")
61+
orcid = orcid.removeprefix("http://orcid.com")
62+
return Person(name=name, orcid=orcid)
63+
64+
65+
def _get_pyproject_toml(owner: str, repo: str, branch: str = "main"):
66+
url = f"https://github.com/{owner}/{repo}/raw/refs/heads/{branch}/pyproject.toml"
67+
res = requests.get(url, timeout=5)
68+
data = tomllib.loads(res.text)
69+
return data
70+
71+
72+
def _get_repository(url: str) -> tuple[str, str]:
73+
"""Get a mapping specification from a package."""
74+
if url.startswith("https://github.com/"):
75+
url = url.removeprefix("https://github.com/")
76+
if url.startswith("http://github.com/"):
77+
url = url.removeprefix("http://github.com/")
78+
79+
parts = url.split("/")
80+
if len(parts) == 1:
81+
raise ValueError(f"no repository given, just an owner: {url}")
82+
owner, repo, *_ = parts
83+
return owner, repo
84+
85+
86+
def extract_script_toml(source: str) -> dict[str, Any] | None:
87+
"""
88+
Extract the raw TOML string from a `# /// script` ... `# ///` block.
89+
Returns the TOML text, or None if no such block is found.
90+
"""
91+
pattern = re.compile(
92+
r"^#\s/// script\s*\n" # opening marker
93+
r"((?:#[^\n]*\n)*?)" # captured comment lines
94+
r"#\s///\s*$", # closing marker
95+
re.MULTILINE,
96+
)
97+
match = pattern.search(source)
98+
if not match:
99+
return None
100+
101+
# Strip the leading `# ` (or `#`) from every line
102+
toml_lines = []
103+
for line in match.group(1).splitlines():
104+
# Remove exactly one leading `# ` or `#`
105+
toml_lines.append(re.sub(r"^#( ?)", "", line))
106+
107+
return tomllib.loads("\n".join(toml_lines))
108+
109+
110+
def _main():
111+
# TODO make function that fixes URL to be raw
112+
better_url = "https://github.com/data-literacy-alliance/oerbservatory/raw/refs/heads/main/src/oerbservatory/sources/dalia.py"
113+
model = get_python_script(better_url)
114+
click.echo(model_dump_yaml(model, exclude_none=True, exclude={"author.type"}))
115+
116+
117+
if __name__ == "__main__":
118+
_main()

0 commit comments

Comments
 (0)