Skip to content

Commit b2c8dd3

Browse files
committed
refactor: increase productivity, improve parsing of annotations, docs
fix: implement minor fixes
1 parent 947ca89 commit b2c8dd3

File tree

8 files changed

+720
-563
lines changed

8 files changed

+720
-563
lines changed

docs/source/DeveloperGuide/APIs/index.rst

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,8 @@ You need to choose the system for which you want to write an API, then create a
4141
The example described is likely to be sufficient for most writing cases. But here are some additional features, see :py:class:`~DIRAC.Core.Tornado.Server.private.BaseRequestHandler`:
4242

4343
- ``USE_AUTHZ_GRANTS`` set the list and order of steps to authorize the request. For example, set ``USE_AUTHZ_GRANTS = ["JWT"]`` to allow access to your endpoint only with a valid access token.
44-
- ``AUTH_PROPS`` set the authorization requirements. For example, ``AUTH_PROPS = ['authenticated']`` will allow access only to authenticated users.
44+
- ``DEFAULT_AUTHORIZATION`` set the authorization requirements. For example, ``DEFAULT_AUTHORIZATION = ['authenticated']`` will allow access only to authenticated users.
4545
- in addition to standard S_OK/S_ERROR you can return text, whether the dictionary for example or nothing, the result will be sent with a 200 status.
46-
- ``path_<my_method>`` will allow you to consider the path parameters as positional arguments of the target method. For example:
47-
48-
.. code-block:: python
49-
50-
# It's allow make request like a GET /user/contacts/Bob
51-
path_user = ["(contacts|IDs)", "([A-z%0-9-_]+)"]
52-
53-
def web_user(self, option:str, name:str):
54-
return Registry.getUserOption(name, option)
55-
5646
- If your API is complex enough and may include, for example, redirection or additional headers, you can use :py:class:`~DIRAC.Core.Tornado.Server.private.BaseRequestHandler.TornadoResponse`
5747
to add all these necessary things, which is thread-safe because TornadoResponse will call your actions outside the thread in which this method is executed:
5848

61.7 KB
Loading

src/DIRAC/Core/Tornado/Server/HandlerManager.py

Lines changed: 80 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,11 @@
99

1010
__RCSID__ = "$Id$"
1111

12-
import inspect
1312
from tornado.web import url as TornadoURL, RequestHandler
1413

1514
from DIRAC import gConfig, gLogger, S_ERROR, S_OK
1615
from DIRAC.ConfigurationSystem.Client import PathFinder
1716
from DIRAC.Core.Base.private.ModuleLoader import ModuleLoader
18-
from DIRAC.Core.Utilities.ObjectLoader import ObjectLoader
19-
20-
21-
def urlFinder(module):
22-
"""
23-
Tries to guess the url from module name.
24-
The URL would be of the form ``/System/Component`` (e.g. ``DataManagement/FileCatalog``)
25-
We search something which looks like ``<...>.<component>System.<...>.<service>Handler``
26-
27-
:param module: full module name (e.g. "DIRAC.something.something")
28-
29-
:returns: the deduced URL or None
30-
"""
31-
sections = module.split(".")
32-
for section in sections:
33-
# This condition is a bit long
34-
# We search something which look like <...>.<component>System.<...>.<service>Handler
35-
# If find we return /<component>/<service>
36-
if section.endswith("System") and sections[-1].endswith("Handler"):
37-
return "/".join(["", section[: -len("System")], sections[-1][: -len("Handler")]])
3817

3918

