Skip to content

Commit 6629a12

Browse files
committed
Added utility function and supporting functions to generate URL path from route manifest given the function name and path parameters
1 parent ad5d0a2 commit 6629a12

File tree

1 file changed

+120
-0
lines changed

1 file changed

+120
-0
lines changed

src/murfey/util/api.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"""
2+
Utility functions to help with URL path lookups using function names for our FastAPI
3+
servers. This makes reference to the route_manifest.yaml file that is also saved in
4+
this directory. This routes_manifest.yaml file should be regenerated whenver changes
5+
are made to the API endpoints. This can be done using the 'generate_route_manifest'
6+
CLI function.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import re
12+
from functools import lru_cache
13+
from pathlib import Path
14+
from typing import Any
15+
16+
import yaml
17+
18+
import murfey.util
19+
20+
route_manifest_file = Path(murfey.util.__path__[0]) / "route_manifest.yaml"
21+
22+
23+
@lru_cache(maxsize=1) # Load the manifest once and reuse
24+
def load_route_manifest(
25+
file: Path = route_manifest_file,
26+
):
27+
with open(file, "r") as f:
28+
return yaml.safe_load(f)
29+
30+
31+
def find_unique_index(
32+
pattern: str,
33+
candidates: list[str],
34+
) -> int:
35+
"""
36+
Finds the index of a unique entry in a list
37+
"""
38+
counter = 0
39+
matches = []
40+
index = 0
41+
for i, candidate in enumerate(candidates):
42+
if pattern in candidate:
43+
counter += 1
44+
matches.append(candidate)
45+
index = i
46+
if counter == 0:
47+
raise KeyError(f"No match found for {pattern!r}")
48+
if counter > 1:
49+
raise KeyError(f"Ambiguous match for {pattern!r}: {matches}")
50+
return index
51+
52+
53+
def render_path(path_template: str, kwargs: dict) -> str:
54+
"""
55+
Replace all FastAPI-style {param[:converter]} path parameters
56+
with corresponding values from kwargs.
57+
"""
58+
59+
pattern = re.compile(r"{([^}]+)}") # Look for all path params
60+
61+
def replace(match):
62+
raw_str = match.group(1)
63+
param_name = raw_str.split(":")[0] # Ignore :converter in the field
64+
if param_name not in kwargs:
65+
raise KeyError(f"Missing path parameter: {param_name}")
66+
return str(kwargs[param_name])
67+
68+
return pattern.sub(replace, path_template)
69+
70+
71+
def url_path_for(
72+
router_name: str, # With logic for partial matches
73+
function_name: str, # With logic for partial matches
74+
**kwargs, # Takes any path param and matches it against curly bracket contents
75+
):
76+
# Use 'Any' first and slowly reveal types as it is unpacked
77+
route_manifest: dict[str, list[Any]] = load_route_manifest()
78+
79+
# Load the routes in the desired router
80+
routers = list(route_manifest.keys())
81+
routes: list[dict[str, Any]] = route_manifest[
82+
routers[find_unique_index(router_name, routers)]
83+
]
84+
85+
# Search router for the function
86+
route_info = routes[
87+
find_unique_index(function_name, [r["function"] for r in routes])
88+
]
89+
90+
# Unpack the dictionary
91+
route_path: str = route_info["path"]
92+
path_params: list[dict[str, str]] = route_info["path_params"]
93+
94+
# Validate the kwargs provided
95+
for param, value in kwargs.items():
96+
# Check if the name is not a match
97+
if param not in [p["name"] for p in path_params] and path_params:
98+
raise KeyError(f"Unknown path parameter provided: {param}")
99+
for path_param in path_params:
100+
if (
101+
path_param["name"] == param
102+
and type(value).__name__ != path_param["type"]
103+
):
104+
raise TypeError(
105+
f"'{param}' must be {path_param['type']!r}; "
106+
f"received {type(value).__name__!r}"
107+
)
108+
109+
# Render and return the path
110+
return render_path(route_path, kwargs)
111+
112+
113+
if __name__ == "__main__":
114+
# Run test on some existing routes
115+
url_path = url_path_for(
116+
"api.router",
117+
"register_processing_parameters",
118+
session_id=3,
119+
)
120+
print(url_path)

0 commit comments

Comments
 (0)