apytizer 0.0.1a0__py3-none-any.whl → 0.0.1b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apytizer/__init__.py +2 -12
- apytizer/adapters/__init__.py +2 -3
- apytizer/adapters/transport_adapter.py +91 -0
- apytizer/apis/__init__.py +6 -0
- apytizer/apis/abstract_api.py +36 -0
- apytizer/apis/web_api.py +461 -0
- apytizer/connections/__init__.py +6 -0
- apytizer/connections/abstract_connection.py +28 -0
- apytizer/connections/http_connection.py +431 -0
- apytizer/decorators/__init__.py +5 -5
- apytizer/decorators/caching.py +60 -9
- apytizer/decorators/chunking.py +105 -0
- apytizer/decorators/connection.py +55 -20
- apytizer/decorators/json_response.py +93 -0
- apytizer/decorators/pagination.py +50 -32
- apytizer/endpoints/__init__.py +6 -0
- apytizer/endpoints/abstract_endpoint.py +38 -0
- apytizer/endpoints/web_endpoint.py +519 -0
- apytizer/engines/__init__.py +6 -0
- apytizer/engines/abstract_engine.py +45 -0
- apytizer/engines/http_engine.py +129 -0
- apytizer/errors.py +34 -0
- apytizer/factories/__init__.py +5 -0
- apytizer/factories/abstract_factory.py +17 -0
- apytizer/http_methods.py +34 -0
- apytizer/managers/__init__.py +12 -0
- apytizer/managers/abstract_manager.py +80 -0
- apytizer/managers/base_manager.py +116 -0
- apytizer/mappers/__init__.py +6 -0
- apytizer/mappers/abstract_mapper.py +48 -0
- apytizer/mappers/base_mapper.py +78 -0
- apytizer/media_types.py +118 -0
- apytizer/models/__init__.py +6 -0
- apytizer/models/abstract_model.py +119 -0
- apytizer/models/base_model.py +85 -0
- apytizer/protocols.py +38 -0
- apytizer/repositories/__init__.py +6 -0
- apytizer/repositories/abstract_repository.py +81 -0
- apytizer/repositories/managed_repository.py +92 -0
- apytizer/routes/__init__.py +6 -0
- apytizer/routes/abstract_route.py +32 -0
- apytizer/routes/base_route.py +138 -0
- apytizer/sessions/__init__.py +33 -0
- apytizer/sessions/abstract_session.py +63 -0
- apytizer/sessions/requests_session.py +125 -0
- apytizer/states/__init__.py +6 -0
- apytizer/states/abstract_state.py +71 -0
- apytizer/states/local_state.py +99 -0
- apytizer/utils/__init__.py +9 -4
- apytizer/utils/caching.py +39 -0
- apytizer/utils/dictionaries.py +375 -0
- apytizer/utils/errors.py +104 -0
- apytizer/utils/iterables.py +91 -0
- apytizer/utils/objects.py +145 -0
- apytizer/utils/strings.py +69 -0
- apytizer/utils/typing.py +29 -0
- apytizer-0.0.1b1.dist-info/METADATA +41 -0
- apytizer-0.0.1b1.dist-info/RECORD +60 -0
- {apytizer-0.0.1a0.dist-info → apytizer-0.0.1b1.dist-info}/WHEEL +1 -2
- apytizer/abstracts/__init__.py +0 -8
- apytizer/abstracts/api.py +0 -147
- apytizer/abstracts/endpoint.py +0 -177
- apytizer/abstracts/model.py +0 -50
- apytizer/abstracts/session.py +0 -39
- apytizer/adapters/transport.py +0 -40
- apytizer/base/__init__.py +0 -8
- apytizer/base/api.py +0 -510
- apytizer/base/endpoint.py +0 -443
- apytizer/base/model.py +0 -119
- apytizer/decorators/json.py +0 -35
- apytizer/utils/generate_key.py +0 -18
- apytizer/utils/merge.py +0 -19
- apytizer-0.0.1a0.dist-info/METADATA +0 -27
- apytizer-0.0.1a0.dist-info/RECORD +0 -25
- apytizer-0.0.1a0.dist-info/top_level.txt +0 -1
- {apytizer-0.0.1a0.dist-info → apytizer-0.0.1b1.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# src/apytizer/base/model.py
|
|
3
|
+
"""Base model class.
|
|
4
|
+
|
|
5
|
+
This module defines the implementation of a base model class.
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# Standard Library Imports
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
from typing import Any
|
|
12
|
+
from typing import Generator
|
|
13
|
+
from typing import Hashable
|
|
14
|
+
from typing import Mapping
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
|
|
17
|
+
# Local Imports
|
|
18
|
+
from .abstract_model import AbstractModel
|
|
19
|
+
from .. import states
|
|
20
|
+
|
|
21
|
+
__all__ = ["BaseModel"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BaseModel(AbstractModel):
|
|
25
|
+
"""Implements a stateful model.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
**kwargs: Data with which to set model state.
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
reference: Hashable
|
|
33
|
+
|
|
34
|
+
def __init__(self, **kwargs: Any):
|
|
35
|
+
self._state = states.LocalState(kwargs)
|
|
36
|
+
|
|
37
|
+
def __contains__(self, key: str) -> bool:
|
|
38
|
+
return key in self._state
|
|
39
|
+
|
|
40
|
+
def __eq__(self, other: object) -> bool:
|
|
41
|
+
return (
|
|
42
|
+
other.reference == self.reference
|
|
43
|
+
if isinstance(other, BaseModel)
|
|
44
|
+
else False
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def __hash__(self) -> int:
|
|
48
|
+
return hash(self.reference)
|
|
49
|
+
|
|
50
|
+
def __getattr__(self, name: str) -> Any:
|
|
51
|
+
attr = self._state.get(name)
|
|
52
|
+
if not attr:
|
|
53
|
+
cls = self.__class__.__name__
|
|
54
|
+
message = f"type object '{cls!s}' has no attribute '{name!s}'"
|
|
55
|
+
raise AttributeError(message)
|
|
56
|
+
|
|
57
|
+
return attr
|
|
58
|
+
|
|
59
|
+
def __getitem__(self, key: str) -> Any:
|
|
60
|
+
value = self._state[key]
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
def __iter__(self) -> Generator[Tuple[str, Any], None, None]:
|
|
64
|
+
yield from self._state.items()
|
|
65
|
+
|
|
66
|
+
def __repr__(self) -> str:
|
|
67
|
+
return self.__class__.__name__
|
|
68
|
+
|
|
69
|
+
def update(self, __m: Mapping[str, Any], **kwargs: Any) -> None:
|
|
70
|
+
"""Update local state with provided data.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
__m (optional): Mapping with which to update local state.
|
|
74
|
+
**kwargs: Data with which to update local state.
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
self._state.update(__m, **kwargs)
|
|
78
|
+
|
|
79
|
+
def rollback(self) -> None:
|
|
80
|
+
"""Rollback changes to local state."""
|
|
81
|
+
self._state.rollback()
|
|
82
|
+
|
|
83
|
+
def save(self) -> None:
|
|
84
|
+
"""Save changes to local state."""
|
|
85
|
+
self._state.save()
|
apytizer/protocols.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# src/apytizer/protocols.py
|
|
3
|
+
|
|
4
|
+
# Standard Library Imports
|
|
5
|
+
import enum
|
|
6
|
+
import re
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
__all__ = ["Protocol", "get_protocol"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@enum.unique
|
|
13
|
+
class Protocol(str, enum.Enum):
|
|
14
|
+
"""Implements standard application layer protocols."""
|
|
15
|
+
|
|
16
|
+
HTTP = "http"
|
|
17
|
+
HTTPS = "https"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ----------------------------------------------------------------------------
|
|
21
|
+
# Selectors
|
|
22
|
+
# ----------------------------------------------------------------------------
|
|
23
|
+
def get_protocol(
|
|
24
|
+
url: str, default: Optional[Protocol] = None
|
|
25
|
+
) -> Optional[Protocol]:
|
|
26
|
+
"""Get protocol from URL.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
url: URL from which to get protocol.
|
|
30
|
+
default (optional): Default protocol. Default ``None``.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Protocol.
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
match = re.match(r"\w+(?=://)", url, re.I)
|
|
37
|
+
result = Protocol(match.group(0)) if match else default
|
|
38
|
+
return result
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# src/apytizer/repositories/abstract_repository.py
|
|
3
|
+
"""Abstract repository class interface.
|
|
4
|
+
|
|
5
|
+
This module defines an abstract repository class which provides an interface
|
|
6
|
+
for subclasses to implement.
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# Standard Library Imports
|
|
11
|
+
import abc
|
|
12
|
+
from typing import Any
|
|
13
|
+
from typing import List
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
# Local Imports
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from ..models import AbstractModel
|
|
19
|
+
|
|
20
|
+
__all__ = ["AbstractRepository"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AbstractRepository(abc.ABC):
|
|
24
|
+
"""Represents an abstract repository."""
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def objects(self) -> List[AbstractModel]:
|
|
28
|
+
"""Objects in repository."""
|
|
29
|
+
raise NotImplementedError
|
|
30
|
+
|
|
31
|
+
@abc.abstractmethod
|
|
32
|
+
def add(self, obj: AbstractModel, /) -> None:
|
|
33
|
+
"""Abstract method to add an object to repository.
|
|
34
|
+
|
|
35
|
+
This method only adds an object to the local state of the repository.
|
|
36
|
+
New objects are not sent to the associated endpoint until the client
|
|
37
|
+
commits changes to the repository.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
obj: Instance of an abstract model subclass.
|
|
41
|
+
|
|
42
|
+
"""
|
|
43
|
+
raise NotImplementedError
|
|
44
|
+
|
|
45
|
+
@abc.abstractmethod
|
|
46
|
+
def get(self, ref: Any, *args: Any, **kwargs: Any) -> AbstractModel:
|
|
47
|
+
"""Abstract method to get an object from the repository.
|
|
48
|
+
|
|
49
|
+
If the object is not found in local state, a request is send to the
|
|
50
|
+
associated endpoint.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
ref: Reference to object.
|
|
54
|
+
*args: Positional arguments.
|
|
55
|
+
**kwargs: Keyword arguments.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Instance of an abstract model subclass.
|
|
59
|
+
|
|
60
|
+
"""
|
|
61
|
+
raise NotImplementedError
|
|
62
|
+
|
|
63
|
+
@abc.abstractmethod
|
|
64
|
+
def remove(self, obj: AbstractModel, /) -> None:
|
|
65
|
+
"""Abstract method to remove an object from repository.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
obj: Instance of an abstract model subclass.
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
raise NotImplementedError
|
|
72
|
+
|
|
73
|
+
@abc.abstractmethod
|
|
74
|
+
def commit(self) -> None:
|
|
75
|
+
"""Abstract method for committing changes to objects."""
|
|
76
|
+
raise NotImplementedError
|
|
77
|
+
|
|
78
|
+
@abc.abstractmethod
|
|
79
|
+
def rollback(self) -> None:
|
|
80
|
+
"""Abstract method for rolling back changes to objects."""
|
|
81
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# src/apytizer/repositories/managed_repository.py
|
|
3
|
+
"""Managed Repository Class.
|
|
4
|
+
|
|
5
|
+
This module defines a managed repository class.
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# Standard Library Imports
|
|
10
|
+
from typing import Any
|
|
11
|
+
from typing import List
|
|
12
|
+
from typing import Optional
|
|
13
|
+
from typing import Set
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
# Local Imports
|
|
17
|
+
from .abstract_repository import AbstractRepository
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from ..managers import AbstractManager
|
|
21
|
+
from ..models import AbstractModel
|
|
22
|
+
|
|
23
|
+
__all__ = ["ManagedRepository"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ManagedRepository(AbstractRepository):
|
|
27
|
+
"""Implements a managed repository.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
manager: Manager.
|
|
31
|
+
objects (optional): Objects to include in repository.
|
|
32
|
+
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
manager: AbstractManager,
|
|
38
|
+
objects: Optional[List[AbstractModel]] = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
self._manager = manager
|
|
41
|
+
self._objects: Set[AbstractModel] = set(objects or [])
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def objects(self) -> List[AbstractModel]:
|
|
45
|
+
"""Objects in repository."""
|
|
46
|
+
result = list(self._objects)
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
def add(self, obj: AbstractModel) -> None:
|
|
50
|
+
"""Add an object to the repository.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
obj: Object to add.
|
|
54
|
+
|
|
55
|
+
"""
|
|
56
|
+
self._objects.add(obj)
|
|
57
|
+
|
|
58
|
+
def get(self, ref: Any) -> AbstractModel:
|
|
59
|
+
"""Get an object from the repository.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
ref: Reference to object.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Object.
|
|
66
|
+
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
result = next(obj for obj in self.objects if obj.reference == ref)
|
|
70
|
+
except StopIteration:
|
|
71
|
+
result = self._manager.read(ref)
|
|
72
|
+
self._objects.add(result)
|
|
73
|
+
|
|
74
|
+
return result
|
|
75
|
+
|
|
76
|
+
def remove(self, obj: AbstractModel) -> None:
|
|
77
|
+
"""Remove an object from the repository.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
obj: Object to remove.
|
|
81
|
+
|
|
82
|
+
"""
|
|
83
|
+
self._manager.delete(obj)
|
|
84
|
+
self._objects.discard(obj)
|
|
85
|
+
|
|
86
|
+
def commit(self) -> None:
|
|
87
|
+
"""Commit changes to objects in the repository."""
|
|
88
|
+
raise NotImplementedError
|
|
89
|
+
|
|
90
|
+
def rollback(self) -> None:
|
|
91
|
+
"""Rollback changes to objects in the repository."""
|
|
92
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# src/apytizer/routes/abstract_route.py
|
|
3
|
+
|
|
4
|
+
# Standard Library Imports
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import abc
|
|
7
|
+
|
|
8
|
+
__all__ = ["AbstractRoute"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AbstractRoute(abc.ABC):
|
|
12
|
+
"""Represents an abstract route."""
|
|
13
|
+
|
|
14
|
+
@abc.abstractmethod
|
|
15
|
+
def __add__(self, other: object) -> AbstractRoute:
|
|
16
|
+
raise NotImplementedError
|
|
17
|
+
|
|
18
|
+
@abc.abstractmethod
|
|
19
|
+
def __eq__(self, other: object) -> bool:
|
|
20
|
+
raise NotImplementedError
|
|
21
|
+
|
|
22
|
+
@abc.abstractmethod
|
|
23
|
+
def __hash__(self) -> int:
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
|
|
26
|
+
@abc.abstractmethod
|
|
27
|
+
def __str__(self) -> str:
|
|
28
|
+
raise NotImplementedError
|
|
29
|
+
|
|
30
|
+
@abc.abstractmethod
|
|
31
|
+
def __truediv__(self, other: object) -> AbstractRoute:
|
|
32
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# src/apytizer/routes/base_route.py
|
|
3
|
+
|
|
4
|
+
# Standard Library Imports
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
# Local Imports
|
|
8
|
+
from .abstract_route import AbstractRoute
|
|
9
|
+
|
|
10
|
+
__all__ = ["Route"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Route(AbstractRoute):
|
|
14
|
+
"""Implements a route.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
__value: Value.
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, __value: str = "/", /) -> None:
|
|
22
|
+
if not isinstance(__value, (AbstractRoute, str)): # type: ignore
|
|
23
|
+
message = f"expected type 'str', got {type(__value)} instead"
|
|
24
|
+
raise TypeError(message)
|
|
25
|
+
|
|
26
|
+
self._value = str(__value).strip("/") + "/"
|
|
27
|
+
|
|
28
|
+
def __eq__(self, other: object) -> bool:
|
|
29
|
+
if not isinstance(other, (AbstractRoute, str)):
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
other_segments = get_segments(str(other))
|
|
33
|
+
self_segments = get_segments(self._value)
|
|
34
|
+
result = segments_are_equal(other_segments, self_segments)
|
|
35
|
+
return result
|
|
36
|
+
|
|
37
|
+
def __hash__(self) -> int:
|
|
38
|
+
return hash(self._value.lower())
|
|
39
|
+
|
|
40
|
+
def __len__(self) -> int:
|
|
41
|
+
return len(get_segments(self._value))
|
|
42
|
+
|
|
43
|
+
def __str__(self) -> str:
|
|
44
|
+
return self._value
|
|
45
|
+
|
|
46
|
+
def __gt__(self, other: object) -> bool:
|
|
47
|
+
if not isinstance(other, (AbstractRoute, str)):
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
self_segments = get_segments(self._value)
|
|
51
|
+
other_segments = get_segments(str(other))
|
|
52
|
+
result = tuple(self_segments) > tuple(other_segments)
|
|
53
|
+
return result
|
|
54
|
+
|
|
55
|
+
def __lt__(self, other: object) -> bool:
|
|
56
|
+
if not isinstance(other, (AbstractRoute, str)):
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
self_segments = get_segments(self._value)
|
|
60
|
+
other_segments = get_segments(str(other))
|
|
61
|
+
result = tuple(self_segments) < tuple(other_segments)
|
|
62
|
+
return result
|
|
63
|
+
|
|
64
|
+
def __add__(self, other: object) -> AbstractRoute:
|
|
65
|
+
if not isinstance(other, str):
|
|
66
|
+
message = f"expected type 'str', got {type(other)} instead"
|
|
67
|
+
raise TypeError(message)
|
|
68
|
+
|
|
69
|
+
result = Route(self._value + other.strip("/"))
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
def __truediv__(self, other: object) -> AbstractRoute:
|
|
73
|
+
if not isinstance(other, str):
|
|
74
|
+
message = f"expected type 'str', got {type(other)} instead"
|
|
75
|
+
raise TypeError(message)
|
|
76
|
+
|
|
77
|
+
result = Route(self._value + other.strip("/"))
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ----------------------------------------------------------------------------
|
|
82
|
+
# Helpers
|
|
83
|
+
# ----------------------------------------------------------------------------
|
|
84
|
+
def count_segments(__value: str, /) -> int:
|
|
85
|
+
"""Get number of segments.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
__value: Value for which to get number of segments.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Number of segments.
|
|
92
|
+
|
|
93
|
+
"""
|
|
94
|
+
result = len(get_segments(__value))
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_segments(__value: str, /) -> List[str]:
|
|
99
|
+
"""Get segments of string seperated by slashes.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
__value: Value for which to get segments.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Segments.
|
|
106
|
+
|
|
107
|
+
"""
|
|
108
|
+
if not isinstance(__value, str): # type: ignore
|
|
109
|
+
message = f"expected type 'str', got {type(__value)} instead"
|
|
110
|
+
raise TypeError(message)
|
|
111
|
+
|
|
112
|
+
result = __value.strip("/").split("/")
|
|
113
|
+
return result
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def segments_are_equal(
|
|
117
|
+
first: List[str], second: List[str], placeholder: str = r"{}"
|
|
118
|
+
) -> bool:
|
|
119
|
+
"""Check whether segments are equal.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
first: First list of segments.
|
|
123
|
+
second: Second list of segments.
|
|
124
|
+
placeholder: Placeholder. Default ``{}``.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Whether segments are equal.
|
|
128
|
+
|
|
129
|
+
"""
|
|
130
|
+
if len(first) != len(second):
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
result = all(
|
|
134
|
+
left.lower() == right.lower()
|
|
135
|
+
for left, right in zip(first, second)
|
|
136
|
+
if right != placeholder
|
|
137
|
+
)
|
|
138
|
+
return result
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# src/apytizer/sessions/__init__.py
|
|
3
|
+
|
|
4
|
+
# Standard Library Imports
|
|
5
|
+
from typing import Type
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
# Local Imports
|
|
9
|
+
from .abstract_session import *
|
|
10
|
+
from .requests_session import *
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ..engines import AbstractEngine
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class sessionmaker:
|
|
17
|
+
"""Implements a sessionmaker."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self, class_: Type[RequestsSession] = RequestsSession
|
|
21
|
+
) -> None:
|
|
22
|
+
self._session_type = class_
|
|
23
|
+
|
|
24
|
+
def __call__(self, __engine: "AbstractEngine", /) -> RequestsSession:
|
|
25
|
+
result = self._session_type(
|
|
26
|
+
adapters=getattr(__engine, "adapters", None),
|
|
27
|
+
auth=getattr(__engine, "auth", None),
|
|
28
|
+
cert=getattr(__engine, "cert", None),
|
|
29
|
+
proxies=getattr(__engine, "proxies", None),
|
|
30
|
+
stream=getattr(__engine, "stream", False),
|
|
31
|
+
verify=getattr(__engine, "verify", True),
|
|
32
|
+
)
|
|
33
|
+
return result
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# src/apytizer/sessions/abstract_session.py
|
|
3
|
+
"""Abstract Session Class.
|
|
4
|
+
|
|
5
|
+
This module defines an abstract session class which provides an interface
|
|
6
|
+
for subclasses to implement.
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# Standard Library Imports
|
|
11
|
+
import abc
|
|
12
|
+
from typing import Any
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
# Third-Party Imports
|
|
16
|
+
import requests
|
|
17
|
+
from requests.adapters import HTTPAdapter
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from ..protocols import Protocol
|
|
21
|
+
|
|
22
|
+
__all__ = ["AbstractSession"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AbstractSession(abc.ABC):
|
|
26
|
+
"""Represents an abstract session."""
|
|
27
|
+
|
|
28
|
+
@abc.abstractmethod
|
|
29
|
+
def start(self) -> None:
|
|
30
|
+
"""Starts session."""
|
|
31
|
+
raise NotImplementedError
|
|
32
|
+
|
|
33
|
+
@abc.abstractmethod
|
|
34
|
+
def close(self) -> None:
|
|
35
|
+
"""Destroys session."""
|
|
36
|
+
raise NotImplementedError
|
|
37
|
+
|
|
38
|
+
@abc.abstractmethod
|
|
39
|
+
def mount(self, protocol: "Protocol", adapter: HTTPAdapter) -> None:
|
|
40
|
+
"""Registers a connection adapter to a protocol prefix.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
protocol: Protocol prefix on which to mount adapter.
|
|
44
|
+
adapter: HTTP adapter to apply to connection.
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
raise NotImplementedError
|
|
48
|
+
|
|
49
|
+
@abc.abstractmethod
|
|
50
|
+
def send(
|
|
51
|
+
self, __request: requests.Request, /, **kwargs: Any
|
|
52
|
+
) -> requests.Response:
|
|
53
|
+
"""Send an HTTP request.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
__request: Request to send.
|
|
57
|
+
**kwargs: Keyword arguments.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Response.
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# src/apytizer/sessions/requests_session.py
|
|
3
|
+
"""Requests Session Class.
|
|
4
|
+
|
|
5
|
+
This module defines the requests session class implementation.
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# Standard Library Imports
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Any
|
|
12
|
+
from typing import Dict
|
|
13
|
+
from typing import MutableMapping
|
|
14
|
+
from typing import Optional
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
from typing import final
|
|
18
|
+
|
|
19
|
+
# Third-Party Imports
|
|
20
|
+
import requests
|
|
21
|
+
from requests.auth import AuthBase
|
|
22
|
+
from requests.adapters import HTTPAdapter
|
|
23
|
+
|
|
24
|
+
# Local Imports
|
|
25
|
+
from .abstract_session import AbstractSession
|
|
26
|
+
from ..protocols import Protocol
|
|
27
|
+
|
|
28
|
+
__all__ = ["RequestsSession"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
log = logging.getLogger("apytizer")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RequestsSession(AbstractSession):
|
|
35
|
+
"""Implements a requests session.
|
|
36
|
+
|
|
37
|
+
The `RequestsSession` class provides `start` and `close` methods for manual
|
|
38
|
+
control of the session.
|
|
39
|
+
|
|
40
|
+
A session instance can also be used as a context manager.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
adapters (optional): Connection adapters. Default ``None``.
|
|
44
|
+
auth (optional): Authentication header. default ``None``.
|
|
45
|
+
cert (optional): Client certificate. Default ``None``.
|
|
46
|
+
proxies (optional): Protocols mapped to proxy URLs. Default ``None``.
|
|
47
|
+
stream (optional): Whether to stream response content. Default ``False``.
|
|
48
|
+
verify (optional): Whether to verify certificate. Default ``True``.
|
|
49
|
+
|
|
50
|
+
.. Requests Documentation:
|
|
51
|
+
https://docs.python-requests.org/en/latest/api/#request-sessions
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
*,
|
|
58
|
+
adapters: Optional[Dict[Protocol, HTTPAdapter]] = None,
|
|
59
|
+
auth: Optional[Union[AuthBase, Tuple[str, str]]] = None,
|
|
60
|
+
cert: Optional[Union[str, Tuple[str, str]]] = None,
|
|
61
|
+
proxies: Optional[MutableMapping[str, str]] = None,
|
|
62
|
+
stream: Optional[bool] = False,
|
|
63
|
+
verify: Optional[bool] = True,
|
|
64
|
+
) -> None:
|
|
65
|
+
self._session = requests.Session()
|
|
66
|
+
self._session.auth = auth
|
|
67
|
+
self._session.cert = cert
|
|
68
|
+
self._session.proxies = proxies # type: ignore
|
|
69
|
+
self._session.stream = stream # type: ignore
|
|
70
|
+
self._session.verify = verify # type: ignore
|
|
71
|
+
|
|
72
|
+
if adapters is not None:
|
|
73
|
+
for protocol, adapter in adapters.items():
|
|
74
|
+
self._session.mount(protocol, adapter)
|
|
75
|
+
|
|
76
|
+
@final
|
|
77
|
+
def __enter__(self) -> AbstractSession:
|
|
78
|
+
"""Starts session as context manager."""
|
|
79
|
+
self.start()
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
@final
|
|
83
|
+
def __exit__(self, *_) -> None:
|
|
84
|
+
"""Stops session as context manager."""
|
|
85
|
+
self.close()
|
|
86
|
+
|
|
87
|
+
def start(self) -> None:
|
|
88
|
+
"""Starts session."""
|
|
89
|
+
log.debug("Starting session...")
|
|
90
|
+
|
|
91
|
+
def close(self, *_: Any) -> None:
|
|
92
|
+
"""Destroys session."""
|
|
93
|
+
self._session.close()
|
|
94
|
+
log.debug("Session closed")
|
|
95
|
+
|
|
96
|
+
def mount(self, protocol: Protocol, adapter: HTTPAdapter) -> None:
|
|
97
|
+
"""Registers connection adapter to protocol prefix.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
protocol: Protocol on which to mount adapter.
|
|
101
|
+
adapter: HTTP adapter to apply to connection.
|
|
102
|
+
|
|
103
|
+
"""
|
|
104
|
+
self._session.mount(f"{protocol.value!s}://", adapter)
|
|
105
|
+
log.debug(
|
|
106
|
+
"Mounted %(adapter)s adapter to %(protocol)s protocol",
|
|
107
|
+
{"adapter": adapter, "protocol": protocol.value},
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def send(
|
|
111
|
+
self, __request: requests.Request, /, **kwargs: Any
|
|
112
|
+
) -> requests.Response:
|
|
113
|
+
"""Send an HTTP request.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
__request: Request to send.
|
|
117
|
+
**kwargs (optional): Keyword arguments.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Response object.
|
|
121
|
+
|
|
122
|
+
"""
|
|
123
|
+
request = self._session.prepare_request(__request)
|
|
124
|
+
response = self._session.send(request, **kwargs)
|
|
125
|
+
return response
|