4019
class HandlerManager(object):
@@ -46,7 +25,7 @@ class HandlerManager(object):
4625
4726
Each of the Handler will have one associated route to it:
4827
49-
* Directly specified as ``LOCATION`` in the handler module
28+
* Directly specified as ``DEFAULT_LOCATION`` in the handler module
5029
* automatically deduced from the module name, of the form
5130
``System/Component`` (e.g. ``DataManagement/FileCatalog``)
5231
"""
@@ -64,59 +43,8 @@ def __init__(self, services, endpoints):
6443
If ``True``, loads all endpoints from CS
6544
:type endpoints: bool or list
6645
"""
67-
self.loader = None
6846
self.__handlers = {}
69-
self.__services = services
70-
self.__endpoints = endpoints
71-
self.__objectLoader = ObjectLoader()
72-
73-
def __addHandler(self, handlerPath, handler, urls=None, port=None):
74-
"""
75-
Function which add handler to list of known handlers
76-
77-
:param str handlerPath: module name, e.g.: `Framework/Auth`
78-
:param object handler: handler class
79-
:param list urls: request path
80-
:param int port: port
81-
82-
:return: S_OK()/S_ERROR()
83-
"""
84-
# First of all check if we can find route
85-
# If urls is not given, try to discover it
86-
if urls is None:
87-
# FIRST TRY: Url is hardcoded
88-
try:
89-
urls = handler.LOCATION
90-
# SECOND TRY: URL can be deduced from path
91-
except AttributeError:
92-
gLogger.debug("No location defined for %s try to get it from path" % handlerPath)
93-
urls = urlFinder(handlerPath)
94-
95-
if not urls:
96-
gLogger.warn("URL not found for %s" % (handlerPath))
97-
return S_ERROR("URL not found for %s" % (handlerPath))
98-
99-
for url in urls if isinstance(urls, (list, tuple)) else [urls]:
100-
# We add "/" if missing at begin, e.g. we found "Framework/Service"
101-
# URL can't be relative in Tornado
102-
if url and not url.startswith("/"):
103-
url = "/%s" % url
104-
105-
# Some new handler
106-
if handlerPath not in self.__handlers:
107-
gLogger.debug("Add new handler %s with port %s" % (handlerPath, port))
108-
self.__handlers[handlerPath] = {"URLs": [], "Port": port}
109-
110-
# Check if URL already loaded
111-
if (url, handler) in self.__handlers[handlerPath]["URLs"]:
112-
gLogger.debug("URL: %s already loaded for %s " % (url, handlerPath))
113-
continue
114-
115-
# Finally add the URL to handlers
116-
gLogger.info("Add new URL %s to %s handler" % (url, handlerPath))
117-
self.__handlers[handlerPath]["URLs"].append((url, handler))
118-
119-
return S_OK()
47+
self.instances = dict(Service=services, API=endpoints)
12048

