Skip to content

Commit 1426b1a

Browse files
committed
migrate languages.json to fastapi
1 parent 3f2d9ec commit 1426b1a

File tree

7 files changed

+109
-30
lines changed

7 files changed

+109
-30
lines changed

openlibrary/asgi_app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,10 @@ def create_app() -> FastAPI:
159159
def health() -> dict[str, str]:
160160
return {"status": "ok"}
161161

162+
from openlibrary.fastapi.languages import router as languages_router # type: ignore
162163
from openlibrary.fastapi.search import router as search_router # type: ignore
163164

165+
app.include_router(languages_router)
164166
app.include_router(search_router)
165167

166168
return app

openlibrary/core/cache.py

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import functools
44
import hashlib
5+
import inspect
56
import json
67
import random
78
import string
@@ -497,21 +498,40 @@ def __init__(
497498
def __call__(self, f):
498499
"""Returns the memoized version of f."""
499500

500-
@functools.wraps(f)
501-
def func(*args, **kwargs):
502-
"""The memoized function.
503-
504-
If this is the first call with these arguments, function :attr:`f` is called and the return value is cached.
505-
Otherwise, value from the cache is returned.
506-
"""
507-
key = self.keyfunc(*args, **kwargs)
508-
value = self.cache_get(key)
509-
if value is None:
510-
value = f(*args, **kwargs)
511-
self.cache_set(key, value)
512-
return value
513-
514-
return func
501+
if inspect.iscoroutinefunction(f):
502+
# Async function - return async wrapper
503+
@functools.wraps(f)
504+
async def async_func(*args, **kwargs):
505+
"""The memoized async function.
506+
507+
If this is the first call with these arguments, function :attr:`f` is called and the return value is cached.
508+
Otherwise, value from the cache is returned.
509+
"""
510+
key = self.keyfunc(*args, **kwargs)
511+
value = self.cache_get(key)
512+
if value is None:
513+
value = await f(*args, **kwargs)
514+
self.cache_set(key, value)
515+
return value
516+
517+
return async_func
518+
else:
519+
# Sync function - return sync wrapper (existing behavior)
520+
@functools.wraps(f)
521+
def func(*args, **kwargs):
522+
"""The memoized function.
523+
524+
If this is the first call with these arguments, function :attr:`f` is called and the return value is cached.
525+
Otherwise, value from the cache is returned.
526+
"""
527+
key = self.keyfunc(*args, **kwargs)
528+
value = self.cache_get(key)
529+
if value is None:
530+
value = f(*args, **kwargs)
531+
self.cache_set(key, value)
532+
return value
533+
534+
return func
515535

516536
def cache_get(self, key: str | tuple):
517537
"""Reads value of a key from the cache.

openlibrary/fastapi/languages.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from __future__ import annotations
2+
3+
from typing import Literal
4+
5+
from fastapi import APIRouter, Query, Request
6+
from fastapi.responses import JSONResponse
7+
8+
from openlibrary.plugins.worksearch.languages import get_top_languages
9+
10+
router = APIRouter()
11+
12+
13+
@router.get("/languages.json", response_class=JSONResponse)
14+
async def list_languages(
15+
request: Request,
16+
sort: Literal["count", "name", "ebook_edition_count"] = Query(
17+
"count", description="The field to sort by."
18+
),
19+
limit: int = Query(15, gt=0, description="The number of results to return."),
20+
):
21+
"""
22+
Get a list of the top languages, sorted by the specified criteria.
23+
"""
24+
return await get_top_languages(limit=limit, user_lang=request.state.lang, sort=sort)

openlibrary/plugins/upstream/utils.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,12 @@ def strip_accents(s: str) -> str:
722722

723723
@functools.cache
724724
def get_languages(limit: int = 1000) -> dict:
725+
# TODO: fix this hack. Should be higher up like in other PR.
726+
if not hasattr(web.ctx, 'site'):
727+
from infogami.utils.delegate import create_site
728+
729+
web.ctx.site = create_site()
730+
725731
keys = web.ctx.site.things({"type": "/type/language", "limit": limit})
726732
return {
727733
lang.key: lang for lang in web.ctx.site.get_many(keys) if not lang.deprecated
@@ -1170,15 +1176,16 @@ def get_marc21_language(language: str) -> str | None:
11701176

11711177

11721178
@public
1173-
def get_language_name(lang_or_key: "Nothing | str | Thing") -> Nothing | str:
1179+
def get_language_name(
1180+
lang_or_key: "Nothing | str | Thing", user_lang: str = 'en'
1181+
) -> Nothing | str:
11741182
if isinstance(lang_or_key, str):
11751183
lang = get_language(lang_or_key)
11761184
if not lang:
11771185
return lang_or_key
11781186
else:
11791187
lang = lang_or_key
11801188

1181-
user_lang = web.ctx.lang or 'en'
11821189
return safeget(lambda: lang['name_translated'][user_lang][0]) or lang.name # type: ignore[index]
11831190

11841191

openlibrary/plugins/worksearch/code.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,11 @@ def process_facet(
135135
key, name = read_author_facet(val)
136136
yield (key, name, count)
137137
elif field == 'language':
138-
yield (val, get_language_name(f'/languages/{val}'), count)
138+
yield (
139+
val,
140+
get_language_name(f'/languages/{val}', web.ctx.lang or 'en'),
141+
count,
142+
)
139143
else:
140144
yield (val, val, count)
141145

openlibrary/plugins/worksearch/languages.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
from typing import Literal, override
77

88
import web
9+
from typing_extensions import deprecated
910

1011
from infogami.plugins.api.code import jsonapi
1112
from infogami.utils import delegate
1213
from infogami.utils.view import render_template, safeint
1314
from openlibrary.core import cache
1415
from openlibrary.plugins.upstream.utils import get_language_name
16+
from openlibrary.utils.async_utils import async_bridge
1517

1618
from . import search, subjects
1719

@@ -39,22 +41,23 @@ def process_key(self, key):
3941
return key.replace("_", " ")
4042

4143

42-
def get_top_languages(
44+
async def get_top_languages(
4345
limit: int,
46+
user_lang: str,
4447
sort: Literal["count", "name", "ebook_edition_count"] = "count",
4548
) -> list[web.storage]:
4649
available_edition_counts = dict(
47-
get_all_language_counts('edition', ebook_access="borrowable")
50+
await get_all_language_counts('edition', ebook_access="borrowable")
4851
)
4952
results = [
5053
web.storage(
51-
name=get_language_name(lang_key),
54+
name=get_language_name(lang_key, user_lang),
5255
key=lang_key,
5356
marc_code=lang_key.split('/')[-1],
5457
count=count,
5558
ebook_edition_count=available_edition_counts.get(lang_key, 0),
5659
)
57-
for (lang_key, count) in get_all_language_counts('work')
60+
for (lang_key, count) in await get_all_language_counts('work')
5861
]
5962
results.sort(
6063
key=lambda x: x[sort], reverse=sort in ("count", "ebook_edition_count")
@@ -63,7 +66,7 @@ def get_top_languages(
6366

6467

6568
@cache.memoize("memcache", key='get_all_language_counts', expires=60 * 60)
66-
def get_all_language_counts(
69+
async def get_all_language_counts(
6770
solr_type: Literal['work', 'edition'],
6871
ebook_access: str | None = None,
6972
) -> list[tuple[str, int]]:
@@ -73,7 +76,7 @@ def get_all_language_counts(
7376
if ebook_access:
7477
ebook_access_query = f' AND ebook_access:[{ebook_access} TO *]'
7578

76-
result = search.get_solr().select(
79+
result = await search.get_solr().select_async(
7780
f'type:{solr_type} {ebook_access_query}',
7881
rows=0,
7982
facets=['language'],
@@ -96,12 +99,16 @@ def GET(self):
9699
if sort not in ("count", "name", "ebook_edition_count"):
97100
raise web.badrequest("Invalid sort parameter")
98101

99-
return render_template("languages/index", get_top_languages(500, sort=sort))
102+
return render_template(
103+
"languages/index",
104+
async_bridge.run(get_top_languages(500, user_lang=web.ctx.lang, sort=sort)),
105+
)
100106

101107
def is_enabled(self):
102108
return True
103109

104110

111+
@deprecated("migrated to fastapi")
105112
class index_json(delegate.page):
106113
path = "/languages"
107114
encoding = "json"
@@ -112,7 +119,11 @@ def GET(self):
112119
limit = safeint(i.limit, 15)
113120
if i.sort not in ("count", "name", "ebook_edition_count"):
114121
raise web.badrequest("Invalid sort parameter")
115-
return json.dumps(get_top_languages(limit, sort=i.sort))
122+
return json.dumps(
123+
async_bridge.run(
124+
get_top_languages(limit, user_lang=web.ctx.lang, sort=i.sort)
125+
)
126+
)
116127

117128

118129
class language_search(delegate.page):

openlibrary/utils/solr.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Python library for accessing Solr"""
22

3+
import functools
34
import logging
45
import re
56
from collections.abc import Callable, Iterable
@@ -117,7 +118,7 @@ def update_in_place(self, request, commit: bool = False):
117118
).json()
118119
return resp
119120

