Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions _docsrc/matching.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ It does this by splitting the input identifier string into various components an
</figure>



## Splitting the identifier string

The provided identifier string is split into several components (Figure 2) by applying the following rules:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "rslv"
version = "0.10.1"
version = "0.11.0"
description = "Provides an identifier resolver service in FastAPI."
authors = [{ name = "datadavev", email = "605409+datadavev@users.noreply.github.com" }]
requires-python = ">=3.9,<3.13"
Expand Down
24 changes: 12 additions & 12 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@
-e .
annotated-types==0.7.0
# via pydantic
anyio==4.10.0
anyio==4.11.0
# via
# httpx
# starlette
certifi==2025.8.3
certifi==2025.10.5
# via
# httpcore
# httpx
click==8.1.8 ; python_full_version < '3.10'
click==8.2.1 ; python_full_version >= '3.10'
click==8.3.0 ; python_full_version >= '3.10'
colorama==0.4.6 ; sys_platform == 'win32'
# via click
exceptiongroup==1.3.0 ; python_full_version < '3.11'
# via anyio
fastapi==0.116.1
fastapi==0.119.0
# via rslv
greenlet==3.2.4 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'
# via sqlalchemy
Expand All @@ -26,29 +26,29 @@ h11==0.16.0
httpcore==1.0.9
# via httpx
httpx==0.28.1
idna==3.10
idna==3.11
# via
# anyio
# httpx
jinja2==3.1.6
# via rslv
markupsafe==3.0.2
markupsafe==3.0.3
# via jinja2
pydantic==2.11.7
pydantic==2.12.2
# via
# fastapi
# pydantic-settings
pydantic-core==2.33.2
pydantic-core==2.41.4
# via pydantic
pydantic-settings==2.10.1
pydantic-settings==2.11.0
# via rslv
python-dotenv==1.1.1
# via pydantic-settings
sniffio==1.3.1
# via anyio
sqlalchemy==2.0.43
sqlalchemy==2.0.44
# via rslv
starlette==0.47.3
starlette==0.48.0
# via fastapi
typing-extensions==4.15.0
# via
Expand All @@ -60,7 +60,7 @@ typing-extensions==4.15.0
# sqlalchemy
# starlette
# typing-inspection
typing-inspection==0.4.1
typing-inspection==0.4.2
# via
# pydantic
# pydantic-settings
9 changes: 9 additions & 0 deletions rslv/lib_rslv/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ def split_identifier_string(pid_str: str) -> typing.Dict[str, typing.Any]:
}
_parts = pid_str.split(":", 1)
parsed["scheme"] = _parts[0].strip().lower()
# Special case for URNs
if parsed["scheme"] == "urn":
_uparts = pid_str.split(":",2)
_parts = [":".join(_uparts[:2]),]
try:
_parts.append(_uparts[2])
except IndexError:
pass
parsed["scheme"] = _parts[0].strip().lower()
try:
parsed["content"] = _parts[1].lstrip(" /:")
parsed["content"] = parsed["content"].strip() # type: ignore
Expand Down
20 changes: 19 additions & 1 deletion rslv/routers/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class CleanedIdentifierRequest:
cleaned: typing.Optional[str] = None
is_introspection: bool = False
has_service_url: bool = False
introspection_part: typing.Optional[str] = None

