10
10
# See the License for the specific language governing permissions and
11
11
# limitations under the License.
12
12
13
- import collections .abc
14
13
import datetime
15
14
import functools
16
15
import xmlrpc .client
17
16
import xmlrpc .server
18
17
18
+ from typing import List , Mapping , Union
19
+
19
20
import elasticsearch
21
+ import typeguard
20
22
21
23
from elasticsearch_dsl import Q
22
24
from packaging .utils import canonicalize_name
23
25
from pyramid .view import view_config
26
+ from pyramid_rpc .mapper import MapplyViewMapper
24
27
from pyramid_rpc .xmlrpc import (
25
28
XmlRpcError ,
29
+ XmlRpcInvalidMethodParams ,
26
30
exception_view as _exception_view ,
27
31
xmlrpc_method as _xmlrpc_method ,
28
32
)
@@ -72,6 +76,7 @@ def xmlrpc_method(**kwargs):
72
76
require_csrf = False ,
73
77
require_methods = ["POST" ],
74
78
decorator = (submit_xmlrpc_metrics (method = kwargs ["method" ]),),
79
+ mapper = TypedMapplyViewMapper ,
75
80
)
76
81
77
82
def decorator (f ):
@@ -106,6 +111,15 @@ class XMLRPCServiceUnavailable(XmlRpcError):
106
111
faultString = "server error; service unavailable"
107
112
108
113
114
+ class XMLRPCInvalidParamTypes (XmlRpcInvalidMethodParams ):
115
+ def __init__ (self , exc ):
116
+ self .exc = exc
117
+
118
+ @property
119
+ def faultString (self ): # noqa
120
+ return f"client error; { self .exc } "
121
+
122
+
109
123
class XMLRPCWrappedError (xmlrpc .server .Fault ):
110
124
def __init__ (self , exc ):
111
125
self .faultCode = - 32500
@@ -116,18 +130,25 @@ def faultString(self): # noqa
116
130
return "{exc.__class__.__name__}: {exc}" .format (exc = self .wrapped_exception )
117
131
118
132
133
+ class TypedMapplyViewMapper (MapplyViewMapper ):
134
+ def mapply (self , fn , args , kwargs ):
135
+ try :
136
+ memo = typeguard ._CallMemo (fn , args = args , kwargs = kwargs )
137
+ typeguard .check_argument_types (memo )
138
+ except TypeError as exc :
139
+ print (exc )
140
+ raise XMLRPCInvalidParamTypes (exc )
141
+
142
+ return super ().mapply (fn , args , kwargs )
143
+
144
+
119
145
@view_config (route_name = "pypi" , context = Exception , renderer = "xmlrpc" )
120
146
def exception_view (exc , request ):
121
147
return _exception_view (exc , request )
122
148
123
149
124
150
@xmlrpc_method (method = "search" )
125
- def search (request , spec , operator = "and" ):
126
- if not isinstance (spec , collections .abc .Mapping ):
127
- raise XMLRPCWrappedError (
128
- TypeError ("Invalid spec, must be a mapping/dictionary." )
129
- )
130
-
151
+ def search (request , spec : Mapping [str , Union [str , List [str ]]], operator : str = "and" ):
131
152
if operator not in {"and" , "or" }:
132
153
raise XMLRPCWrappedError (
133
154
ValueError ("Invalid operator, must be one of 'and' or 'or'." )
@@ -220,7 +241,7 @@ def list_packages_with_serial(request):
220
241
221
242
222
243
@xmlrpc_method (method = "package_hosting_mode" )
223
- def package_hosting_mode (request , package_name ):
244
+ def package_hosting_mode (request , package_name : str ):
224
245
try :
225
246
project = (
226
247
request .db .query (Project )
@@ -234,7 +255,7 @@ def package_hosting_mode(request, package_name):
234
255
235
256
236
257
@xmlrpc_method (method = "user_packages" )
237
- def user_packages (request , username ):
258
+ def user_packages (request , username : str ):
238
259
roles = (
239
260
request .db .query (Role )
240
261
.join (User , Project )
@@ -253,7 +274,7 @@ def top_packages(request, num=None):
253
274
254
275
255
276
@xmlrpc_cache_by_project (method = "package_releases" )
256
- def package_releases (request , package_name , show_hidden = False ):
277
+ def package_releases (request , package_name : str , show_hidden : bool = False ):
257
278
try :
258
279
project = (
259
280
request .db .query (Project )
@@ -293,7 +314,7 @@ def package_data(request, package_name, version):
293
314
294
315
295
316
@xmlrpc_cache_by_project (method = "release_data" )
296
- def release_data (request , package_name , version ):
317
+ def release_data (request , package_name : str , version : str ):
297
318
try :
298
319
release = (
299
320
request .db .query (Release )
@@ -367,7 +388,7 @@ def package_urls(request, package_name, version):
367
388
368
389
369
390
@xmlrpc_cache_by_project (method = "release_urls" )
370
- def release_urls (request , package_name , version ):
391
+ def release_urls (request , package_name : str , version : str ):
371
392
files = (
372
393
request .db .query (File )
373
394
.join (Release , Project )
@@ -401,7 +422,7 @@ def release_urls(request, package_name, version):
401
422
402
423
403
424
@xmlrpc_cache_by_project (method = "package_roles" )
404
- def package_roles (request , package_name ):
425
+ def package_roles (request , package_name : str ):
405
426
roles = (
406
427
request .db .query (Role )
407
428
.join (User , Project )
@@ -418,7 +439,7 @@ def changelog_last_serial(request):
418
439
419
440
420
441
@xmlrpc_method (method = "changelog_since_serial" )
421
- def changelog_since_serial (request , serial ):
442
+ def changelog_since_serial (request , serial : int ):
422
443
entries = (
423
444
request .db .query (JournalEntry )
424
445
.filter (JournalEntry .id > serial )
@@ -439,7 +460,7 @@ def changelog_since_serial(request, serial):
439
460
440
461
441
462
@xmlrpc_method (method = "changelog" )
442
- def changelog (request , since , with_ids = False ):
463
+ def changelog (request , since : int , with_ids : bool = False ):
443
464
since = datetime .datetime .utcfromtimestamp (since )
444
465
entries = (
445
466
request .db .query (JournalEntry )
@@ -466,7 +487,7 @@ def changelog(request, since, with_ids=False):
466
487
467
488
468
489
@xmlrpc_method (method = "browse" )
469
- def browse (request , classifiers ):
490
+ def browse (request , classifiers : List [ str ] ):
470
491
classifiers_q = (
471
492
request .db .query (Classifier )
472
493
.filter (Classifier .classifier .in_ (classifiers ))
0 commit comments