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