attp-sdk 0.1.0__tar.gz
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.
- attp_sdk-0.1.0/PKG-INFO +61 -0
- attp_sdk-0.1.0/README.md +45 -0
- attp_sdk-0.1.0/pyproject.toml +26 -0
- attp_sdk-0.1.0/src/attp/__init__.py +8 -0
- attp_sdk-0.1.0/src/attp/client/__init__.py +6 -0
- attp_sdk-0.1.0/src/attp/client/authenticator.py +84 -0
- attp_sdk-0.1.0/src/attp/client/configs.py +32 -0
- attp_sdk-0.1.0/src/attp/client/service_discovery.py +107 -0
- attp_sdk-0.1.0/src/attp/client/session_driver.py +120 -0
- attp_sdk-0.1.0/src/attp/decorators/__init__.py +6 -0
- attp_sdk-0.1.0/src/attp/decorators/attp_call.py +15 -0
- attp_sdk-0.1.0/src/attp/decorators/attp_event.py +20 -0
- attp_sdk-0.1.0/src/attp/decorators/attp_handler.py +15 -0
- attp_sdk-0.1.0/src/attp/decorators/attp_lifecycle.py +19 -0
- attp_sdk-0.1.0/src/attp/loadbalancer/__init__.py +7 -0
- attp_sdk-0.1.0/src/attp/loadbalancer/abc/cacher.py +23 -0
- attp_sdk-0.1.0/src/attp/loadbalancer/abc/candidate.py +7 -0
- attp_sdk-0.1.0/src/attp/loadbalancer/abc/strategy.py +22 -0
- attp_sdk-0.1.0/src/attp/loadbalancer/balancer.py +57 -0
- attp_sdk-0.1.0/src/attp/loadbalancer/caches/__init__.py +0 -0
- attp_sdk-0.1.0/src/attp/loadbalancer/caches/memory_cache.py +43 -0
- attp_sdk-0.1.0/src/attp/loadbalancer/configs.py +7 -0
- attp_sdk-0.1.0/src/attp/loadbalancer/evaluator.py +30 -0
- attp_sdk-0.1.0/src/attp/loadbalancer/strategies/__init__.py +0 -0
- attp_sdk-0.1.0/src/attp/loadbalancer/strategies/round_robin.py +29 -0
- attp_sdk-0.1.0/src/attp/providers.py +322 -0
- attp_sdk-0.1.0/src/attp/server/__init__.py +6 -0
- attp_sdk-0.1.0/src/attp/server/abc/auth_strategy.py +13 -0
- attp_sdk-0.1.0/src/attp/server/attp_server.py +103 -0
- attp_sdk-0.1.0/src/attp/server/auth_hmac.py +162 -0
- attp_sdk-0.1.0/src/attp/server/configs.py +15 -0
- attp_sdk-0.1.0/src/attp/server/session_driver.py +143 -0
- attp_sdk-0.1.0/src/attp/shared/lifecycle_service.py +16 -0
- attp_sdk-0.1.0/src/attp/shared/limits.py +10 -0
- attp_sdk-0.1.0/src/attp/shared/multireceiver.py +87 -0
- attp_sdk-0.1.0/src/attp/shared/namespaces/dispatcher.py +46 -0
- attp_sdk-0.1.0/src/attp/shared/namespaces/router.py +115 -0
- attp_sdk-0.1.0/src/attp/shared/objects/dispatcher.py +106 -0
- attp_sdk-0.1.0/src/attp/shared/objects/eventbus.py +114 -0
- attp_sdk-0.1.0/src/attp/shared/objects/stream.py +27 -0
- attp_sdk-0.1.0/src/attp/shared/receiver.py +19 -0
- attp_sdk-0.1.0/src/attp/shared/secrets.py +73 -0
- attp_sdk-0.1.0/src/attp/shared/sessions/additional_mixins.py +140 -0
- attp_sdk-0.1.0/src/attp/shared/sessions/driver.py +197 -0
- attp_sdk-0.1.0/src/attp/shared/transmitter.py +250 -0
- attp_sdk-0.1.0/src/attp/shared/utils/ack_gate.py +87 -0
- attp_sdk-0.1.0/src/attp/shared/utils/callbacks.py +94 -0
- attp_sdk-0.1.0/src/attp/shared/utils/executor.py +130 -0
- attp_sdk-0.1.0/src/attp/shared/utils/qsequence.py +219 -0
- attp_sdk-0.1.0/src/attp/shared/utils/stream_receiver.py +39 -0
- attp_sdk-0.1.0/src/attp/types/context.py +26 -0
- attp_sdk-0.1.0/src/attp/types/exceptions/attp_exception.py +59 -0
- attp_sdk-0.1.0/src/attp/types/exceptions/load_balancer.py +14 -0
- attp_sdk-0.1.0/src/attp/types/exceptions/protocol_error.py +15 -0
- attp_sdk-0.1.0/src/attp/types/frame.py +68 -0
- attp_sdk-0.1.0/src/attp/types/frames/accepted.py +8 -0
- attp_sdk-0.1.0/src/attp/types/frames/auth.py +8 -0
- attp_sdk-0.1.0/src/attp/types/frames/error.py +19 -0
- attp_sdk-0.1.0/src/attp/types/frames/ready.py +13 -0
- attp_sdk-0.1.0/src/attp/types/frames/route_mapping.py +13 -0
- attp_sdk-0.1.0/src/attp/types/routes.py +25 -0
- attp_sdk-0.1.0/src/attp/types/streaming_signature.py +6 -0
attp_sdk-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: attp-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: attp-sdk
|
|
5
|
+
Author: Zahcoder34
|
|
6
|
+
Author-email: Zakhriw@gmail.com
|
|
7
|
+
Requires-Python: >=3.11,<3.14
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Requires-Dist: ascender-framework (>=2.0.4,<3.0.0)
|
|
13
|
+
Requires-Dist: attp-core (>=0.2.4,<0.3.0)
|
|
14
|
+
Requires-Dist: msgpack (>=1.1.2,<2.0.0)
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# Ascender Framework Project
|
|
18
|
+
|
|
19
|
+
## Overview
|
|
20
|
+
|
|
21
|
+
This project is built using the **Ascender Framework**, a structured FastAPI-based framework designed for modular and scalable applications.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Getting Started
|
|
26
|
+
|
|
27
|
+
### Prerequisites
|
|
28
|
+
|
|
29
|
+
Ensure you have the following dependencies installed:
|
|
30
|
+
|
|
31
|
+
- **Python 3.11 or higher**
|
|
32
|
+
- **Poetry** (for dependency management)
|
|
33
|
+
- **pip** (Python's package manager)
|
|
34
|
+
|
|
35
|
+
### Installation
|
|
36
|
+
|
|
37
|
+
1. **Install Poetry** (if not already installed):
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install poetry
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
2. **Configure Poetry to use virtual environments**:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
poetry config virtualenvs.create true
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
3. **Install project dependencies**:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
poetry install
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
4. **Run development server**:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
ascender run serve
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
If everything works, you're done!
|
attp_sdk-0.1.0/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Ascender Framework Project
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This project is built using the **Ascender Framework**, a structured FastAPI-based framework designed for modular and scalable applications.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Getting Started
|
|
10
|
+
|
|
11
|
+
### Prerequisites
|
|
12
|
+
|
|
13
|
+
Ensure you have the following dependencies installed:
|
|
14
|
+
|
|
15
|
+
- **Python 3.11 or higher**
|
|
16
|
+
- **Poetry** (for dependency management)
|
|
17
|
+
- **pip** (Python's package manager)
|
|
18
|
+
|
|
19
|
+
### Installation
|
|
20
|
+
|
|
21
|
+
1. **Install Poetry** (if not already installed):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install poetry
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
2. **Configure Poetry to use virtual environments**:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
poetry config virtualenvs.create true
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
3. **Install project dependencies**:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
poetry install
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
4. **Run development server**:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
ascender run serve
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
If everything works, you're done!
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "attp-sdk"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "attp-sdk"
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "Zahcoder34",email = "Zakhriw@gmail.com"},
|
|
7
|
+
{name = "AscenderTeam",email = "team@ascender.space"},
|
|
8
|
+
]
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11,<3.14"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"ascender-framework (>=2.0.4,<3.0.0)",
|
|
13
|
+
"attp-core (>=0.2.4,<0.3.0)",
|
|
14
|
+
"msgpack (>=1.1.2,<2.0.0)",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[tool.poetry]
|
|
18
|
+
package-mode = true
|
|
19
|
+
packages = [
|
|
20
|
+
{ include = "attp", from = "src" }
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
26
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from attp.decorators import AttpCall, AttpErrorHandler, AttpEvent, AttpLifecycle
|
|
2
|
+
from .providers import provideAttp
|
|
3
|
+
from .types.frame import AttpFrameDTO
|
|
4
|
+
from .types.context import AttpContext
|
|
5
|
+
from .shared.objects.stream import StreamObject
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
__all__ = ["provideAttp", "AttpFrameDTO", "AttpContext", "StreamObject", "AttpCall", "AttpEvent", "AttpLifecycle", "AttpErrorHandler"]
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from .authenticator import ConnectionAuthenticator
|
|
2
|
+
from .configs import ServiceDiscoveryConfigs, AttpClientConfigs
|
|
3
|
+
from .service_discovery import ServiceDiscovery
|
|
4
|
+
from .session_driver import ClientSessionDriver
|
|
5
|
+
|
|
6
|
+
__all__ = ["ConnectionAuthenticator", "ServiceDiscoveryConfigs", "AttpClientConfigs", "ServiceDiscovery", "ClientSessionDriver"]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import hmac
|
|
3
|
+
import os
|
|
4
|
+
import secrets
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Mapping
|
|
7
|
+
|
|
8
|
+
from attp.shared.secrets import resolve_secret_if_ref, parse_secret_ref, SecretRef
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ConnectionAuthenticator:
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
conn_uri: str,
|
|
15
|
+
namespace: str,
|
|
16
|
+
authorization: Any | None = None,
|
|
17
|
+
data: Any | None = None
|
|
18
|
+
) -> None:
|
|
19
|
+
self.authorization = authorization
|
|
20
|
+
self.conn_uri = conn_uri
|
|
21
|
+
self.namespace = namespace
|
|
22
|
+
self.data = data
|
|
23
|
+
self.AUTH_TIMEOUT = float(os.getenv("ATTP_CLIENT_CONNECTION_TIMEOUT", 10))
|
|
24
|
+
|
|
25
|
+
async def authenticate(self):
|
|
26
|
+
"""
|
|
27
|
+
Method is invoked when authentication process starts.
|
|
28
|
+
Usually server requires protocol authentication, and the connection authenticator signs the
|
|
29
|
+
"""
|
|
30
|
+
return resolve_secret_if_ref(self.authorization)
|
|
31
|
+
|
|
32
|
+
async def send_hello(self):
|
|
33
|
+
"""
|
|
34
|
+
Method is invoked only after authentication, to send the `data` given on `__init__` to the remote peer.
|
|
35
|
+
This method is mandatory for handshake part, for future authentications, it
|
|
36
|
+
"""
|
|
37
|
+
return self.data
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class HmacConnectionAuthenticator(ConnectionAuthenticator):
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
conn_uri: str,
|
|
44
|
+
namespace: str,
|
|
45
|
+
*,
|
|
46
|
+
secret: str | SecretRef | Mapping[str, Any],
|
|
47
|
+
node_id: str | None = None,
|
|
48
|
+
key_id: str | None = None,
|
|
49
|
+
ttl_seconds: int = 30,
|
|
50
|
+
max_clock_skew: int = 5,
|
|
51
|
+
) -> None:
|
|
52
|
+
super().__init__(conn_uri, namespace, authorization=None, data=None)
|
|
53
|
+
self._secret_ref = parse_secret_ref(secret)
|
|
54
|
+
if self._secret_ref is None:
|
|
55
|
+
raise TypeError("secret must be a string or secret reference.")
|
|
56
|
+
|
|
57
|
+
self.node_id = node_id or os.getenv("ATTP_NODE_ID") or "node"
|
|
58
|
+
self.key_id = key_id
|
|
59
|
+
self.ttl_seconds = ttl_seconds
|
|
60
|
+
self.max_clock_skew = max_clock_skew
|
|
61
|
+
self.AUTH_TIMEOUT = float(
|
|
62
|
+
os.getenv("ATTP_CLIENT_CONNECTION_TIMEOUT", ttl_seconds + max_clock_skew + 5)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def _sign(self, ts: int, nonce: str) -> str:
|
|
66
|
+
secret = self._secret_ref.resolve() # type: ignore
|
|
67
|
+
message = f"{self.namespace}:{self.node_id}:{ts}:{nonce}".encode("utf-8")
|
|
68
|
+
return hmac.new(secret.encode("utf-8"), message, hashlib.sha256).hexdigest()
|
|
69
|
+
|
|
70
|
+
async def authenticate(self):
|
|
71
|
+
ts = int(time.time())
|
|
72
|
+
nonce = secrets.token_hex(16)
|
|
73
|
+
sig = self._sign(ts, nonce)
|
|
74
|
+
|
|
75
|
+
payload = {
|
|
76
|
+
"alg": "HS256",
|
|
77
|
+
"ts": ts,
|
|
78
|
+
"nonce": nonce,
|
|
79
|
+
"sig": sig,
|
|
80
|
+
"node_id": self.node_id,
|
|
81
|
+
}
|
|
82
|
+
if self.key_id:
|
|
83
|
+
payload["kid"] = self.key_id
|
|
84
|
+
return payload
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from typing import Any, Callable
|
|
2
|
+
from ascender.common import BaseDTO
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
|
|
5
|
+
from attp.client.authenticator import ConnectionAuthenticator
|
|
6
|
+
from attp.shared.limits import AttpLimits
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AttpClientConfigs(BaseDTO):
|
|
10
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
11
|
+
|
|
12
|
+
remote_uri: str # attp://host:port
|
|
13
|
+
namespace: str = "default"
|
|
14
|
+
data: Any | None = None
|
|
15
|
+
authorization: Any | None = None
|
|
16
|
+
auth: Any | None = None
|
|
17
|
+
capabilities: list[str] = Field(default_factory=lambda: ["schema/msgpack", "streaming"])
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ServiceDiscoveryConfigs(BaseDTO):
|
|
21
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
22
|
+
|
|
23
|
+
peers: list[str | AttpClientConfigs]
|
|
24
|
+
limits: AttpLimits
|
|
25
|
+
authenticator: (
|
|
26
|
+
ConnectionAuthenticator
|
|
27
|
+
| type[ConnectionAuthenticator]
|
|
28
|
+
| Callable[[AttpClientConfigs], ConnectionAuthenticator]
|
|
29
|
+
| None
|
|
30
|
+
) = None
|
|
31
|
+
reconnection: bool = True
|
|
32
|
+
max_retries: int = 20
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from logging import Logger
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
from ascender.core import Inject
|
|
6
|
+
from attp.client.authenticator import ConnectionAuthenticator
|
|
7
|
+
from attp.client.configs import AttpClientConfigs, ServiceDiscoveryConfigs
|
|
8
|
+
from attp.client.session_driver import ClientSessionDriver
|
|
9
|
+
from attp.server.session_driver import ServerSessionDriver
|
|
10
|
+
from attp.shared.lifecycle_service import LifecycleService
|
|
11
|
+
|
|
12
|
+
from attp_core.rs_api import AttpClientSession, PyAttpMessage
|
|
13
|
+
|
|
14
|
+
from attp.shared.multireceiver import AttpMultiReceiver
|
|
15
|
+
from attp.shared.namespaces.dispatcher import NamespaceDispatcher
|
|
16
|
+
from attp.shared.objects.dispatcher import AttpFrameDispatcher
|
|
17
|
+
from attp.shared.sessions.driver import AttpSessionDriver
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ServiceDiscovery(LifecycleService):
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
configs: ServiceDiscoveryConfigs,
|
|
24
|
+
namespaces: NamespaceDispatcher,
|
|
25
|
+
dispatcher: AttpFrameDispatcher,
|
|
26
|
+
logger: Annotated[Logger, Inject("ASC_LOGGER")]
|
|
27
|
+
) -> None:
|
|
28
|
+
super().__init__()
|
|
29
|
+
self.configs = configs
|
|
30
|
+
|
|
31
|
+
self.multireceiver = AttpMultiReceiver[tuple[AttpSessionDriver, PyAttpMessage]](lambda d: d[0].namespace, fanout_global=True, auto_create=True)
|
|
32
|
+
self.namespaces = namespaces
|
|
33
|
+
self.dispatcher = dispatcher
|
|
34
|
+
|
|
35
|
+
self.conlock = asyncio.Lock()
|
|
36
|
+
|
|
37
|
+
self.logger = logger
|
|
38
|
+
self.is_active = False
|
|
39
|
+
|
|
40
|
+
def activate(self):
|
|
41
|
+
if not self.is_active:
|
|
42
|
+
self.logger.info("Activating service discovery...")
|
|
43
|
+
self.is_active = True
|
|
44
|
+
|
|
45
|
+
async def start_initial_connections(self):
|
|
46
|
+
for peer in self.configs.peers:
|
|
47
|
+
if isinstance(peer, str):
|
|
48
|
+
peer = AttpClientConfigs(remote_uri=peer)
|
|
49
|
+
|
|
50
|
+
authenticator = self.configs.authenticator
|
|
51
|
+
if isinstance(authenticator, type) and issubclass(authenticator, ConnectionAuthenticator):
|
|
52
|
+
authenticator = authenticator(peer.remote_uri, peer.namespace)
|
|
53
|
+
elif callable(authenticator):
|
|
54
|
+
authenticator = authenticator(peer)
|
|
55
|
+
try:
|
|
56
|
+
await self.initiate_connection(peer, conn_authenticator=authenticator)
|
|
57
|
+
except Exception as exc:
|
|
58
|
+
self.logger.error(
|
|
59
|
+
"[cyan]ATTP[/] ┆ Failed to connect to peer %s (%s)",
|
|
60
|
+
peer.remote_uri,
|
|
61
|
+
exc,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
async def on_startup(self):
|
|
65
|
+
asyncio.create_task(self.start_initial_connections())
|
|
66
|
+
|
|
67
|
+
async def on_shutdown(self):
|
|
68
|
+
self.dispatcher.stop_all()
|
|
69
|
+
|
|
70
|
+
async def on_session_termination(self, session_driver: ClientSessionDriver):
|
|
71
|
+
try:
|
|
72
|
+
self.namespaces.remove_session(session_driver.namespace, session_driver)
|
|
73
|
+
self.logger.info("[cyan]ATTP[/] ┆ Session (%s) disconnected.", session_driver.session_id or "unknown")
|
|
74
|
+
|
|
75
|
+
except ValueError:
|
|
76
|
+
self.logger.info("[cyan]ATTP[/] ┆ Session (%s) disconnected before being registered", session_driver._session or "unknown")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def initiate_connection(self, config: AttpClientConfigs, conn_authenticator: ConnectionAuthenticator | None = None):
|
|
80
|
+
client = AttpClientSession(config.remote_uri, limits=self.configs.limits.to_model())
|
|
81
|
+
|
|
82
|
+
client = await client.connect(max_retries=self.configs.max_retries)
|
|
83
|
+
|
|
84
|
+
if not client.session:
|
|
85
|
+
raise ConnectionError("Failed to connect to the server.")
|
|
86
|
+
|
|
87
|
+
driver = ClientSessionDriver(client.session, on_termination=self.on_session_termination)
|
|
88
|
+
|
|
89
|
+
if not conn_authenticator:
|
|
90
|
+
authenticator = ConnectionAuthenticator(config.remote_uri, config.namespace, config.authorization, config.data)
|
|
91
|
+
else:
|
|
92
|
+
authenticator = conn_authenticator
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
namespace, _ = await driver.start(config.capabilities, authenticator)
|
|
96
|
+
except TimeoutError:
|
|
97
|
+
self.logger.error("[cyan]ATTP[/] ┆ Session (%s) %s failed to authenticate, flushing the connection!", client.session.peername, client.session.session_id)
|
|
98
|
+
await driver.close()
|
|
99
|
+
del driver
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
async with self.conlock:
|
|
103
|
+
self.namespaces.add_session(namespace, driver)
|
|
104
|
+
receiver = self.multireceiver.receiver(namespace)
|
|
105
|
+
self.dispatcher.start(receiver)
|
|
106
|
+
|
|
107
|
+
await driver.listen(receiver)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import traceback
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
from ascender.core import inject
|
|
6
|
+
from typing_extensions import Doc
|
|
7
|
+
from attp.client.authenticator import ConnectionAuthenticator
|
|
8
|
+
from attp.shared.namespaces.router import AttpRouter
|
|
9
|
+
from attp.shared.sessions.additional_mixins import EnhancedFrameTransmitterMixin, StreamingFrameTransmitterMixin
|
|
10
|
+
from attp.shared.sessions.driver import SessionTerminatorMixin
|
|
11
|
+
|
|
12
|
+
from attp_core.rs_api import PyAttpMessage, AttpCommand
|
|
13
|
+
|
|
14
|
+
from attp.types.exceptions.attp_exception import AttpException
|
|
15
|
+
from attp.types.exceptions.protocol_error import ProtocolError
|
|
16
|
+
from attp.types.frames.accepted import IAcceptedDTO
|
|
17
|
+
from attp.types.frames.auth import IAuthDTO
|
|
18
|
+
from attp.types.frames.ready import IReadyDTO
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ClientSessionDriver(
|
|
22
|
+
SessionTerminatorMixin,
|
|
23
|
+
EnhancedFrameTransmitterMixin,
|
|
24
|
+
StreamingFrameTransmitterMixin
|
|
25
|
+
):
|
|
26
|
+
connection_estabilished_at: Annotated[str | None, Doc("Last connection time according to server's timezone in an ISO-format.")]
|
|
27
|
+
authenticator: ConnectionAuthenticator
|
|
28
|
+
router: AttpRouter = inject(AttpRouter)
|
|
29
|
+
|
|
30
|
+
async def start(self, capabilities: list, conn_authenticator: ConnectionAuthenticator):
|
|
31
|
+
asyncio.create_task(self.start_listener())
|
|
32
|
+
self._role = "client"
|
|
33
|
+
self._capabilities = capabilities
|
|
34
|
+
self._namespace = conn_authenticator.namespace
|
|
35
|
+
if os.getenv("ATTP_AUTH_DEBUG") == "1":
|
|
36
|
+
log = getattr(self.logger, "info", None)
|
|
37
|
+
if callable(log):
|
|
38
|
+
log(
|
|
39
|
+
"[cyan]ATTP[/] ┆ Sending AUTH namespace=%s",
|
|
40
|
+
conn_authenticator.namespace,
|
|
41
|
+
)
|
|
42
|
+
await self.send_frame(
|
|
43
|
+
PyAttpMessage(
|
|
44
|
+
route_id=1,
|
|
45
|
+
command_type=AttpCommand.AUTH,
|
|
46
|
+
correlation_id=None,
|
|
47
|
+
payload=IAuthDTO(
|
|
48
|
+
namespace=conn_authenticator.namespace,
|
|
49
|
+
data=await conn_authenticator.authenticate()
|
|
50
|
+
).mpd(),
|
|
51
|
+
version=self.version_bytes()
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
self.authenticator = conn_authenticator
|
|
55
|
+
try:
|
|
56
|
+
await asyncio.wait_for(self.auth_flag.wait(), timeout=conn_authenticator.AUTH_TIMEOUT)
|
|
57
|
+
except asyncio.TimeoutError:
|
|
58
|
+
raise TimeoutError("Authentication timed out for session {}".format(self.session_id))
|
|
59
|
+
|
|
60
|
+
return self.namespace, self.session_id
|
|
61
|
+
|
|
62
|
+
async def _on_event(self, events: list[PyAttpMessage]):
|
|
63
|
+
self.logger.debug(f"[cyan]ATTP[/] ┆ Received a new message from session {self.session_id}")
|
|
64
|
+
|
|
65
|
+
for event in events:
|
|
66
|
+
if event.route_id == 0 and event.command_type == AttpCommand.READY:
|
|
67
|
+
try:
|
|
68
|
+
if not event.payload:
|
|
69
|
+
continue
|
|
70
|
+
if os.getenv("ATTP_AUTH_DEBUG") == "1":
|
|
71
|
+
log = getattr(self.logger, "info", None)
|
|
72
|
+
if callable(log):
|
|
73
|
+
log("[cyan]ATTP[/] ┆ READY received")
|
|
74
|
+
self._enqueue_incoming(event)
|
|
75
|
+
await self._register_connection(IAcceptedDTO.mps(event.payload))
|
|
76
|
+
except Exception as e:
|
|
77
|
+
traceback.print_exc()
|
|
78
|
+
# TODO: Implement error reporting
|
|
79
|
+
await self.handle_disconnect()
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
elif event.command_type == AttpCommand.DISCONNECT:
|
|
83
|
+
await self.handle_disconnect()
|
|
84
|
+
return
|
|
85
|
+
elif event.command_type == AttpCommand.ERR:
|
|
86
|
+
self._enqueue_incoming(event)
|
|
87
|
+
return
|
|
88
|
+
else:
|
|
89
|
+
if self.is_authenticated:
|
|
90
|
+
self._enqueue_incoming(event)
|
|
91
|
+
|
|
92
|
+
async def _register_connection(self, frame: IAcceptedDTO):
|
|
93
|
+
super()._register_connection(frame)
|
|
94
|
+
try:
|
|
95
|
+
print("RRR", frame.routes)
|
|
96
|
+
self.router.include_remote_routes(self.namespace, frame.routes, "client")
|
|
97
|
+
except ProtocolError as e:
|
|
98
|
+
await self.send_error(0, exception=AttpException(400, message=str(e), detail=traceback.format_exc(), fatal=True))
|
|
99
|
+
await self.close()
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
self.auth_flag.set()
|
|
103
|
+
self.connection_estabilished_at = frame.server_time
|
|
104
|
+
|
|
105
|
+
routes = self.router.get_routes(namespace=self.namespace)
|
|
106
|
+
print("CLIENT REGISTERING ROUTES:", routes)
|
|
107
|
+
|
|
108
|
+
await self.send_frame(
|
|
109
|
+
PyAttpMessage(
|
|
110
|
+
route_id=0,
|
|
111
|
+
command_type=AttpCommand.READY,
|
|
112
|
+
correlation_id=None,
|
|
113
|
+
payload=IReadyDTO(
|
|
114
|
+
data=self.authenticator.data,
|
|
115
|
+
caps=self.capabilities,
|
|
116
|
+
routes=routes
|
|
117
|
+
).mpd(),
|
|
118
|
+
version=self.version_bytes()
|
|
119
|
+
)
|
|
120
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import Any, Callable
|
|
2
|
+
from ascender.core import ControllerDecoratorHook, inject
|
|
3
|
+
|
|
4
|
+
from attp.shared.namespaces.router import AttpRouter
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AttpCall(ControllerDecoratorHook):
|
|
8
|
+
router: AttpRouter = inject(AttpRouter)
|
|
9
|
+
|
|
10
|
+
def __init__(self, pattern: str, namespace: str = "default") -> None:
|
|
11
|
+
self.pattern = pattern
|
|
12
|
+
self.namespace = namespace
|
|
13
|
+
|
|
14
|
+
def on_load(self, callable: Callable[..., Any]):
|
|
15
|
+
self.router.add_route("message", self.pattern, callable, namespace=self.namespace)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from typing import Any, Callable
|
|
2
|
+
from ascender.core import ControllerDecoratorHook, inject
|
|
3
|
+
|
|
4
|
+
from attp.shared.namespaces.router import AttpRouter
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AttpEvent(ControllerDecoratorHook):
|
|
8
|
+
|
|
9
|
+
router: AttpRouter = inject(AttpRouter)
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
pattern: str,
|
|
14
|
+
namespace: str = "default"
|
|
15
|
+
) -> None:
|
|
16
|
+
self.pattern = pattern
|
|
17
|
+
self.namespace = namespace
|
|
18
|
+
|
|
19
|
+
def on_load(self, callable: Callable[..., Any]):
|
|
20
|
+
self.router.add_event(self.pattern, callable, namespace=self.namespace)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import Any, Callable
|
|
2
|
+
from ascender.core import ControllerDecoratorHook, inject
|
|
3
|
+
|
|
4
|
+
from attp.shared.namespaces.router import AttpRouter
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AttpErrorHandler(ControllerDecoratorHook):
|
|
8
|
+
router: AttpRouter = inject(AttpRouter)
|
|
9
|
+
|
|
10
|
+
def __init__(self, pattern: str, namespace: str = "default") -> None:
|
|
11
|
+
self.pattern = pattern
|
|
12
|
+
self.namespace = namespace
|
|
13
|
+
|
|
14
|
+
def on_load(self, callable: Callable[..., Any]):
|
|
15
|
+
self.router.add_error_handler(self.pattern, callable, namespace=self.namespace)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typing import Any, Callable, Literal
|
|
2
|
+
from ascender.core import ControllerDecoratorHook, inject
|
|
3
|
+
|
|
4
|
+
from attp.client.service_discovery import ServiceDiscovery
|
|
5
|
+
from attp.server.attp_server import AttpServer
|
|
6
|
+
from attp.shared.namespaces.router import AttpRouter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AttpLifecycle(ControllerDecoratorHook):
|
|
10
|
+
router: AttpRouter = inject(AttpRouter)
|
|
11
|
+
|
|
12
|
+
def __init__(self, event: Literal["connect", "disconnect"], namespace: str = "default") -> None:
|
|
13
|
+
self.event = event
|
|
14
|
+
self.namespace = namespace
|
|
15
|
+
|
|
16
|
+
def on_load(self, callable: Callable[..., Any]):
|
|
17
|
+
inject(AttpServer).activate()
|
|
18
|
+
inject(ServiceDiscovery).activate()
|
|
19
|
+
self.router.add_route(self.event, self.event, callable, namespace=self.namespace) # type: ignore
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from .balancer import AttpLoadBalancer
|
|
2
|
+
from .configs import BalancerConfigs
|
|
3
|
+
from .abc.cacher import StrategyCacher
|
|
4
|
+
from .abc.candidate import Candidate
|
|
5
|
+
from .abc.strategy import BalancingStrategy
|
|
6
|
+
|
|
7
|
+
__all__ = ["AttpLoadBalancer", "BalancerConfigs", "StrategyCacher", "Candidate", "BalancingStrategy"]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any, TypeVar
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
T = TypeVar("T")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class StrategyCacher(ABC):
|
|
9
|
+
|
|
10
|
+
@abstractmethod
|
|
11
|
+
async def store(self, key: str, value: Any) -> Any:
|
|
12
|
+
...
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
async def get(self, key: str, *, expected_type: type[T] | None = None) -> T | Any:
|
|
16
|
+
...
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
async def increment(self, key: str, *, delta: int = 1, initial: int = 0) -> int:
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
async def keys(self):
|
|
23
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from attp.loadbalancer.abc.cacher import StrategyCacher
|
|
5
|
+
from attp.loadbalancer.abc.candidate import Candidate
|
|
6
|
+
from attp.shared.utils.qsequence import QSequence
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BalancingStrategy(ABC):
|
|
10
|
+
name: str
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
configs: Any,
|
|
16
|
+
cacher: StrategyCacher,
|
|
17
|
+
) -> None:
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
async def balance(self, default: Candidate, candidates: QSequence[Candidate]) -> Candidate:
|
|
22
|
+
pass
|