120-
def select(
121+
async def select_async(
121122
self,
122123
query,
123124
fields=None,
@@ -130,7 +131,7 @@ def select(
130131
_pass_time_allowed=DEFAULT_PASS_TIME_ALLOWED,
131132
**kw,
132133
):
133-
"""Execute a solr query.
134+
"""Asynchronously execute a solr query.
134135
135136
query can be a string or a dictionary. If query is a dictionary, query
136137
is constructed by concatenating all the key-value pairs with AND condition.
@@ -163,8 +164,8 @@ def select(
163164
name = f
164165
params['facet.field'].append(name)
165166

166-
json_data = async_bridge.run(
167-
self.raw_request(
167+
json_data = (
168+
await self.raw_request(
168169
'select',
169170
urlencode(params, doseq=True),
170171
_timeout=_timeout,
@@ -176,6 +177,16 @@ def select(
176177
json_data, doc_wrapper=doc_wrapper, facet_wrapper=facet_wrapper
177178
)
178179

180+
@functools.wraps(select_async)
181+
def select(self, *args, **kwargs):
182+
"""
183+
Synchronously execute a solr query.
184+
185+
This is a wrapper around the async `select_async` method.
186+
All parameters are passed directly to the async version.
187+
"""
188+
return async_bridge.run(self.select_async(*args, **kwargs))
189+
179190
async def raw_request(
180191
self,
181192
path_or_url: str,

0 commit comments

Comments
 (0)