tooluniverse 1.0.4__py3-none-any.whl → 1.0.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of tooluniverse might be problematic. Click here for more details.

Files changed (57) hide show
  1. tooluniverse/__init__.py +56 -5
  2. tooluniverse/agentic_tool.py +90 -14
  3. tooluniverse/arxiv_tool.py +113 -0
  4. tooluniverse/biorxiv_tool.py +97 -0
  5. tooluniverse/core_tool.py +153 -0
  6. tooluniverse/crossref_tool.py +73 -0
  7. tooluniverse/data/agentic_tools.json +2 -2
  8. tooluniverse/data/arxiv_tools.json +87 -0
  9. tooluniverse/data/biorxiv_tools.json +70 -0
  10. tooluniverse/data/core_tools.json +105 -0
  11. tooluniverse/data/crossref_tools.json +70 -0
  12. tooluniverse/data/dblp_tools.json +73 -0
  13. tooluniverse/data/doaj_tools.json +94 -0
  14. tooluniverse/data/fatcat_tools.json +72 -0
  15. tooluniverse/data/hal_tools.json +70 -0
  16. tooluniverse/data/medrxiv_tools.json +70 -0
  17. tooluniverse/data/odphp_tools.json +354 -0
  18. tooluniverse/data/openaire_tools.json +85 -0
  19. tooluniverse/data/osf_preprints_tools.json +77 -0
  20. tooluniverse/data/pmc_tools.json +109 -0
  21. tooluniverse/data/pubmed_tools.json +65 -0
  22. tooluniverse/data/unpaywall_tools.json +86 -0
  23. tooluniverse/data/wikidata_sparql_tools.json +42 -0
  24. tooluniverse/data/zenodo_tools.json +82 -0
  25. tooluniverse/dblp_tool.py +62 -0
  26. tooluniverse/default_config.py +18 -0
  27. tooluniverse/doaj_tool.py +124 -0
  28. tooluniverse/execute_function.py +70 -9
  29. tooluniverse/fatcat_tool.py +66 -0
  30. tooluniverse/hal_tool.py +77 -0
  31. tooluniverse/llm_clients.py +487 -0
  32. tooluniverse/mcp_tool_registry.py +3 -3
  33. tooluniverse/medrxiv_tool.py +97 -0
  34. tooluniverse/odphp_tool.py +226 -0
  35. tooluniverse/openaire_tool.py +145 -0
  36. tooluniverse/osf_preprints_tool.py +67 -0
  37. tooluniverse/pmc_tool.py +181 -0
  38. tooluniverse/pubmed_tool.py +110 -0
  39. tooluniverse/remote/boltz/boltz_mcp_server.py +2 -2
  40. tooluniverse/remote/uspto_downloader/uspto_downloader_mcp_server.py +2 -2
  41. tooluniverse/smcp.py +313 -191
  42. tooluniverse/smcp_server.py +4 -7
  43. tooluniverse/test/test_claude_sdk.py +93 -0
  44. tooluniverse/test/test_odphp_tool.py +166 -0
  45. tooluniverse/test/test_openrouter_client.py +288 -0
  46. tooluniverse/test/test_stdio_hooks.py +1 -1
  47. tooluniverse/test/test_tool_finder.py +1 -1
  48. tooluniverse/unpaywall_tool.py +63 -0
  49. tooluniverse/wikidata_sparql_tool.py +61 -0
  50. tooluniverse/zenodo_tool.py +74 -0
  51. {tooluniverse-1.0.4.dist-info → tooluniverse-1.0.6.dist-info}/METADATA +101 -74
  52. {tooluniverse-1.0.4.dist-info → tooluniverse-1.0.6.dist-info}/RECORD +56 -19
  53. {tooluniverse-1.0.4.dist-info → tooluniverse-1.0.6.dist-info}/entry_points.txt +1 -0
  54. tooluniverse-1.0.6.dist-info/licenses/LICENSE +201 -0
  55. tooluniverse-1.0.4.dist-info/licenses/LICENSE +0 -21
  56. {tooluniverse-1.0.4.dist-info → tooluniverse-1.0.6.dist-info}/WHEEL +0 -0
  57. {tooluniverse-1.0.4.dist-info → tooluniverse-1.0.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,109 @@
1
+ [
2
+ {
3
+ "type": "PMCTool",
4
+ "name": "PMC_search_papers",
5
+ "description": "Search for full-text biomedical literature using PMC (PubMed Central) API. PMC is the free full-text archive of biomedical and life sciences journal literature at the U.S. National Institutes of Health's National Library of Medicine.",
6
+ "parameter": {
7
+ "type": "object",
8
+ "properties": {
9
+ "query": {
10
+ "type": "string",
11
+ "description": "Search query for PMC papers. Use keywords separated by spaces to refine your search."
12
+ },
13
+ "limit": {
14
+ "type": "integer",
15
+ "description": "Maximum number of papers to return. This sets the maximum number of papers retrieved from PMC.",
16
+ "default": 10,
17
+ "minimum": 1,
18
+ "maximum": 100
19
+ },
20
+ "date_from": {
21
+ "type": "string",
22
+ "description": "Start date for publication date filter (YYYY/MM/DD format). Optional parameter to limit search to papers published from this date onwards."
23
+ },
24
+ "date_to": {
25
+ "type": "string",
26
+ "description": "End date for publication date filter (YYYY/MM/DD format). Optional parameter to limit search to papers published up to this date."
27
+ },
28
+ "article_type": {
29
+ "type": "string",
30
+ "description": "Article type filter (e.g., 'research-article', 'review', 'case-report'). Optional parameter to limit search to specific article types."
31
+ }
32
+ },
33
+ "required": ["query"]
34
+ },
35
+ "return_schema": {
36
+ "type": "array",
37
+ "description": "List of full-text biomedical papers from PMC matching the search query",
38
+ "items": {
39
+ "type": "object",
40
+ "properties": {
41
+ "title": {
42
+ "type": "string",
43
+ "description": "Title of the paper"
44
+ },
45
+ "abstract": {
46
+ "type": "string",
47
+ "description": "Abstract of the paper"
48
+ },
49
+ "authors": {
50
+ "type": "array",
51
+ "items": {"type": "string"},
52
+ "description": "List of author names"
53
+ },
54
+ "year": {
55
+ "type": "string",
56
+ "description": "Publication year"
57
+ },
58
+ "pmc_id": {
59
+ "type": "string",
60
+ "description": "PMC identifier"
61
+ },
62
+ "pmid": {
63
+ "type": "string",
64
+ "description": "PubMed identifier"
65
+ },
66
+ "doi": {
67
+ "type": "string",
68
+ "description": "Digital Object Identifier (DOI) of the paper"
69
+ },
70
+ "url": {
71
+ "type": "string",
72
+ "description": "URL to access the paper on PMC"
73
+ },
74
+ "venue": {
75
+ "type": "string",
76
+ "description": "Journal or venue name"
77
+ },
78
+ "open_access": {
79
+ "type": "boolean",
80
+ "description": "Open access status (always true for PMC)"
81
+ },
82
+ "source": {
83
+ "type": "string",
84
+ "description": "Source identifier (always 'PMC')"
85
+ },
86
+ "article_type": {
87
+ "type": "string",
88
+ "description": "Type of article (e.g., research-article, review)"
89
+ },
90
+ "citations": {
91
+ "type": "integer",
92
+ "description": "Number of citations"
93
+ }
94
+ }
95
+ }
96
+ },
97
+ "test_examples": [
98
+ {
99
+ "query": "cancer research",
100
+ "limit": 2
101
+ },
102
+ {
103
+ "query": "COVID-19",
104
+ "limit": 1,
105
+ "date_from": "2020/01/01"
106
+ }
107
+ ]
108
+ }
109
+ ]
@@ -0,0 +1,65 @@
1
+ [
2
+ {
3
+ "type": "PubMedTool",
4
+ "name": "PubMed_search_articles",
5
+ "description": "Search PubMed using NCBI E-utilities (esearch + esummary) and return articles. Returns articles with title, journal, year, DOI, and PubMed URL.",
6
+ "parameter": {
7
+ "type": "object",
8
+ "properties": {
9
+ "query": {
10
+ "type": "string",
11
+ "description": "Search query for PubMed articles. Use keywords separated by spaces to refine your search."
12
+ },
13
+ "limit": {
14
+ "type": "integer",
15
+ "description": "Number of articles to return. This sets the maximum number of articles retrieved from PubMed.",
16
+ "default": 10
17
+ },
18
+ "api_key": {
19
+ "type": "string",
20
+ "description": "Optional NCBI API key for higher rate limits. Get your free key at https://www.ncbi.nlm.nih.gov/account/"
21
+ }
22
+ },
23
+ "required": ["query"]
24
+ },
25
+ "return_schema": {
26
+ "type": "array",
27
+ "description": "List of PubMed articles matching the search query",
28
+ "items": {
29
+ "type": "object",
30
+ "properties": {
31
+ "title": {
32
+ "type": "string",
33
+ "description": "Title of the article"
34
+ },
35
+ "journal": {
36
+ "type": "string",
37
+ "description": "Journal name where the article was published"
38
+ },
39
+ "year": {
40
+ "type": "integer",
41
+ "description": "Publication year"
42
+ },
43
+ "doi": {
44
+ "type": "string",
45
+ "description": "Digital Object Identifier (if available)"
46
+ },
47
+ "url": {
48
+ "type": "string",
49
+ "description": "URL to the article on PubMed"
50
+ }
51
+ }
52
+ }
53
+ },
54
+ "test_examples": [
55
+ {
56
+ "query": "cancer research",
57
+ "limit": 2
58
+ },
59
+ {
60
+ "query": "COVID-19",
61
+ "limit": 1
62
+ }
63
+ ]
64
+ }
65
+ ]
@@ -0,0 +1,86 @@
1
+ [
2
+ {
3
+ "type": "UnpaywallTool",
4
+ "name": "Unpaywall_check_oa_status",
5
+ "description": "Query Unpaywall by DOI to check open-access status and OA locations. Requires a contact email for API access.",
6
+ "parameter": {
7
+ "type": "object",
8
+ "properties": {
9
+ "doi": {
10
+ "type": "string",
11
+ "description": "DOI (Digital Object Identifier) of the article to check for open access status."
12
+ },
13
+ "email": {
14
+ "type": "string",
15
+ "description": "Contact email address required by Unpaywall API for polite usage tracking."
16
+ }
17
+ },
18
+ "required": ["doi", "email"]
19
+ },
20
+ "return_schema": {
21
+ "type": "object",
22
+ "description": "Open access status and location information for the given DOI",
23
+ "properties": {
24
+ "is_oa": {
25
+ "type": "boolean",
26
+ "description": "Whether the article is open access"
27
+ },
28
+ "oa_status": {
29
+ "type": "string",
30
+ "description": "Open access status (e.g., 'gold', 'green', 'hybrid', 'closed')"
31
+ },
32
+ "best_oa_location": {
33
+ "type": "object",
34
+ "description": "Best available open access location"
35
+ },
36
+ "oa_locations": {
37
+ "type": "array",
38
+ "description": "All available open access locations"
39
+ },
40
+ "journal_is_oa": {
41
+ "type": "boolean",
42
+ "description": "Whether the journal is open access"
43
+ },
44
+ "journal_issn_l": {
45
+ "type": "string",
46
+ "description": "Journal ISSN-L"
47
+ },
48
+ "journal_issns": {
49
+ "type": "array",
50
+ "items": {"type": "string"},
51
+ "description": "Journal ISSNs"
52
+ },
53
+ "doi": {
54
+ "type": "string",
55
+ "description": "The DOI that was queried"
56
+ },
57
+ "title": {
58
+ "type": "string",
59
+ "description": "Article title"
60
+ },
61
+ "year": {
62
+ "type": "integer",
63
+ "description": "Publication year"
64
+ },
65
+ "publisher": {
66
+ "type": "string",
67
+ "description": "Publisher name"
68
+ },
69
+ "url": {
70
+ "type": "string",
71
+ "description": "URL to the article"
72
+ }
73
+ }
74
+ },
75
+ "test_examples": [
76
+ {
77
+ "doi": "10.1038/nature12373",
78
+ "email": "test@example.com"
79
+ },
80
+ {
81
+ "doi": "10.1126/science.1234567",
82
+ "email": "user@example.com"
83
+ }
84
+ ]
85
+ }
86
+ ]
@@ -0,0 +1,42 @@
1
+ [
2
+ {
3
+ "type": "WikidataSPARQLTool",
4
+ "name": "Wikidata_SPARQL_query",
5
+ "description": "Execute SPARQL queries against Wikidata to retrieve structured data. This tool powers Scholia-style visualizations and can query academic topics, authors, institutions, and research relationships.",
6
+ "parameter": {
7
+ "type": "object",
8
+ "properties": {
9
+ "sparql": {
10
+ "type": "string",
11
+ "description": "SPARQL query string to execute against Wikidata. Use SPARQL syntax to query academic entities, relationships, and properties."
12
+ },
13
+ "max_results": {
14
+ "type": "integer",
15
+ "description": "Optional result limit override. If not specified, uses the LIMIT clause in the SPARQL query or returns all results.",
16
+ "minimum": 1
17
+ }
18
+ },
19
+ "required": ["sparql"]
20
+ },
21
+ "return_schema": {
22
+ "type": "array",
23
+ "description": "List of query results from Wikidata SPARQL endpoint",
24
+ "items": {
25
+ "type": "object",
26
+ "description": "Each result is a dictionary with variable names as keys and their values from the SPARQL query results"
27
+ }
28
+ },
29
+ "test_examples": [
30
+ {
31
+ "sparql": "SELECT ?item ?itemLabel WHERE { ?item wdt:P31 wd:Q11424 . ?item rdfs:label ?itemLabel . FILTER(LANG(?itemLabel) = 'en') } LIMIT 5",
32
+ "max_results": 5
33
+ },
34
+ {
35
+ "sparql": "SELECT ?author ?authorLabel WHERE { ?author wdt:P31 wd:Q5 . ?author wdt:P106 wd:Q188094 . ?author rdfs:label ?authorLabel . FILTER(LANG(?authorLabel) = 'en') } LIMIT 3"
36
+ },
37
+ {
38
+ "sparql": "SELECT ?university ?universityLabel WHERE { ?university wdt:P31 wd:Q3918 . ?university rdfs:label ?universityLabel . FILTER(LANG(?universityLabel) = 'en') } LIMIT 3"
39
+ }
40
+ ]
41
+ }
42
+ ]
@@ -0,0 +1,82 @@
1
+ [
2
+ {
3
+ "type": "ZenodoTool",
4
+ "name": "Zenodo_search_records",
5
+ "description": "Search Zenodo for research data, publications, and datasets. Zenodo is an open-access repository that hosts research outputs from all fields of science, including papers, datasets, software, and more.",
6
+ "parameter": {
7
+ "type": "object",
8
+ "properties": {
9
+ "query": {
10
+ "type": "string",
11
+ "description": "Free text search query for Zenodo records. Use keywords to search across titles, descriptions, authors, and other metadata."
12
+ },
13
+ "max_results": {
14
+ "type": "integer",
15
+ "description": "Maximum number of results to return. Must be between 1 and 200.",
16
+ "default": 10,
17
+ "minimum": 1,
18
+ "maximum": 200
19
+ },
20
+ "community": {
21
+ "type": "string",
22
+ "description": "Optional community slug to filter results by specific research community (e.g., 'zenodo', 'ecfunded')."
23
+ }
24
+ },
25
+ "required": ["query"]
26
+ },
27
+ "return_schema": {
28
+ "type": "array",
29
+ "description": "List of Zenodo records matching the search query",
30
+ "items": {
31
+ "type": "object",
32
+ "properties": {
33
+ "title": {
34
+ "type": "string",
35
+ "description": "Title of the record"
36
+ },
37
+ "authors": {
38
+ "type": "array",
39
+ "items": {"type": "string"},
40
+ "description": "List of author/creator names"
41
+ },
42
+ "date": {
43
+ "type": "string",
44
+ "description": "Publication date"
45
+ },
46
+ "doi": {
47
+ "type": "string",
48
+ "description": "Digital Object Identifier (DOI)"
49
+ },
50
+ "url": {
51
+ "type": "string",
52
+ "description": "URL to the record on Zenodo"
53
+ },
54
+ "files": {
55
+ "type": ["array", "string"],
56
+ "description": "List of files or link to files associated with the record"
57
+ },
58
+ "source": {
59
+ "type": "string",
60
+ "description": "Source identifier (always 'Zenodo')"
61
+ }
62
+ }
63
+ }
64
+ },
65
+ "test_examples": [
66
+ {
67
+ "query": "machine learning",
68
+ "max_results": 2
69
+ },
70
+ {
71
+ "query": "COVID-19",
72
+ "max_results": 1
73
+ },
74
+ {
75
+ "query": "climate change",
76
+ "max_results": 3,
77
+ "community": "zenodo"
78
+ }
79
+ ]
80
+ }
81
+ ]
82
+
@@ -0,0 +1,62 @@
1
+ import requests
2
+ from .base_tool import BaseTool
3
+ from .tool_registry import register_tool
4
+
5
+
6
+ @register_tool("DBLPTool")
7
+ class DBLPTool(BaseTool):
8
+ """
9
+ Search DBLP Computer Science Bibliography for publications.
10
+ """
11
+
12
+ def __init__(
13
+ self,
14
+ tool_config,
15
+ base_url="https://dblp.org/search/publ/api",
16
+ ):
17
+ super().__init__(tool_config)
18
+ self.base_url = base_url
19
+
20
+ def run(self, arguments):
21
+ query = arguments.get("query")
22
+ limit = int(arguments.get("limit", 10))
23
+ if not query:
24
+ return {"error": "`query` parameter is required."}
25
+ return self._search(query, limit)
26
+
27
+ def _search(self, query, limit):
28
+ params = {
29
+ "q": query,
30
+ "h": max(1, min(limit, 100)),
31
+ "format": "json",
32
+ }
33
+ try:
34
+ response = requests.get(self.base_url, params=params, timeout=20)
35
+ except requests.RequestException as e:
36
+ return {
37
+ "error": "Network error calling DBLP API",
38
+ "reason": str(e),
39
+ }
40
+
41
+ if response.status_code != 200:
42
+ return {
43
+ "error": f"DBLP API error {response.status_code}",
44
+ "reason": response.reason,
45
+ }
46
+
47
+ hits = response.json().get("result", {}).get("hits", {}).get("hit", [])
48
+ results = []
49
+ for hit in hits:
50
+ info = hit.get("info", {})
51
+ results.append(
52
+ {
53
+ "title": info.get("title"),
54
+ "authors": info.get("authors", {}).get("author"),
55
+ "year": info.get("year"),
56
+ "venue": info.get("venue"),
57
+ "url": info.get("url"),
58
+ "ee": info.get("ee"),
59
+ }
60
+ )
61
+
62
+ return results
@@ -33,6 +33,23 @@ default_tool_files = {
33
33
  "Enrichr": os.path.join(current_dir, "data", "enrichr_tools.json"),
34
34
  "HumanBase": os.path.join(current_dir, "data", "humanbase_tools.json"),
35
35
  "OpenAlex": os.path.join(current_dir, "data", "openalex_tools.json"),
36
+ # Literature search tools
37
+ "arxiv": os.path.join(current_dir, "data", "arxiv_tools.json"),
38
+ "crossref": os.path.join(current_dir, "data", "crossref_tools.json"),
39
+ "dblp": os.path.join(current_dir, "data", "dblp_tools.json"),
40
+ "pubmed": os.path.join(current_dir, "data", "pubmed_tools.json"),
41
+ "doaj": os.path.join(current_dir, "data", "doaj_tools.json"),
42
+ "unpaywall": os.path.join(current_dir, "data", "unpaywall_tools.json"),
43
+ "biorxiv": os.path.join(current_dir, "data", "biorxiv_tools.json"),
44
+ "medrxiv": os.path.join(current_dir, "data", "medrxiv_tools.json"),
45
+ "hal": os.path.join(current_dir, "data", "hal_tools.json"),
46
+ "core": os.path.join(current_dir, "data", "core_tools.json"),
47
+ "pmc": os.path.join(current_dir, "data", "pmc_tools.json"),
48
+ "zenodo": os.path.join(current_dir, "data", "zenodo_tools.json"),
49
+ "openaire": os.path.join(current_dir, "data", "openaire_tools.json"),
50
+ "osf_preprints": os.path.join(current_dir, "data", "osf_preprints_tools.json"),
51
+ "fatcat": os.path.join(current_dir, "data", "fatcat_tools.json"),
52
+ "wikidata_sparql": os.path.join(current_dir, "data", "wikidata_sparql_tools.json"),
36
53
  "agents": os.path.join(current_dir, "data", "agentic_tools.json"),
37
54
  "dataset": os.path.join(current_dir, "data", "dataset_tools.json"),
38
55
  # 'mcp_clients': os.path.join(current_dir, 'data', 'mcp_client_tools_example.json'),
@@ -113,6 +130,7 @@ default_tool_files = {
113
130
  "output_summarization": os.path.join(
114
131
  current_dir, "data", "output_summarization_tools.json"
115
132
  ),
133
+ "odphp": os.path.join(current_dir, "data", "odphp_tools.json"),
116
134
  }
117
135
 
118
136
 
@@ -0,0 +1,124 @@
1
+ import requests
2
+ from .base_tool import BaseTool
3
+ from .tool_registry import register_tool
4
+
5
+
6
+ @register_tool("DOAJTool")
7
+ class DOAJTool(BaseTool):
8
+ """
9
+ Search DOAJ (Directory of Open Access Journals) articles and journals.
10
+
11
+ Parameters (arguments):
12
+ query (str): Query string (Lucene syntax supported by DOAJ)
13
+ max_results (int): Max number of results (default 10, max 100)
14
+ type (str): "articles" or "journals" (default: "articles")
15
+ """
16
+
17
+ def __init__(self, tool_config):
18
+ super().__init__(tool_config)
19
+ self.base_url = "https://doaj.org/api/search"
20
+
21
+ def run(self, arguments=None):
22
+ arguments = arguments or {}
23
+ query = arguments.get("query")
24
+ search_type = arguments.get("type", "articles")
25
+ max_results = int(arguments.get("max_results", 10))
26
+
27
+ if not query:
28
+ return {"error": "`query` parameter is required."}
29
+
30
+ if search_type not in ["articles", "journals"]:
31
+ return {"error": "`type` must be 'articles' or 'journals'."}
32
+
33
+ endpoint = f"{self.base_url}/{search_type}/{query}"
34
+ params = {
35
+ "pageSize": max(1, min(max_results, 100)),
36
+ }
37
+ try:
38
+ resp = requests.get(endpoint, params=params, timeout=20)
39
+ resp.raise_for_status()
40
+ data = resp.json()
41
+ except requests.RequestException as e:
42
+ return {
43
+ "error": "Network/API error calling DOAJ",
44
+ "reason": str(e),
45
+ }
46
+ except ValueError:
47
+ return {"error": "Failed to decode DOAJ response as JSON"}
48
+
49
+ results = data.get("results", [])
50
+ items = []
51
+ if search_type == "articles":
52
+ for r in results:
53
+ b = r.get("bibjson", {})
54
+ title = b.get("title")
55
+ year = None
56
+ try:
57
+ year = int((b.get("year") or 0))
58
+ except Exception:
59
+ year = b.get("year")
60
+ authors = [
61
+ a.get("name")
62
+ for a in b.get("author", [])
63
+ if a.get("name")
64
+ ]
65
+ doi = None
66
+ for i in b.get("identifier", []):
67
+ if i.get("type") == "doi":
68
+ doi = i.get("id")
69
+ break
70
+ url = None
71
+ for link_item in b.get("link", []):
72
+ if (
73
+ link_item.get("type") == "fulltext"
74
+ or link_item.get("url")
75
+ ):
76
+ url = link_item.get("url")
77
+ break
78
+ journal = (b.get("journal") or {}).get("title")
79
+ items.append(
80
+ {
81
+ "title": title,
82
+ "authors": authors,
83
+ "year": year,
84
+ "doi": doi,
85
+ "venue": journal,
86
+ "url": url,
87
+ "source": "DOAJ",
88
+ }
89
+ )
90
+ else:
91
+ for r in results:
92
+ b = r.get("bibjson", {})
93
+ title = b.get("title")
94
+ publisher = b.get("publisher")
95
+ eissn = None
96
+ pissn = None
97
+ for i in b.get("identifier", []):
98
+ if i.get("type") == "eissn":
99
+ eissn = i.get("id")
100
+ if i.get("type") == "pissn":
101
+ pissn = i.get("id")
102
+ homepage_url = None
103
+ for link_item in b.get("link", []):
104
+ if link_item.get("url"):
105
+ homepage_url = link_item.get("url")
106
+ break
107
+ subjects = [
108
+ s.get("term")
109
+ for s in b.get("subject", [])
110
+ if s.get("term")
111
+ ]
112
+ items.append(
113
+ {
114
+ "title": title,
115
+ "publisher": publisher,
116
+ "eissn": eissn,
117
+ "pissn": pissn,
118
+ "subjects": subjects,
119
+ "url": homepage_url,
120
+ "source": "DOAJ",
121
+ }
122
+ )
123
+
124
+ return items