12149
def discoverHandlers(self, handlerInstance):
12250
"""
@@ -160,130 +88,91 @@ def discoverHandlers(self, handlerInstance):
16088
pass
16189
return urls
16290

163-
def loadServicesHandlers(self, services=None):
164-
"""
165-
Load a list of handler from list of service using DIRAC moduleLoader
91+
def __load(self, instances, componentType, pathFinder):
92+
"""Load a list of handler from list of given instances using DIRAC moduleLoader
16693
Use :py:class:`DIRAC.Core.Base.private.ModuleLoader`
16794
95+
:return: S_OK()/S_ERROR()
96+
"""
97+
# list of instances, e.g. ['Framework/Hello', 'Configuration/Server']
98+
if isinstance(instances, str):
99+
# make sure we have a list
100+
instances = [instances]
101+
102+
instances = self.instances[componentType] if instances is None else instances if instances else []
103+
104+
# `True` means automatically view the configuration
105+
if instances is True:
106+
instances = self.discoverHandlers(f"{componentType}s")
107+
if not instances:
108+
return S_OK()
109+
110+
# Extract ports
111+
ports, instances = self.__extractPorts(instances)
112+
113+
loader = ModuleLoader(componentType, pathFinder, RequestHandler, moduleSuffix="Handler")
114+
115+
# Use DIRAC system to load: search in CS if path is given and if not defined
116+
# it search in place it should be (e.g. in DIRAC/FrameworkSystem/< component type >)
117+
result = loader.loadModules(instances)
118+
if result["OK"]:
119+
for module in loader.getModules().values():
120+
handler = module["classObj"]
121+
fullComponentName = module["modName"]
122+
123+
# Define the system and component name as the attributes of the handler that belongs to them
124+
handler.SYSTEM_NAME, handler.COMPONENT_NAME = fullComponentName.split("/")
125+
126+
gLogger.info(f"Found new handler {fullComponentName}")
127+
128+
# at this stage we run the basic handler initialization
129+
# see DIRAC.Core.Tornado.Server.private.BaseRequestHandler for more details
130+
# this method should return a list of routes associated with the handler, it is a regular expressions
131+
# see https://www.tornadoweb.org/en/stable/routing.html#tornado.routing.URLSpec, ``pattern`` argument.
132+
urls = handler._BaseRequestHandler__pre_initialize()
133+
134+
# First of all check if we can find route
135+
if not urls:
136+
gLogger.warn(f"URL not found for {fullComponentName}")
137+
return S_ERROR(f"URL not found for {fullComponentName}")
138+
139+
# Add new handler routes
140+
self.__handlers[fullComponentName] = dict(URLs=list(set(urls)), Port=ports.get(fullComponentName))
141+
142+
return result
143+
144+
def loadServicesHandlers(self, services=None):
145+
"""Load services
146+
168147
:param services: List of service handlers to load. Default value set at initialization
169148
If ``True``, loads all services from CS
170149
:type services: bool or list
171150
172151
:return: S_OK()/S_ERROR()
173152
"""
174-
# list of services, e.g. ['Framework/Hello', 'Configuration/Server']
175-
if isinstance(services, str):
176-
services = [services]
177-
# list of services
178-
self.__services = self.__services if services is None else services if services else []
179-
180-
if self.__services is True:
181-
self.__services = self.discoverHandlers("Services")
182-
183-
if self.__services:
184-
# Extract ports
185-
ports, self.__services = self.__extractPorts(self.__services)
186-
187-
self.loader = ModuleLoader("Service", PathFinder.getServiceSection, RequestHandler, moduleSuffix="Handler")
188-
189-
# Use DIRAC system to load: search in CS if path is given and if not defined
190-
# it search in place it should be (e.g. in DIRAC/FrameworkSystem/Service)
191-
load = self.loader.loadModules(self.__services)
192-
if not load["OK"]:
193-
return load
194-
for module in self.loader.getModules().values():
195-
url = module["loadName"]
196-
197-
# URL can be like https://domain:port/service/name or just service/name
198-
# Here we just want the service name, for tornado
199-
serviceTuple = url.replace("https://", "").split("/")[-2:]
200-
url = "%s/%s" % (serviceTuple[0], serviceTuple[1])
201-
self.__addHandler(module["loadName"], module["classObj"], url, ports.get(module["modName"]))
202-
return S_OK()
203-
204-
def __extractPorts(self, serviceURIs):
205-
"""Extract ports from serviceURIs
206-
207-
:param list serviceURIs: list of uri that can contain port, .e.g:: System/Service:port
208-
209-
:return: (dict, list)
210-
"""
211-
portMapping = {}
212-
newURLs = []
213-
for _url in serviceURIs:
214-
if ":" in _url:
215-
urlTuple = _url.split(":")
216-
if urlTuple[0] not in portMapping:
217-
portMapping[urlTuple[0]] = urlTuple[1]
218-
newURLs.append(urlTuple[0])
219-
else:
220-
newURLs.append(_url)
221-
return (portMapping, newURLs)
153+
return self.__load(services, "Service", PathFinder.getServiceSection)
222154

