1# pylint: disable=ansible-format-automatic-specification,raise-missing-from 2from __future__ import absolute_import, division, print_function 3__metaclass__ = type 4try: 5 from typing import Any, Iterable, List, Optional, Tuple # pylint: disable=unused-import 6except ImportError: 7 pass 8 9 10""" 11Apypie Action module 12""" 13 14try: 15 base_string = basestring 16except NameError: # Python 3 has no base_string 17 base_string = str # pylint: disable=invalid-name,redefined-builtin 18 19 20class Action(object): 21 """ 22 Apipie Action 23 """ 24 25 def __init__(self, name, resource, api): 26 # type: (str, str, Api) -> None 27 self.name = name 28 self.resource = resource 29 self.api = api 30 31 @property 32 def apidoc(self): 33 # type: () -> dict 34 """ 35 The apidoc of this action. 36 37 :returns: The apidoc. 38 """ 39 40 resource_methods = self.api.apidoc['docs']['resources'][self.resource]['methods'] 41 return [method for method in resource_methods if method['name'] == self.name][0] 42 43 @property 44 def routes(self): 45 # type: () -> List[Route] 46 """ 47 The routes this action can be invoked by. 48 49 :returns: The routes 50 """ 51 52 return [Route(route['api_url'], route['http_method'], route['short_description']) for route in self.apidoc['apis']] 53 54 @property 55 def params(self): 56 # type: () -> List[Param] 57 """ 58 The params accepted by this action. 59 60 :returns: The params. 61 """ 62 63 return [Param(**param) for param in self.apidoc['params']] 64 65 @property 66 def examples(self): 67 # type: () -> List[Example] 68 """ 69 The examples of this action. 70 71 :returns: The examples. 72 """ 73 74 return [Example.parse(example) for example in self.apidoc['examples']] 75 76 def call(self, params=None, headers=None, options=None, data=None, files=None): # pylint: disable=too-many-arguments 77 # type: (dict, Optional[dict], Optional[dict], Optional[Any], Optional[dict]) -> dict 78 """ 79 Call the API to execute the action. 80 81 :param params: The params that should be passed to the API. 82 :param headers: Additional headers to be passed to the API. 83 :param options: Options 84 :param data: Binary data to be submitted to the API. 85 :param files: Files to be submitted to the API. 86 87 :returns: The API response. 88 """ 89 90 return self.api.call(self.resource, self.name, params, headers, options, data, files) 91 92 def find_route(self, params=None): 93 # type: (Optional[dict]) -> Route 94 """ 95 Find the best matching route for a given set of params. 96 97 :param params: Params that should be submitted to the API. 98 99 :returns: The best route. 100 """ 101 102 param_keys = set(self.filter_empty_params(params).keys()) 103 sorted_routes = sorted(self.routes, key=lambda route: [-1 * len(route.params_in_path), route.path]) 104 for route in sorted_routes: 105 if set(route.params_in_path) <= param_keys: 106 return route 107 return sorted_routes[-1] 108 109 def validate(self, values, data=None, files=None): 110 # type: (dict, Optional[Any], Optional[dict]) -> None 111 """ 112 Validate a given set of parameter values against the required set of parameters. 113 114 :param values: The values to validate. 115 :param data: Additional binary data to validate. 116 :param files: Additional files to validate. 117 """ 118 119 self._validate(self.params, values, data, files) 120 121 @staticmethod 122 def _add_to_path(path=None, additions=None): 123 # type: (Optional[str], Optional[List[str]]) -> str 124 if path is None: 125 path = '' 126 if additions is None: 127 additions = [] 128 for addition in additions: 129 if path == '': 130 path = "{}".format(addition) 131 else: 132 path = "{}[{}]".format(path, addition) 133 return path 134 135 def _validate(self, params, values, data=None, files=None, path=None): # pylint: disable=too-many-arguments,too-many-locals 136 # type: (Iterable[Param], dict, Optional[Any], Optional[dict], Optional[str]) -> None 137 if not isinstance(values, dict): 138 raise InvalidArgumentTypesError 139 given_params = set(values.keys()) 140 given_files = set((files or {}).keys()) 141 given_data = set((data or {}).keys()) 142 required_params = {param.name for param in params if param.required} 143 missing_params = required_params - given_params - given_files - given_data 144 if missing_params: 145 missing_params_with_path = [self._add_to_path(path, [param]) for param in missing_params] 146 message = "The following required parameters are missing: {}".format(', '.join(missing_params_with_path)) 147 raise MissingArgumentsError(message) 148 149 for param, value in values.items(): 150 param_descriptions = [p for p in params if p.name == param] 151 if param_descriptions: 152 param_description = param_descriptions[0] 153 if param_description.params and value is not None: 154 if param_description.expected_type == 'array': 155 for num, item in enumerate(value): 156 self._validate(param_description.params, item, path=self._add_to_path(path, [param_description.name, str(num)])) 157 elif param_description.expected_type == 'hash': 158 self._validate(param_description.params, value, path=self._add_to_path(path, [param_description.name])) 159 if (param_description.expected_type == 'numeric' and isinstance(value, base_string)): 160 try: 161 value = int(value) 162 except ValueError: 163 # this will be caught in the next check 164 pass 165 if (not param_description.allow_nil and value is None): 166 raise ValueError("{} can't be {}".format(param, value)) 167 # pylint: disable=too-many-boolean-expressions 168 if (value is not None 169 and ((param_description.expected_type == 'boolean' and not isinstance(value, bool) and not (isinstance(value, int) and value in [0, 1])) 170 or (param_description.expected_type == 'numeric' and not isinstance(value, int)) 171 or (param_description.expected_type == 'string' and not isinstance(value, (base_string, int))))): 172 raise ValueError("{} ({}): {}".format(param, value, param_description.validator)) 173 174 @staticmethod 175 def filter_empty_params(params=None): 176 # type: (Optional[dict]) -> dict 177 """ 178 Filter out any params that have no value. 179 180 :param params: The params to filter. 181 182 :returns: The filtered params. 183 """ 184 result = {} 185 if params is not None: 186 if isinstance(params, dict): 187 result = {k: v for k, v in params.items() if v is not None} 188 else: 189 raise InvalidArgumentTypesError 190 return result 191 192 def prepare_params(self, input_dict): 193 # type: (dict) -> dict 194 """ 195 Transform a dict with data into one that can be accepted as params for calling the action. 196 197 This will ignore any keys that are not accepted as params when calling the action. 198 It also allows generating nested params without forcing the user to care about them. 199 200 :param input_dict: a dict with data that should be used to fill in the params 201 :return: :class:`dict` object 202 :rtype: dict 203 204 Usage:: 205 206 >>> action.prepare_params({'id': 1}) 207 {'user': {'id': 1}} 208 """ 209 params = self._prepare_params(self.params, input_dict) 210 route_params = self._prepare_route_params(input_dict) 211 params.update(route_params) 212 return params 213 214 def _prepare_params(self, action_params, input_dict): 215 # type: (Iterable[Param], dict) -> dict 216 result = {} 217 218 for param in action_params: 219 if param.expected_type == 'hash' and param.params: 220 nested_dict = input_dict.get(param.name, input_dict) 221 nested_result = self._prepare_params(param.params, nested_dict) 222 if nested_result: 223 result[param.name] = nested_result 224 elif param.name in input_dict: 225 result[param.name] = input_dict[param.name] 226 227 return result 228 229 def _prepare_route_params(self, input_dict): 230 # type: (dict) -> dict 231 result = {} 232 233 route = self.find_route(input_dict) 234 235 for url_param in route.params_in_path: 236 if url_param in input_dict: 237 result[url_param] = input_dict[url_param] 238 239 return result 240 241 242""" 243Apypie Api module 244""" 245 246 247import errno 248import glob 249import json 250try: 251 import requests 252except ImportError: 253 pass 254try: 255 from json.decoder import JSONDecodeError # type: ignore 256except ImportError: 257 JSONDecodeError = ValueError # type: ignore 258import os 259try: 260 from urlparse import urljoin # type: ignore 261except ImportError: 262 from urllib.parse import urljoin # type: ignore 263 264 265def _qs_param(param): 266 # type: (Any) -> Any 267 if isinstance(param, bool): 268 return str(param).lower() 269 return param 270 271 272class Api(object): 273 """ 274 Apipie API bindings 275 276 :param uri: base URL of the server 277 :param username: username to access the API 278 :param password: username to access the API 279 :param api_version: version of the API. Defaults to `1` 280 :param language: prefered locale for the API description 281 :param apidoc_cache_base_dir: base directory for building apidoc_cache_dir. Defaults to `~/.cache/apipie_bindings`. 282 :param apidoc_cache_dir: where to cache the JSON description of the API. Defaults to `apidoc_cache_base_dir/<URI>`. 283 :param apidoc_cache_name: name of the cache file. If there is cache in the `apidoc_cache_dir`, it is used. Defaults to `default`. 284 :param verify_ssl: should the SSL certificate be verified. Defaults to `True`. 285 286 Usage:: 287 288 >>> import apypie 289 >>> api = apypie.Api(uri='https://api.example.com', username='admin', password='changeme') 290 """ 291 292 def __init__(self, **kwargs): 293 self.uri = kwargs.get('uri') 294 self.api_version = kwargs.get('api_version', 1) 295 self.language = kwargs.get('language') 296 297 # Find where to put the cache by default according to the XDG spec 298 # Not using just get('XDG_CACHE_HOME', '~/.cache') because the spec says 299 # that the defaut should be used if "$XDG_CACHE_HOME is either not set or empty" 300 xdg_cache_home = os.environ.get('XDG_CACHE_HOME', None) 301 if not xdg_cache_home: 302 xdg_cache_home = '~/.cache' 303 304 apidoc_cache_base_dir = kwargs.get('apidoc_cache_base_dir', os.path.join(os.path.expanduser(xdg_cache_home), 'apypie')) 305 apidoc_cache_dir_default = os.path.join(apidoc_cache_base_dir, self.uri.replace(':', '_').replace('/', '_'), 'v{}'.format(self.api_version)) 306 self.apidoc_cache_dir = kwargs.get('apidoc_cache_dir', apidoc_cache_dir_default) 307 self.apidoc_cache_name = kwargs.get('apidoc_cache_name', self._find_cache_name()) 308 309 self._session = requests.Session() 310 self._session.verify = kwargs.get('verify_ssl', True) 311 312 self._session.headers['Accept'] = 'application/json;version={}'.format(self.api_version) 313 self._session.headers['User-Agent'] = 'apypie (https://github.com/Apipie/apypie)' 314 if self.language: 315 self._session.headers['Accept-Language'] = self.language 316 317 if kwargs.get('username') and kwargs.get('password'): 318 self._session.auth = (kwargs['username'], kwargs['password']) 319 320 self._apidoc = None 321 322 @property 323 def apidoc(self): 324 # type: () -> dict 325 """ 326 The full apidoc. 327 328 The apidoc will be fetched from the server, if that didn't happen yet. 329 330 :returns: The apidoc. 331 """ 332 333 if self._apidoc is None: 334 self._apidoc = self._load_apidoc() 335 return self._apidoc 336 337 @property 338 def apidoc_cache_file(self): 339 # type: () -> str 340 """ 341 Full local path to the cached apidoc. 342 """ 343 344 return os.path.join(self.apidoc_cache_dir, '{0}{1}'.format(self.apidoc_cache_name, self.cache_extension)) 345 346 def _cache_dir_contents(self): 347 # type: () -> Iterable[str] 348 return glob.iglob(os.path.join(self.apidoc_cache_dir, '*{}'.format(self.cache_extension))) 349 350 def _find_cache_name(self, default='default'): 351 cache_file = next(self._cache_dir_contents(), None) 352 cache_name = default 353 if cache_file: 354 cache_name = os.path.basename(cache_file)[:-len(self.cache_extension)] 355 return cache_name 356 357 def validate_cache(self, cache_name): 358 # type: (str) -> None 359 """ 360 Ensure the cached apidoc matches the one presented by the server. 361 362 :param cache_name: The name of the apidoc on the server. 363 """ 364 365 if cache_name is not None and cache_name != self.apidoc_cache_name: 366 self.clean_cache() 367 self.apidoc_cache_name = os.path.basename(os.path.normpath(cache_name)) 368 369 def clean_cache(self): 370 # type: () -> None 371 """ 372 Remove any locally cached apidocs. 373 """ 374 375 self._apidoc = None 376 for filename in self._cache_dir_contents(): 377 os.unlink(filename) 378 379 @property 380 def resources(self): 381 # type: () -> Iterable 382 """ 383 List of available resources. 384 385 Usage:: 386 387 >>> api.resources 388 ['comments', 'users'] 389 """ 390 return sorted(self.apidoc['docs']['resources'].keys()) 391 392 def resource(self, name): 393 # type: (str) -> Resource 394 """ 395 Get a resource. 396 397 :param name: the name of the resource to load 398 :return: :class:`Resource <Resource>` object 399 :rtype: apypie.Resource 400 401 Usage:: 402 403 >>> api.resource('users') 404 """ 405 if name in self.resources: 406 return Resource(self, name) 407 message = "Resource '{}' does not exist in the API. Existing resources: {}".format(name, ', '.join(sorted(self.resources))) 408 raise KeyError(message) 409 410 def _load_apidoc(self): 411 # type: () -> dict 412 try: 413 with open(self.apidoc_cache_file, 'r') as apidoc_file: 414 api_doc = json.load(apidoc_file) 415 except (IOError, JSONDecodeError): 416 api_doc = self._retrieve_apidoc() 417 return api_doc 418 419 def _retrieve_apidoc(self): 420 # type: () -> dict 421 try: 422 os.makedirs(self.apidoc_cache_dir) 423 except OSError as err: 424 if err.errno != errno.EEXIST or not os.path.isdir(self.apidoc_cache_dir): 425 raise 426 response = None 427 if self.language: 428 response = self._retrieve_apidoc_call('/apidoc/v{0}.{1}.json'.format(self.api_version, self.language), safe=True) 429 language_family = self.language.split('_')[0] 430 if not response and language_family != self.language: 431 response = self._retrieve_apidoc_call('/apidoc/v{0}.{1}.json'.format(self.api_version, language_family), safe=True) 432 if not response: 433 try: 434 response = self._retrieve_apidoc_call('/apidoc/v{}.json'.format(self.api_version)) 435 except Exception as exc: 436 raise DocLoadingError("""Could not load data from {0}: {1} 437 - is your server down? 438 - was rake apipie:cache run when using apipie cache? (typical production settings)""".format(self.uri, exc)) 439 with open(self.apidoc_cache_file, 'w') as apidoc_file: 440 apidoc_file.write(json.dumps(response)) 441 return response 442 443 def _retrieve_apidoc_call(self, path, safe=False): 444 try: 445 return self.http_call('get', path) 446 except requests.exceptions.HTTPError: 447 if not safe: 448 raise 449 450 def call(self, resource_name, action_name, params=None, headers=None, options=None, data=None, files=None): # pylint: disable=too-many-arguments 451 """ 452 Call an action in the API. 453 454 It finds most fitting route based on given parameters 455 with other attributes necessary to do an API call. 456 457 :param resource_name: name of the resource 458 :param action_name: action_name name of the action 459 :param params: Dict of parameters to be sent in the request 460 :param headers: Dict of headers to be sent in the request 461 :param options: Dict of options to influence the how the call is processed 462 * `skip_validation` (Bool) *false* - skip validation of parameters 463 :param data: Binary data to be sent in the request 464 :param files: Binary files to be sent in the request 465 :return: :class:`dict` object 466 :rtype: dict 467 468 Usage:: 469 470 >>> api.call('users', 'show', {'id': 1}) 471 """ 472 if options is None: 473 options = {} 474 if params is None: 475 params = {} 476 477 resource = Resource(self, resource_name) 478 action = resource.action(action_name) 479 if not options.get('skip_validation', False): 480 action.validate(params, data, files) 481 482 return self._call_action(action, params, headers, data, files) 483 484 def _call_action(self, action, params=None, headers=None, data=None, files=None): # pylint: disable=too-many-arguments 485 if params is None: 486 params = {} 487 488 route = action.find_route(params) 489 get_params = {key: value for key, value in params.items() if key not in route.params_in_path} 490 return self.http_call( 491 route.method, 492 route.path_with_params(params), 493 get_params, 494 headers, data, files) 495 496 def http_call(self, http_method, path, params=None, headers=None, data=None, files=None): # pylint: disable=too-many-arguments 497 """ 498 Execute an HTTP request. 499 500 :param params: Dict of parameters to be sent in the request 501 :param headers: Dict of headers to be sent in the request 502 :param data: Binary data to be sent in the request 503 :param files: Binary files to be sent in the request 504 505 :return: :class:`dict` object 506 :rtype: dict 507 """ 508 509 full_path = urljoin(self.uri, path) 510 kwargs = { 511 'verify': self._session.verify, 512 } 513 514 if headers: 515 kwargs['headers'] = headers 516 517 if params: 518 if http_method in ['get', 'head']: 519 kwargs['params'] = {k: _qs_param(v) for k, v in params.items()} 520 else: 521 kwargs['json'] = params 522 elif http_method in ['post', 'put', 'patch'] and not data and not files: 523 kwargs['json'] = {} 524 525 if files: 526 kwargs['files'] = files 527 528 if data: 529 kwargs['data'] = data 530 531 request = self._session.request(http_method, full_path, **kwargs) 532 request.raise_for_status() 533 self.validate_cache(request.headers.get('apipie-checksum')) 534 if request.status_code == requests.codes['no_content']: 535 return None 536 return request.json() 537 538 @property 539 def cache_extension(self): 540 """ 541 File extension for the local cache file. 542 543 Will include the language if set. 544 """ 545 546 if self.language: 547 ext = '.{}.json'.format(self.language) 548 else: 549 ext = '.json' 550 return ext 551 552 553""" 554Apypie Example module 555""" 556 557 558import re 559 560EXAMPLE_PARSER = re.compile(r'(\w+)\s+([^\n]*)\n?(.*)\n(\d+)\n(.*)', re.DOTALL) 561 562 563class Example(object): # pylint: disable=too-few-public-methods 564 """ 565 Apipie Example 566 """ 567 568 def __init__(self, http_method, path, args, status, response): # pylint: disable=too-many-arguments 569 # type: (str, str, str, str, str) -> None 570 self.http_method = http_method 571 self.path = path 572 self.args = args 573 self.status = int(status) 574 self.response = response 575 576 @classmethod 577 def parse(cls, example): 578 """ 579 Parse an example from an apidoc string 580 581 :returns: The parsed :class:`Example` 582 """ 583 parsed = EXAMPLE_PARSER.match(example) 584 return cls(*parsed.groups()) 585 586 587""" 588Apypie Exceptions 589""" 590 591 592class DocLoadingError(Exception): 593 """ 594 Exception to be raised when apidoc could not be loaded. 595 """ 596 597 598class MissingArgumentsError(Exception): 599 """ 600 Exception to be raised when required arguments are missing. 601 """ 602 603 604class InvalidArgumentTypesError(Exception): 605 """ 606 Exception to be raised when arguments are of the wrong type. 607 """ 608 609 610""" 611Apypie Inflector module 612 613Based on ActiveSupport Inflector (https://github.com/rails/rails.git) 614Inflection rules taken from davidcelis's Inflections (https://github.com/davidcelis/inflections.git) 615""" 616 617 618import re 619 620 621class Inflections(object): 622 """ 623 Inflections - rules how to convert words from singular to plural and vice versa. 624 """ 625 626 def __init__(self): 627 self.plurals = [] 628 self.singulars = [] 629 self.uncountables = [] 630 self.humans = [] 631 self.acronyms = {} 632 self.acronym_regex = r'/(?=a)b/' 633 634 def acronym(self, word): 635 # type: (str) -> None 636 """ 637 Add a new acronym. 638 """ 639 640 self.acronyms[word.lower()] = word 641 self.acronym_regex = '|'.join(self.acronyms.values()) 642 643 def plural(self, rule, replacement): 644 # type: (str, str) -> None 645 """ 646 Add a new plural rule. 647 """ 648 649 if rule in self.uncountables: 650 self.uncountables.remove(rule) 651 if replacement in self.uncountables: 652 self.uncountables.remove(replacement) 653 654 self.plurals.insert(0, (rule, replacement)) 655 656 def singular(self, rule, replacement): 657 # type: (str, str) -> None 658 """ 659 Add a new singular rule. 660 """ 661 662 if rule in self.uncountables: 663 self.uncountables.remove(rule) 664 if replacement in self.uncountables: 665 self.uncountables.remove(replacement) 666 667 self.singulars.insert(0, (rule, replacement)) 668 669 def irregular(self, singular, plural): 670 # type: (str, str) -> None 671 """ 672 Add a new irregular rule 673 """ 674 675 if singular in self.uncountables: 676 self.uncountables.remove(singular) 677 if plural in self.uncountables: 678 self.uncountables.remove(plural) 679 680 sfirst = singular[0] 681 srest = singular[1:] 682 683 pfirst = plural[0] 684 prest = plural[1:] 685 686 if sfirst.upper() == pfirst.upper(): 687 self.plural(r'(?i)({}){}$'.format(sfirst, srest), r'\1' + prest) 688 self.plural(r'(?i)({}){}$'.format(pfirst, prest), r'\1' + prest) 689 690 self.singular(r'(?i)({}){}$'.format(sfirst, srest), r'\1' + srest) 691 self.singular(r'(?i)({}){}$'.format(pfirst, prest), r'\1' + srest) 692 else: 693 self.plural(r'{}(?i){}$'.format(sfirst.upper(), srest), pfirst.upper() + prest) 694 self.plural(r'{}(?i){}$'.format(sfirst.lower(), srest), pfirst.lower() + prest) 695 self.plural(r'{}(?i){}$'.format(pfirst.upper(), prest), pfirst.upper() + prest) 696 self.plural(r'{}(?i){}$'.format(pfirst.lower(), prest), pfirst.lower() + prest) 697 698 self.singular(r'{}(?i){}$'.format(sfirst.upper(), srest), sfirst.upper() + srest) 699 self.singular(r'{}(?i){}$'.format(sfirst.lower(), srest), sfirst.lower() + srest) 700 self.singular(r'{}(?i){}$'.format(pfirst.upper(), prest), sfirst.upper() + srest) 701 self.singular(r'{}(?i){}$'.format(pfirst.lower(), prest), sfirst.lower() + srest) 702 703 def uncountable(self, *words): 704 """ 705 Add new uncountables. 706 """ 707 708 self.uncountables.extend(words) 709 710 def human(self, rule, replacement): 711 # type: (str, str) -> None 712 """ 713 Add a new humanize rule. 714 """ 715 716 self.humans.insert(0, (rule, replacement)) 717 718 719class Inflector(object): 720 """ 721 Inflector - perform inflections 722 """ 723 724 def __init__(self): 725 # type: () -> None 726 self.inflections = Inflections() 727 self.inflections.plural(r'$', 's') 728 self.inflections.plural(r'(?i)([sxz]|[cs]h)$', r'\1es') 729 self.inflections.plural(r'(?i)([^aeiouy]o)$', r'\1es') 730 self.inflections.plural(r'(?i)([^aeiouy])y$', r'\1ies') 731 732 self.inflections.singular(r'(?i)s$', r'') 733 self.inflections.singular(r'(?i)(ss)$', r'\1') 734 self.inflections.singular(r'([sxz]|[cs]h)es$', r'\1') 735 self.inflections.singular(r'([^aeiouy]o)es$', r'\1') 736 self.inflections.singular(r'(?i)([^aeiouy])ies$', r'\1y') 737 738 self.inflections.irregular('child', 'children') 739 self.inflections.irregular('man', 'men') 740 self.inflections.irregular('medium', 'media') 741 self.inflections.irregular('move', 'moves') 742 self.inflections.irregular('person', 'people') 743 self.inflections.irregular('self', 'selves') 744 self.inflections.irregular('sex', 'sexes') 745 746 self.inflections.uncountable('equipment', 'information', 'money', 'species', 'series', 'fish', 'sheep', 'police') 747 748 def pluralize(self, word): 749 # type: (str) -> str 750 """ 751 Pluralize a word. 752 """ 753 754 return self._apply_inflections(word, self.inflections.plurals) 755 756 def singularize(self, word): 757 # type: (str) -> str 758 """ 759 Singularize a word. 760 """ 761 762 return self._apply_inflections(word, self.inflections.singulars) 763 764 def _apply_inflections(self, word, rules): 765 # type: (str, Iterable[Tuple[str, str]]) -> str 766 result = word 767 768 if word != '' and result.lower() not in self.inflections.uncountables: 769 for (rule, replacement) in rules: 770 result = re.sub(rule, replacement, result) 771 if result != word: 772 break 773 774 return result 775 776 777""" 778Apypie Param module 779""" 780 781 782import re 783 784HTML_STRIP = re.compile(r'<\/?[^>]+?>') 785 786 787class Param(object): # pylint: disable=too-many-instance-attributes,too-few-public-methods 788 """ 789 Apipie Param 790 """ 791 792 def __init__(self, **kwargs): 793 self.allow_nil = kwargs.get('allow_nil') 794 self.description = HTML_STRIP.sub('', kwargs.get('description')) 795 self.expected_type = kwargs.get('expected_type') 796 self.full_name = kwargs.get('full_name') 797 self.name = kwargs.get('name') 798 self.params = [Param(**param) for param in kwargs.get('params', [])] 799 self.required = bool(kwargs.get('required')) 800 self.validator = kwargs.get('validator') 801 802 803""" 804Apypie Resource module 805""" 806 807 808class Resource(object): 809 """ 810 Apipie Resource 811 """ 812 813 def __init__(self, api, name): 814 # type: (Api, str) -> None 815 self.api = api 816 self.name = name 817 818 @property 819 def actions(self): 820 # type: () -> List 821 """ 822 Actions available for this resource. 823 824 :returns: The actions. 825 """ 826 return sorted([method['name'] for method in self.api.apidoc['docs']['resources'][self.name]['methods']]) 827 828 def action(self, name): 829 # type: (str) -> Action 830 """ 831 Get an :class:`Action` for this resource. 832 833 :param name: The name of the action. 834 """ 835 if self.has_action(name): 836 return Action(name, self.name, self.api) 837 message = "Unknown action '{}'. Supported actions: {}".format(name, ', '.join(sorted(self.actions))) 838 raise KeyError(message) 839 840 def has_action(self, name): 841 # type: (str) -> bool 842 """ 843 Check whether the resource has a given action. 844 845 :param name: The name of the action. 846 """ 847 return name in self.actions 848 849 def call(self, action, params=None, headers=None, options=None, data=None, files=None): # pylint: disable=too-many-arguments 850 # type: (str, Optional[dict], Optional[dict], Optional[dict], Optional[Any], Optional[dict]) -> dict 851 """ 852 Call the API to execute an action for this resource. 853 854 :param action: The action to call. 855 :param params: The params that should be passed to the API. 856 :param headers: Additional headers to be passed to the API. 857 :param options: Options 858 :param data: Binary data to be submitted to the API. 859 :param files: Files to be submitted to the API. 860 861 :returns: The API response. 862 """ 863 864 return self.api.call(self.name, action, params, headers, options, data, files) 865 866 867""" 868Apypie Route module 869""" 870 871 872class Route(object): 873 """ 874 Apipie Route 875 """ 876 877 def __init__(self, path, method, description=""): 878 # type: (str, str, str) -> None 879 self.path = path 880 self.method = method.lower() 881 self.description = description 882 883 @property 884 def params_in_path(self): 885 # type: () -> List 886 """ 887 Params that can be passed in the path (URL) of the route. 888 889 :returns: The params. 890 """ 891 return [part[1:] for part in self.path.split('/') if part.startswith(':')] 892 893 def path_with_params(self, params=None): 894 # type: (Optional[dict]) -> str 895 """ 896 Fill in the params into the path. 897 898 :returns: The path with params. 899 """ 900 result = self.path 901 if params is not None: 902 for param in self.params_in_path: 903 if param in params: 904 result = result.replace(':{}'.format(param), str(params[param])) 905 else: 906 raise KeyError("missing param '{}' in parameters".format(param)) 907 return result 908