77.. moduleauthor:: Olaf Conradi <olaf@conradi.org>
88"""
99
10+ import asyncio
11+ import re
1012import string
13+ import threading
1114import urllib .parse as urllib_parse
1215
1316from pyld .jsonld import (JsonLdError , parse_link_header , LINK_HEADER_REL )
1417
1518
19+ # Background event loop (used when inside an existing async environment)
20+ _background_loop = None
21+ _background_thread = None
22+
23+
24+ def _ensure_background_loop ():
25+ """Start a persistent background event loop if not running."""
26+ global _background_loop , _background_thread
27+ if _background_loop is None :
28+ _background_loop = asyncio .new_event_loop ()
29+
30+ def run_loop (loop ):
31+ asyncio .set_event_loop (loop )
32+ loop .run_forever ()
33+
34+ _background_thread = threading .Thread (
35+ target = run_loop , args = (_background_loop ,), daemon = True )
36+ _background_thread .start ()
37+ return _background_loop
38+
39+
1640def aiohttp_document_loader (loop = None , secure = False , ** kwargs ):
1741 """
1842 Create an Asynchronous document loader using aiohttp.
1943
20- :param loop: the event loop used for processing HTTP requests .
44+ :param loop: deprecated / ignored (kept for backward compatibility) .
2145 :param secure: require all requests to use HTTPS (default: False).
2246 :param **kwargs: extra keyword args for the aiohttp request get() call.
2347
2448 :return: the RemoteDocument loader function.
2549 """
26- import asyncio
2750 import aiohttp
2851
29- if loop is None :
30- loop = asyncio .get_event_loop ()
31-
3252 async def async_loader (url , headers ):
3353 """
3454 Retrieves JSON-LD at the given URL asynchronously.
3555
3656 :param url: the URL to retrieve.
57+ :param headers: the request headers.
3758
3859 :return: the RemoteDocument.
3960 """
@@ -56,7 +77,7 @@ async def async_loader(url, headers):
5677 'the URL\' s scheme is not "https".' ,
5778 'jsonld.InvalidUrl' , {'url' : url },
5879 code = 'loading document failed' )
59- async with aiohttp .ClientSession (loop = loop ) as session :
80+ async with aiohttp .ClientSession () as session :
6081 async with session .get (url ,
6182 headers = headers ,
6283 ** kwargs ) as response :
@@ -104,16 +125,35 @@ async def async_loader(url, headers):
104125 'jsonld.LoadDocumentError' , code = 'loading document failed' ,
105126 cause = cause )
106127
107- def loader (url , options = {} ):
128+ def loader (url , options = None ):
108129 """
109- Retrieves JSON-LD at the given URL.
130+ Retrieves JSON-LD at the given URL synchronously.
131+
132+ Works safely in both synchronous and asynchronous environments.
110133
111134 :param url: the URL to retrieve.
135+ :param options: the request options.
112136
113137 :return: the RemoteDocument.
114138 """
115- return loop .run_until_complete (
116- async_loader (url ,
117- options .get ('headers' , {'Accept' : 'application/ld+json, application/json' })))
139+ if options is None :
140+ options = {}
141+ headers = options .get (
142+ 'headers' , {'Accept' : 'application/ld+json, application/json' })
143+
144+ # Detect whether we're already in an async environment
145+ try :
146+ running_loop = asyncio .get_running_loop ()
147+ except RuntimeError :
148+ running_loop = None
149+
150+ # Sync environment
151+ if not running_loop or not running_loop .is_running ():
152+ return asyncio .run (async_loader (url , headers ))
153+
154+ # Inside async environment: use background event loop
155+ loop = _ensure_background_loop ()
156+ future = asyncio .run_coroutine_threadsafe (async_loader (url , headers ), loop )
157+ return future .result ()
118158
119159 return loader
0 commit comments