223155
def loadEndpointsHandlers(self, endpoints=None):
224-
"""
225-
Load a list of handler from list of endpoints using DIRAC moduleLoader
226-
Use :py:class:`DIRAC.Core.Base.private.ModuleLoader`
156+
"""Load endpoints
227157
228158
:param endpoints: List of endpoint handlers to load. Default value set at initialization
229159
If ``True``, loads all endpoints from CS
230160
:type endpoints: bool or list
231161
232162
:return: S_OK()/S_ERROR()
233163
"""
234-
# list of endpoints, e.g. ['Framework/Auth', ...]
235-
if isinstance(endpoints, str):
236-
endpoints = [endpoints]
237-
# list of endpoints. If __endpoints is ``True`` then list of endpoints will dicover from CS
238-
self.__endpoints = self.__endpoints if endpoints is None else endpoints if endpoints else []
239-
240-
if self.__endpoints is True:
241-
self.__endpoints = self.discoverHandlers("APIs")
242-
243-
if self.__endpoints:
244-
# Extract ports
245-
ports, self.__endpoints = self.__extractPorts(self.__endpoints)
246-
247-
self.loader = ModuleLoader("API", PathFinder.getAPISection, RequestHandler, moduleSuffix="Handler")
248-
249-
# Use DIRAC system to load: search in CS if path is given and if not defined
250-
# it search in place it should be (e.g. in DIRAC/FrameworkSystem/API)
251-
load = self.loader.loadModules(self.__endpoints)
252-
if not load["OK"]:
253-
return load
254-
for module in self.loader.getModules().values():
255-
handler = module["classObj"]
256-
if not handler.LOCATION:
257-
handler.LOCATION = urlFinder(module["loadName"])
258-
urls = []
259-
# Look for methods that are exported
260-
for mName, mObj in inspect.getmembers(handler):
261-
if inspect.isroutine(mObj) and mName.find(handler.METHOD_PREFIX) == 0:
262-
methodName = mName[len(handler.METHOD_PREFIX) :]
263-
args = getattr(handler, "path_%s" % methodName, [])
264-
gLogger.debug(
265-
" - Route %s/%s -> %s %s" % (handler.LOCATION, methodName, module["loadName"], mName)
266-
)
267-
url = "%s%s" % (handler.LOCATION, "" if methodName == "index" else ("/%s" % methodName))
268-
if args:
269-
url += r"[\/]?%s" % "/".join(args)
270-
urls.append(url)
271-
gLogger.debug(" * %s" % url)
272-
self.__addHandler(module["loadName"], handler, urls, ports.get(module["modName"]))
273-
return S_OK()
164+
return self.__load(endpoints, "API", PathFinder.getAPISection)
274165

275166
def getHandlersURLs(self):
276167
"""
277168
Get all handler for usage in Tornado, as a list of tornado.web.url
278-
If there is no handler found before, it try to find them
279169
280170
:returns: a list of URL (not the string with "https://..." but the tornado object)
281171
see http://www.tornadoweb.org/en/stable/web.html#tornado.web.URLSpec
282172
"""
283173
urls = []
284-
for handlerData in self.__handlers.values():
285-
for url in handlerData["URLs"]:
286-
urls.append(TornadoURL(*url))
174+
for handler in self.__handlers:
175+
urls += self.__handlers[handler]["URLs"]
287176
return urls
288177

289178
def getHandlersDict(self):
@@ -295,3 +184,22 @@ def getHandlersDict(self):
295184
and port as value for 'Port' key
296185
"""
297186
return self.__handlers
187+
188+
def __extractPorts(self, serviceURIs: list) -> tuple:
189+
"""Extract ports from serviceURIs
190+
191+
:param list serviceURIs: list of uri that can contain port, .e.g:: System/Service:port
192+
193+
:return: (dict, list)
194+
"""
195+
portMapping = {}
196+
newURLs = []
197+
for _url in serviceURIs:
198+
if ":" in _url:
199+
urlTuple = _url.split(":")
200+
if urlTuple[0] not in portMapping:
201+
portMapping[urlTuple[0]] = urlTuple[1]
202+
newURLs.append(urlTuple[0])
203+
else:
204+
newURLs.append(_url)
205+
return (portMapping, newURLs)

0 commit comments

Comments
 (0)