@classmethod
def from_request_url(
Expand All @@ -69,6 +70,7 @@ def from_request_url(
cleaned = cleaned.lstrip(" /:.;,")
has_service_url = False
is_introspection = False
introspection_part = None
if service_pattern is not None:
(cleaned, n_subs) = re.subn(
service_pattern, "", cleaned, count=1, flags=re.IGNORECASE
Expand All @@ -92,12 +94,14 @@ def from_request_url(
if requested_identifier.endswith(check):
requested_identifier = requested_identifier[: -len(check)]
is_introspection = True
introspection_part = check
break
return CleanedIdentifierRequest(
original=_original,
cleaned=requested_identifier,
is_introspection=is_introspection,
has_service_url=has_service_url,
introspection_part=introspection_part,
)


Expand Down Expand Up @@ -298,7 +302,13 @@ def get_resolve(
pid_parts, definition = pid_config.parse(cleaned_identifier.cleaned)

# If the request was for introspection (inflection) use the info handler
if cleaned_identifier.is_introspection:
# Note: introspection handler should only be called if there is an exact
# match of the incoming identifier with the configured entry. Otherwise, the
# request should be forwarded as a normal resolve redirect for the
# downstream service to handle the request.
# If there's no suffix then it's an exact match to the definition
if pid_parts["suffix"] == "" and cleaned_identifier.is_introspection:
#if cleaned_identifier.is_introspection:
return handle_get_info(
request,
cleaned_identifier,
Expand All @@ -320,6 +330,7 @@ def get_resolve(
)
pid_parts["target"] = pid_format(pid_parts, definition.target)
_target = pid_parts["target"]

# If there's no value component in the PID, then return the information
# this service has about the identifier.
# TODO: Should this be checking the content portion instead of the value? That is, if the
Expand Down Expand Up @@ -350,6 +361,13 @@ def get_resolve(
pid_parts,
definition
)
# If this was an inflection request and we reached here, then pass the inflection
# request on to the target by appending the original inflection parameter to the
# generated target URL.
if cleaned_identifier.is_introspection:
_target = f"{_target}{cleaned_identifier.introspection_part}"
pid_parts["target"] = _target

# OK, past all the edge cases, redirect the client to the registered target.
pid_parts["canonical"] = pid_format(pid_parts, definition.canonical)
pid_parts["status_code"] = response_status_code
Expand Down
8 changes: 4 additions & 4 deletions tests/test_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ def test_info_schemes(test, expected):
(["ark:99999/foo", "DELETE"], {"target":"https://example.99999.com/info/99999/foo", "status":307}),
(["bark:99999/hhdd", "GET"], {"target":"https://example.99999.com/info/99999/hhdd", "status": 302}),
(["ark:99999/fkhhdd", "GET"], {"target": "http://fk.example.com/ark:99999/fkhhdd", "status": 302}),
(["ark:99999/fkhhdd?info", "GET"], {"target": "http://fk.example.com/ark:99999/fkhhdd", "status": 200, "tag": 4}),
(["ark:99999/fkhhdd?info", "GET"], {"target": "http://fk.example.com/ark:99999/fkhhdd?info", "status": 302, "tag": 4}),
(["ark:99999/fkhhdd?query=param&q2=2", "GET"], {"target": "http://fk.example.com/ark:99999/fkhhdd?query=param&q2=2", "status": 302, "tag": 4}),
(["ark:99999/fkhhdd?query=param&q2=2", "POST"], {"target": "http://fk.example.com/ark:99999/fkhhdd?query=param&q2=2", "status": 307, "tag": 4}),
(["ark:99999/fkhhdd?query=param&q2=2", "PUT"], {"target": "http://fk.example.com/ark:99999/fkhhdd?query=param&q2=2", "status": 307, "tag": 4}),
Expand All @@ -184,13 +184,13 @@ def test_info_schemes(test, expected):
(["ark:99999/fk4foo", "GET"], {"target": "https://fk4.example.com/foo", "status": 302, "tag":3}),
(["purl:dc/terms/creator", "GET"], {"target": "http://purl.org/dc/terms/creator", "status": 302, "tag": 8}),
(["purl:dc/terms/creator?query=param&q2=2", "GET"], {"target": "http://purl.org/dc/terms/creator?query=param&q2=2", "status": 302, "tag": 8}),
(["purl:dc/terms/creator??", "GET"], {"target": "http://purl.org/dc/terms/creator", "status": 200, "tag": 8}),
(["purl:dc/terms/creator?info", "GET"], {"target": "http://purl.org/dc/terms/creator", "status": 200, "tag": 8}),
(["purl:dc/terms/creator??", "GET"], {"target": "http://purl.org/dc/terms/creator??", "status": 302, "tag": 8}),
(["purl:dc/terms/creator?info", "GET"], {"target": "http://purl.org/dc/terms/creator?info", "status": 302, "tag": 8}),
(["http://testserver/ark:99999/hhdd", "GET"], {"target": "https://example.99999.com/info/99999/hhdd", "status": 302}),
(["http://example.com/ark:99999/hhdd", "GET"], {"target": "https://example.99999.com/info/99999/hhdd", "status": 302}),
(["ark:12345/up", "GET"], {"target":"https://example.com/ark:12345/up", "status": 302}),
(["ark:12345/upextra?query=param&q2=2", "GET"], {"target":"https://example.com/ark:12345/upextra?query=param&q2=2", "status": 302}),
(["ark:12345/up?info", "GET"], {"target":"https://example.com/ark:12345/up", "status": 200}),
(["ark:12345/up?info", "GET"], {"target":"https://example.com/ark:12345/up?info", "status": 302}),
(["ark:/12345", "GET"], {"target": "https://example.com/ark:/12345", "status": 200}),
(["ark:12345", "GET"], {"target": "https://example.com/ark:12345", "status": 200}),
(["ark:99999/912345/foo", "GET"], {"target": "http://arks.org/ark:12345/foo", "status": 302, "tag":9}),
Expand Down
10 changes: 10 additions & 0 deletions tests/test_splitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@
"prefix": "BDSC_64349",
"value": None
}
),
(
"urn:nbn:nl:ui:12-85062",
{
"pid": "urn:nbn:nl:ui:12-85062",
"scheme": "urn:nbn",
"content": "nl:ui:12-85062",
"prefix": "nl:ui:12-85062",
"value": None
}
)
)

Expand Down
Loading