22
33from __future__ import annotations
44
5- import datetime
6- import json
75import logging
8- import zipfile
96from collections .abc import Iterable
10- from functools import lru_cache
11- from pathlib import Path
12- from typing import Any , Literal , NamedTuple , TypeAlias
7+ from typing import Any
138
149import bioregistry
15- import zenodo_client
16- from pydantic import BaseModel , ValidationError
10+ import ror_downloader
11+ from pydantic import ValidationError
12+ from ror_downloader import OrganizationType
1713from tqdm .auto import tqdm
1814
1915from pyobo .struct import Obo , Reference , Term
2925)
3026
3127__all__ = [
32- "OrganizationType" ,
33- "RORStatus" ,
34- "get_ror_records" ,
35- "get_ror_status" ,
28+ "RORGetter" ,
3629 "get_ror_to_country_geonames" ,
3730]
3831
3932logger = logging .getLogger (__name__ )
4033PREFIX = "ror"
41- ROR_ZENODO_RECORD_ID = "17953395"
4234
4335# Constants
4436ORG_CLASS = Reference (prefix = "OBI" , identifier = "0000245" , name = "organization" )
@@ -70,7 +62,7 @@ class RORGetter(Obo):
7062 root_terms = [CITY_CLASS , ORG_CLASS ]
7163
7264 def __post_init__ (self ):
73- self .data_version , _url , _path = get_ror_status ()
65+ self .data_version , _url , _path = ror_downloader . get_version_info ()
7466 super ().__post_init__ ()
7567
7668 def iter_terms (self , force : bool = False ) -> Iterable [Term ]:
@@ -83,18 +75,6 @@ def iter_terms(self, force: bool = False) -> Iterable[Term]:
8375 yield from iterate_ror_terms (force = force )
8476
8577
86- OrganizationType : TypeAlias = Literal [
87- "education" ,
88- "facility" ,
89- "funder" ,
90- "company" ,
91- "government" ,
92- "healthcare" ,
93- "archive" ,
94- "nonprofit" ,
95- "other" ,
96- ]
97-
9878ROR_ORGANIZATION_TYPE_TO_OBI : dict [OrganizationType , Term ] = {
9979 "education" : Term .default (PREFIX , "education" , "educational organization" ),
10080 "facility" : Term .default (PREFIX , "facility" , "facility" ),
@@ -115,127 +95,9 @@ def iter_terms(self, force: bool = False) -> Iterable[Term]:
11595_MISSED_ORG_TYPES : set [str ] = set ()
11696
11797
118- class LocationDetails (BaseModel ):
119- """The location details slot in the ROR schema."""
120-
121- continent_code : str
122- continent_name : str
123- country_code : str
124- country_name : str
125- country_subdivision_code : str | None = None
126- country_subdivision_name : str | None = None
127- lat : float
128- lng : float
129- name : str
130-
131-
132- class Location (BaseModel ):
133- """The lcoation slot in the ROR schema."""
134-
135- geonames_id : int
136- geonames_details : LocationDetails
137-
138-
139- class ExternalID (BaseModel ):
140- """The external ID slot in the ROR schema."""
141-
142- type : str
143- all : list [str ]
144- preferred : str | None = None
145-
146-
147- class Link (BaseModel ):
148- """The link slot in the ROR schema."""
149-
150- type : str
151- value : str
152-
153-
154- class Name (BaseModel ):
155- """The name slot in the ROR schema."""
156-
157- value : str
158- types : list [str ]
159- lang : str | None = None
160-
161-
162- class Relationship (BaseModel ):
163- """The relationship slot in the ROR schema."""
164-
165- type : str
166- label : str
167- id : str
168-
169-
170- class DateAnnotated (BaseModel ):
171- """The annotated date slot in the ROR schema."""
172-
173- date : datetime .date
174- schema_version : str
175-
176-
177- class Admin (BaseModel ):
178- """The admin slot in the ROR schema."""
179-
180- created : DateAnnotated
181- last_modified : DateAnnotated
182-
183-
184- Status : TypeAlias = Literal ["active" , "inactive" , "withdrawn" ]
185-
186-
187- class Record (BaseModel ):
188- """A ROR record."""
189-
190- locations : list [Location ]
191- established : int | None = None
192- external_ids : list [ExternalID ]
193- id : str
194- domains : list [str ]
195- links : list [Link ]
196- names : list [Name ]
197- relationships : list [Relationship ]
198- status : Status
199- types : list [OrganizationType ]
200- admin : Admin
201-
202- def get_preferred_label (self ) -> str | None :
203- """Get the preferred label."""
204- primary_name : str | None = None
205- for name in self .names :
206- if "ror_display" in name .types :
207- primary_name = name .value
208- if primary_name is None :
209- return None
210- primary_name = NAME_REMAPPING .get (primary_name , primary_name )
211- return primary_name
212-
213-
214- _description_prefix = {
215- "education" : "an educational organization" ,
216- "facility" : "a facility" ,
217- "funder" : "a funder" ,
218- "company" : "a company" ,
219- "government" : "a governmental organization" ,
220- "healthcare" : "a healthcare organization" ,
221- "archive" : "an archive" ,
222- "nonprofit" : "a nonprofit organization" ,
223- "other" : "an organization" ,
224- }
225-
226-
227- def _get_description (record : Record ) -> str | None :
228- description = (
229- f"{ _description_prefix [record .types [0 ]]} in { record .locations [0 ].geonames_details .name } "
230- )
231- if record .established :
232- description += f" established in { record .established } "
233- return description
234-
235-
23698def iterate_ror_terms (* , force : bool = False ) -> Iterable [Term ]:
23799 """Iterate over terms in ROR."""
238- status , records = get_ror_records (force = force )
100+ status , records = ror_downloader . get_organizations (force = force )
239101 unhandled_xref_prefixes : set [str ] = set ()
240102
241103 seen_geonames_references = set ()
@@ -249,7 +111,7 @@ def iterate_ror_terms(*, force: bool = False) -> Iterable[Term]:
249111 term = Term (
250112 reference = Reference (prefix = PREFIX , identifier = identifier , name = primary_name ),
251113 type = "Instance" ,
252- definition = _get_description ( record ),
114+ definition = record . get_description ( ),
253115 )
254116 for organization_type in record .types :
255117 if organization_type in ROR_ORGANIZATION_TYPE_TO_OBI :
@@ -339,63 +201,6 @@ def iterate_ror_terms(*, force: bool = False) -> Iterable[Term]:
339201 yield geonames_term
340202
341203
342- class RORStatus (NamedTuple ):
343- """A version information tuple."""
344-
345- version : str
346- url : str
347- path : Path
348-
349-
350- def get_ror_status (* , force : bool = False , authenticate_zenodo : bool = True ) -> RORStatus :
351- """Ensure the latest ROR record, metadata, and filepath.
352-
353- :param force: Should the record be downloaded again? This almost
354- never needs to be true, since the data doesn't change for
355- a given version
356- :param authenticate_zenodo: Should Zenodo be authenticated?
357- This isn't required, but can help avoid rate limits
358- :return: A version information tuple
359-
360- .. note::
361-
362- this goes into the ``~/.data/zenodo/6347574`` folder,
363- because 6347574 is the super-record ID, which groups all
364- versions together. this is different from the value
365- for :data:`ROR_ZENODO_RECORD_ID`
366- """
367- client = zenodo_client .Zenodo ()
368- latest_record_id = client .get_latest_record (
369- ROR_ZENODO_RECORD_ID , authenticate = authenticate_zenodo
370- )
371- response = client .get_record (latest_record_id , authenticate = authenticate_zenodo )
372- response_json = response .json ()
373- version = response_json ["metadata" ]["version" ].lstrip ("v" )
374- file_record = response_json ["files" ][0 ]
375- name = file_record ["key" ]
376- url = file_record ["links" ]["self" ]
377- path = client .download (latest_record_id , name = name , force = force )
378- return RORStatus (version = version , url = url , path = path )
379-
380-
381- @lru_cache
382- def get_ror_records (
383- * , force : bool = False , authenticate_zenodo : bool = True
384- ) -> tuple [RORStatus , list [Record ]]:
385- """Get the latest ROR metadata and records."""
386- status = get_ror_status (force = force , authenticate_zenodo = authenticate_zenodo )
387- with zipfile .ZipFile (status .path ) as zf :
388- for zip_info in zf .filelist :
389- if zip_info .filename .endswith (".json" ):
390- with zf .open (zip_info ) as file :
391- records = [
392- Record .model_validate (record )
393- for record in tqdm (json .load (file ), unit_scale = True )
394- ]
395- return status , records
396- raise FileNotFoundError
397-
398-
399204def get_ror_to_country_geonames (** kwargs : Any ) -> dict [str , str ]:
400205 """Get a mapping of ROR ids to GeoNames IDs for countries."""
401206 from pyobo .sources .geonames .geonames import get_city_to_country
0 commit comments