Skip to content

Commit 87059a3

Browse files
authored
Merge pull request #5399 from TaykYoku/v7r4_fixBaseRequestHandlerArgs
[8.0] BaseRequestHandler: use annotation, refactor + explanation docs
2 parents 5a759cf + 1e8d4a2 commit 87059a3

File tree

9 files changed

+1067
-833
lines changed

9 files changed

+1067
-833
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: 81 additions & 180 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,11 @@
22
This module contains the necessary tools to discover and load
33
the handlers for serving HTTPS
44
"""
5-
6-
from __future__ import absolute_import
7-
from __future__ import division
8-
from __future__ import print_function
9-
10-
__RCSID__ = "$Id$"
11-
12-
import inspect
13-
from tornado.web import url as TornadoURL, RequestHandler
5+
from tornado.web import RequestHandler
146

157
from DIRAC import gConfig, gLogger, S_ERROR, S_OK
168
from DIRAC.ConfigurationSystem.Client import PathFinder
179
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")]])
3810

3911

4012
class HandlerManager(object):
@@ -46,7 +18,7 @@ class HandlerManager(object):
4618
4719
Each of the Handler will have one associated route to it:
4820
49-
* Directly specified as ``LOCATION`` in the handler module
21+
* Directly specified as ``DEFAULT_LOCATION`` in the handler module
5022
* automatically deduced from the module name, of the form
5123
``System/Component`` (e.g. ``DataManagement/FileCatalog``)
5224
"""
@@ -64,59 +36,8 @@ def __init__(self, services, endpoints):
6436
If ``True``, loads all endpoints from CS
6537
:type endpoints: bool or list
6638
"""
67-
self.loader = None
6839
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()
40+
self.instances = dict(Service=services, API=endpoints)
12041

12142
def discoverHandlers(self, handlerInstance):
12243
"""
@@ -160,130 +81,91 @@ def discoverHandlers(self, handlerInstance):
16081
pass
16182
return urls
16283

163-
def loadServicesHandlers(self, services=None):
164-
"""
165-
Load a list of handler from list of service using DIRAC moduleLoader
84+
def __load(self, instances, componentType, pathFinder):
85+
"""Load a list of handler from list of given instances using DIRAC moduleLoader
16686
Use :py:class:`DIRAC.Core.Base.private.ModuleLoader`
16787
88+
:return: S_OK()/S_ERROR()
89+
"""
90+
# list of instances, e.g. ['Framework/Hello', 'Configuration/Server']
91+
if isinstance(instances, str):
92+
# make sure we have a list
93+
instances = [instances]
94+
95+
instances = self.instances[componentType] if instances is None else instances if instances else []
96+
97+
# `True` means automatically view the configuration
98+
if instances is True:
99+
instances = self.discoverHandlers(f"{componentType}s")
100+
if not instances:
101+
return S_OK()
102+
103+
# Extract ports, e.g.: ['Framework/MyService', 'Framework/MyService2:9443]
104+
port, instances = self.__extractPorts(instances)
105+
106+
loader = ModuleLoader(componentType, pathFinder, RequestHandler, moduleSuffix="Handler")
107+
108+
# Use DIRAC system to load: search in CS if path is given and if not defined
109+
# it search in place it should be (e.g. in DIRAC/FrameworkSystem/< component type >)
110+
result = loader.loadModules(instances)
111+
if result["OK"]:
112+
for module in loader.getModules().values():
113+
handler = module["classObj"]
114+
fullComponentName = module["modName"]
115+
116+
# Define the system and component name as the attributes of the handler that belongs to them
117+
handler.SYSTEM_NAME, handler.COMPONENT_NAME = fullComponentName.split("/")
118+
119+
gLogger.info(f"Found new handler {fullComponentName}")
120+
121+
# at this stage we run the basic handler initialization
122+
# see DIRAC.Core.Tornado.Server.private.BaseRequestHandler for more details
123+
# this method should return a list of routes associated with the handler, it is a regular expressions
124+
# see https://www.tornadoweb.org/en/stable/routing.html#tornado.routing.URLSpec, ``pattern`` argument.
125+
urls = handler._BaseRequestHandler__pre_initialize()
126+
127+
# First of all check if we can find route
128+
if not urls:
129+
gLogger.warn(f"URL not found for {fullComponentName}")
130+
return S_ERROR(f"URL not found for {fullComponentName}")
131+
132+
# Add new handler routes
133+
self.__handlers[fullComponentName] = dict(URLs=list(set(urls)), Port=port.get(fullComponentName))
134+
135+
return result
136+
137+
def loadServicesHandlers(self, services=None):
138+
"""Load services
139+
168140
:param services: List of service handlers to load. Default value set at initialization
169141
If ``True``, loads all services from CS
170142
:type services: bool or list
171143
172144
:return: S_OK()/S_ERROR()
173145
"""
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)
146+
return self.__load(services, "Service", PathFinder.getServiceSection)
222147

223148
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`
149+
"""Load endpoints
227150
228151
:param endpoints: List of endpoint handlers to load. Default value set at initialization
229152
If ``True``, loads all endpoints from CS
230153
:type endpoints: bool or list
231154
232155
:return: S_OK()/S_ERROR()
233156
"""
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()
157+
return self.__load(endpoints, "API", PathFinder.getAPISection)
274158

275159
def getHandlersURLs(self):
276160
"""
277161
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
279162
280163
:returns: a list of URL (not the string with "https://..." but the tornado object)
281164
see http://www.tornadoweb.org/en/stable/web.html#tornado.web.URLSpec
282165
"""
283166
urls = []
284-
for handlerData in self.__handlers.values():
285-
for url in handlerData["URLs"]:
286-
urls.append(TornadoURL(*url))
167+
for handler in self.__handlers:
168+
urls += self.__handlers[handler]["URLs"]
287169
return urls
288170

289171
def getHandlersDict(self):
@@ -295,3 +177,22 @@ def getHandlersDict(self):
295177
and port as value for 'Port' key
296178
"""
297179
return self.__handlers
180+
181+
def __extractPorts(self, serviceURIs: list) -> tuple:
182+
"""Extract ports from serviceURIs
183+
184+
:param list serviceURIs: list of uri that can contain port, .e.g:: System/Service:port
185+
186+
:return: (dict, list)
187+
"""
188+
portMapping = {}
189+
newURLs = []
190+
for _url in serviceURIs:
191+
if ":" in _url:
192+
urlTuple = _url.split(":")
193+
if urlTuple[0] not in portMapping:
194+
portMapping[urlTuple[0]] = urlTuple[1]
195+
newURLs.append(urlTuple[0])
196+
else:
197+
newURLs.append(_url)
198+
return (portMapping, newURLs)

0 commit comments

Comments
 (0)