cremalink 0.1.0b5__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.
- cremalink/__init__.py +33 -0
- cremalink/clients/__init__.py +10 -0
- cremalink/clients/cloud.py +130 -0
- cremalink/core/__init__.py +6 -0
- cremalink/core/binary.py +102 -0
- cremalink/crypto/__init__.py +142 -0
- cremalink/devices/AY008ESP1.json +114 -0
- cremalink/devices/__init__.py +116 -0
- cremalink/domain/__init__.py +11 -0
- cremalink/domain/device.py +245 -0
- cremalink/domain/factory.py +98 -0
- cremalink/local_server.py +76 -0
- cremalink/local_server_app/__init__.py +20 -0
- cremalink/local_server_app/api.py +272 -0
- cremalink/local_server_app/config.py +64 -0
- cremalink/local_server_app/device_adapter.py +96 -0
- cremalink/local_server_app/jobs.py +104 -0
- cremalink/local_server_app/logging.py +116 -0
- cremalink/local_server_app/models.py +76 -0
- cremalink/local_server_app/protocol.py +135 -0
- cremalink/local_server_app/state.py +358 -0
- cremalink/parsing/__init__.py +7 -0
- cremalink/parsing/monitor/__init__.py +22 -0
- cremalink/parsing/monitor/decode.py +79 -0
- cremalink/parsing/monitor/extractors.py +69 -0
- cremalink/parsing/monitor/frame.py +132 -0
- cremalink/parsing/monitor/model.py +42 -0
- cremalink/parsing/monitor/profile.py +144 -0
- cremalink/parsing/monitor/view.py +196 -0
- cremalink/parsing/properties/__init__.py +9 -0
- cremalink/parsing/properties/decode.py +53 -0
- cremalink/resources/__init__.py +10 -0
- cremalink/resources/api_config.json +14 -0
- cremalink/resources/api_config.py +30 -0
- cremalink/resources/lang.json +223 -0
- cremalink/transports/__init__.py +7 -0
- cremalink/transports/base.py +94 -0
- cremalink/transports/cloud/__init__.py +9 -0
- cremalink/transports/cloud/transport.py +166 -0
- cremalink/transports/local/__init__.py +9 -0
- cremalink/transports/local/transport.py +164 -0
- cremalink-0.1.0b5.dist-info/METADATA +138 -0
- cremalink-0.1.0b5.dist-info/RECORD +47 -0
- cremalink-0.1.0b5.dist-info/WHEEL +5 -0
- cremalink-0.1.0b5.dist-info/entry_points.txt +2 -0
- cremalink-0.1.0b5.dist-info/licenses/LICENSE +661 -0
- cremalink-0.1.0b5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines the core `Device` class, which represents a physical coffee
|
|
3
|
+
machine. It serves as the primary high-level interface for interacting with a
|
|
4
|
+
device, abstracting away the underlying transport and command details.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import time
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from base64 import b64encode
|
|
12
|
+
from typing import Any, Dict, Optional
|
|
13
|
+
|
|
14
|
+
from cremalink.parsing.monitor.frame import MonitorFrame
|
|
15
|
+
from cremalink.parsing.monitor.model import MonitorSnapshot
|
|
16
|
+
from cremalink.parsing.monitor.profile import MonitorProfile
|
|
17
|
+
from cremalink.parsing.monitor.view import MonitorView
|
|
18
|
+
from cremalink.transports.base import DeviceTransport
|
|
19
|
+
from cremalink.devices import device_map
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _load_device_map(device_map_path: Optional[str]) -> Dict[str, Any]:
|
|
23
|
+
"""Loads a JSON device map from the given file path."""
|
|
24
|
+
if not device_map_path:
|
|
25
|
+
return {}
|
|
26
|
+
with open(device_map_path, "r", encoding="utf-8") as f:
|
|
27
|
+
data = json.load(f)
|
|
28
|
+
return data if isinstance(data, dict) else {}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _encode_command(hex_command: str) -> str:
|
|
32
|
+
"""
|
|
33
|
+
Encodes a hexadecimal command string into the base64 format expected by the device.
|
|
34
|
+
It prepends the command bytes with a current timestamp.
|
|
35
|
+
"""
|
|
36
|
+
head = bytearray.fromhex(hex_command)
|
|
37
|
+
timestamp = bytearray.fromhex(hex(int(time.time()))[2:])
|
|
38
|
+
return b64encode(head + timestamp).decode("utf-8")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class Device:
|
|
43
|
+
"""
|
|
44
|
+
Represents a coffee machine, providing methods to control and monitor it.
|
|
45
|
+
|
|
46
|
+
This class holds the device's state (e.g., IP, model) and uses a `DeviceTransport`
|
|
47
|
+
object to handle the actual communication. Device-specific capabilities are
|
|
48
|
+
loaded from a "device map" file.
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
transport: The transport object responsible for communication.
|
|
52
|
+
dsn: Device Serial Number.
|
|
53
|
+
model: The model identifier of the device.
|
|
54
|
+
nickname: A user-defined name for the device.
|
|
55
|
+
ip: The local IP address of the device.
|
|
56
|
+
lan_key: The key used for LAN-based authentication.
|
|
57
|
+
scheme: The communication scheme (e.g., 'http', 'mqtt').
|
|
58
|
+
is_online: Boolean indicating if the device is currently reachable.
|
|
59
|
+
last_seen: Timestamp of the last communication.
|
|
60
|
+
firmware: The device's firmware version.
|
|
61
|
+
serial: The device's serial number.
|
|
62
|
+
coffee_count: The total number of coffees made.
|
|
63
|
+
command_map: A mapping of command aliases to their hex codes.
|
|
64
|
+
property_map: A mapping of property aliases to their technical names.
|
|
65
|
+
monitor_profile: Configuration for parsing monitor data.
|
|
66
|
+
extra: A dictionary for any other miscellaneous data.
|
|
67
|
+
"""
|
|
68
|
+
transport: DeviceTransport
|
|
69
|
+
dsn: Optional[str] = None
|
|
70
|
+
model: Optional[str] = None
|
|
71
|
+
nickname: Optional[str] = None
|
|
72
|
+
ip: Optional[str] = None
|
|
73
|
+
lan_key: Optional[str] = None
|
|
74
|
+
scheme: Optional[str] = None
|
|
75
|
+
is_online: Optional[bool] = None
|
|
76
|
+
last_seen: Optional[str] = None
|
|
77
|
+
firmware: Optional[str] = None
|
|
78
|
+
serial: Optional[str] = None
|
|
79
|
+
coffee_count: Optional[int] = None
|
|
80
|
+
command_map: Dict[str, Any] = field(default_factory=dict)
|
|
81
|
+
property_map: Dict[str, Any] = field(default_factory=dict)
|
|
82
|
+
monitor_profile: MonitorProfile = field(default_factory=MonitorProfile)
|
|
83
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def from_map(
|
|
87
|
+
cls,
|
|
88
|
+
transport: DeviceTransport,
|
|
89
|
+
device_map_path: Optional[str] = None,
|
|
90
|
+
**kwargs,
|
|
91
|
+
) -> "Device":
|
|
92
|
+
"""
|
|
93
|
+
Factory method to create a Device instance with a loaded device map.
|
|
94
|
+
|
|
95
|
+
If `device_map_path` is not provided, it attempts to find one using the
|
|
96
|
+
device's model.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
transport: The communication transport to use.
|
|
100
|
+
device_map_path: Optional path to the device map JSON file.
|
|
101
|
+
**kwargs: Additional attributes to set on the Device instance.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
A configured Device instance.
|
|
105
|
+
"""
|
|
106
|
+
if not device_map_path:
|
|
107
|
+
device_map_path = device_map(cls.model) if cls.model else None
|
|
108
|
+
|
|
109
|
+
map_data = _load_device_map(device_map_path)
|
|
110
|
+
command_map = map_data.get("command_map", {}) if isinstance(map_data, dict) else {}
|
|
111
|
+
property_map = map_data.get("property_map", {}) if isinstance(map_data, dict) else {}
|
|
112
|
+
monitor_profile_data = map_data.get("monitor_profile", {}) if isinstance(map_data, dict) else {}
|
|
113
|
+
monitor_profile = MonitorProfile.from_dict(monitor_profile_data)
|
|
114
|
+
|
|
115
|
+
# If the transport supports it, pass the mappings to it.
|
|
116
|
+
if hasattr(transport, "set_mappings"):
|
|
117
|
+
try:
|
|
118
|
+
transport.set_mappings(command_map, property_map)
|
|
119
|
+
except Exception:
|
|
120
|
+
pass # Ignore if setting mappings fails
|
|
121
|
+
|
|
122
|
+
return cls(
|
|
123
|
+
transport=transport,
|
|
124
|
+
command_map=command_map,
|
|
125
|
+
property_map=property_map,
|
|
126
|
+
monitor_profile=monitor_profile,
|
|
127
|
+
**kwargs,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# --- Transport delegations ---
|
|
131
|
+
def configure(self) -> None:
|
|
132
|
+
"""Configures the underlying transport."""
|
|
133
|
+
self.transport.configure()
|
|
134
|
+
|
|
135
|
+
def send_command(self, command: str) -> Any:
|
|
136
|
+
"""
|
|
137
|
+
Encodes and sends a raw hex command to the device via the transport.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
command: The hex command string to send.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
The response from the transport.
|
|
144
|
+
"""
|
|
145
|
+
encoded = _encode_command(command)
|
|
146
|
+
return self.transport.send_command(encoded)
|
|
147
|
+
|
|
148
|
+
def refresh_monitor(self) -> Any:
|
|
149
|
+
"""Requests a refresh of the device's monitoring data."""
|
|
150
|
+
return self.transport.refresh_monitor()
|
|
151
|
+
|
|
152
|
+
def get_properties(self) -> Any:
|
|
153
|
+
"""Fetches all available properties from the device."""
|
|
154
|
+
return self.transport.get_properties()
|
|
155
|
+
|
|
156
|
+
def get_property_aliases(self) -> list[str]:
|
|
157
|
+
"""Returns a list of all available property aliases from the device map."""
|
|
158
|
+
return list(self.property_map.keys())
|
|
159
|
+
|
|
160
|
+
def get_property(self, name: str) -> Any:
|
|
161
|
+
"""
|
|
162
|
+
Fetches a single property by its alias or technical name.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
name: The alias or name of the property to fetch.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
The value of the requested property.
|
|
169
|
+
"""
|
|
170
|
+
actual_name = self.resolve_property(name, default=name)
|
|
171
|
+
return self.transport.get_property(actual_name)
|
|
172
|
+
|
|
173
|
+
def health(self) -> Any:
|
|
174
|
+
"""Checks the health of the device connection."""
|
|
175
|
+
return self.transport.health()
|
|
176
|
+
|
|
177
|
+
# --- Command map helpers ---
|
|
178
|
+
def do(self, drink_name: str) -> Any:
|
|
179
|
+
"""
|
|
180
|
+
Executes a command by its friendly name (e.g., 'espresso').
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
drink_name: The name of the command to execute, as defined in the command_map.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
The response from the transport.
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
ValueError: If the command name is not found in the device map.
|
|
190
|
+
"""
|
|
191
|
+
key = drink_name.lower().strip()
|
|
192
|
+
hex_command = self.command_map.get(key, {}).get("command")
|
|
193
|
+
if not hex_command:
|
|
194
|
+
raise ValueError(f"Command '{key}' not implemented; check device_map.")
|
|
195
|
+
return self.send_command(hex_command)
|
|
196
|
+
|
|
197
|
+
def get_commands(self) -> list[str]:
|
|
198
|
+
"""Returns a list of all available command names from the device map."""
|
|
199
|
+
return list(self.command_map.keys())
|
|
200
|
+
|
|
201
|
+
# --- Property map helpers ---
|
|
202
|
+
def resolve_property(self, alias: str, default: Optional[str] = None) -> str:
|
|
203
|
+
"""
|
|
204
|
+
Translates a property alias to its technical name using the property_map.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
alias: The property alias to resolve.
|
|
208
|
+
default: A default value to return if the alias is not found.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
The resolved technical name, or the alias/default if not found.
|
|
212
|
+
"""
|
|
213
|
+
return self.property_map.get(alias, default or alias)
|
|
214
|
+
|
|
215
|
+
# --- Monitor helpers ---
|
|
216
|
+
def get_monitor_snapshot(self) -> MonitorSnapshot:
|
|
217
|
+
"""
|
|
218
|
+
Retrieves the latest raw monitoring data from the transport.
|
|
219
|
+
"""
|
|
220
|
+
return self.transport.get_monitor()
|
|
221
|
+
|
|
222
|
+
def get_monitor(self) -> MonitorView:
|
|
223
|
+
"""
|
|
224
|
+
Retrieves and parses monitoring data into a structured, human-readable view.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
A `MonitorView` instance containing parsed status information.
|
|
228
|
+
"""
|
|
229
|
+
snapshot = self.get_monitor_snapshot()
|
|
230
|
+
return MonitorView(snapshot=snapshot, profile=self.monitor_profile)
|
|
231
|
+
|
|
232
|
+
def get_monitor_frame(self) -> Optional[MonitorFrame]:
|
|
233
|
+
"""
|
|
234
|
+
Decodes the raw monitor data into a `MonitorFrame` for low-level analysis.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
A `MonitorFrame` if decoding is successful, otherwise None.
|
|
238
|
+
"""
|
|
239
|
+
snapshot = self.get_monitor_snapshot()
|
|
240
|
+
if not snapshot.raw_b64:
|
|
241
|
+
return None
|
|
242
|
+
try:
|
|
243
|
+
return MonitorFrame.from_b64(snapshot.raw_b64)
|
|
244
|
+
except Exception:
|
|
245
|
+
return None
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides factory functions for creating `Device` instances.
|
|
3
|
+
|
|
4
|
+
These factories simplify the process of instantiating a `Device` by handling
|
|
5
|
+
the setup of the appropriate communication transport (`LocalTransport` or
|
|
6
|
+
`CloudTransport`) and the subsequent creation of the `Device` object itself.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from cremalink.domain.device import Device
|
|
13
|
+
from cremalink.transports.cloud.transport import CloudTransport
|
|
14
|
+
from cremalink.transports.local.transport import LocalTransport
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_local_device(
|
|
18
|
+
dsn: str,
|
|
19
|
+
lan_key: str,
|
|
20
|
+
device_ip: Optional[str],
|
|
21
|
+
server_host: str,
|
|
22
|
+
server_port: int = 10280,
|
|
23
|
+
device_scheme: str = "http",
|
|
24
|
+
auto_configure: bool = True,
|
|
25
|
+
device_map_path: Optional[str] = None,
|
|
26
|
+
) -> Device:
|
|
27
|
+
"""
|
|
28
|
+
Creates a `Device` instance configured for local network communication.
|
|
29
|
+
|
|
30
|
+
This factory sets up a `LocalTransport` which requires details about the
|
|
31
|
+
local network environment, including the device's IP and the local server's
|
|
32
|
+
address.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
dsn: The Device Serial Number.
|
|
36
|
+
lan_key: The key for authenticating on the local network.
|
|
37
|
+
device_ip: The IP address of the coffee machine.
|
|
38
|
+
server_host: The hostname or IP address of the local proxy server.
|
|
39
|
+
server_port: The port of the local proxy server.
|
|
40
|
+
device_scheme: The communication protocol to use with the device (e.g., 'http').
|
|
41
|
+
auto_configure: If True, the transport will be configured automatically.
|
|
42
|
+
device_map_path: Optional path to a device-specific command map file.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
A `Device` instance configured with a `LocalTransport`.
|
|
46
|
+
"""
|
|
47
|
+
transport = LocalTransport(
|
|
48
|
+
dsn=dsn,
|
|
49
|
+
lan_key=lan_key,
|
|
50
|
+
device_ip=device_ip,
|
|
51
|
+
server_host=server_host,
|
|
52
|
+
server_port=server_port,
|
|
53
|
+
device_scheme=device_scheme,
|
|
54
|
+
auto_configure=auto_configure,
|
|
55
|
+
)
|
|
56
|
+
# After transport initialization, some device attributes might be populated.
|
|
57
|
+
# We pass these to the Device constructor.
|
|
58
|
+
return Device.from_map(
|
|
59
|
+
transport=transport,
|
|
60
|
+
device_map_path=device_map_path,
|
|
61
|
+
dsn=dsn,
|
|
62
|
+
ip=device_ip,
|
|
63
|
+
lan_key=lan_key,
|
|
64
|
+
scheme=device_scheme,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def create_cloud_device(
|
|
69
|
+
dsn: str,
|
|
70
|
+
access_token: str,
|
|
71
|
+
device_map_path: Optional[str] = None,
|
|
72
|
+
) -> Device:
|
|
73
|
+
"""
|
|
74
|
+
Creates a `Device` instance configured for cloud-based communication.
|
|
75
|
+
|
|
76
|
+
This factory sets up a `CloudTransport`, which communicates with the device
|
|
77
|
+
via the manufacturer's cloud services, requiring an access token.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
dsn: The Device Serial Number.
|
|
81
|
+
access_token: The authentication token for the cloud service.
|
|
82
|
+
device_map_path: Optional path to a device-specific command map file.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
A `Device` instance configured with a `CloudTransport`.
|
|
86
|
+
"""
|
|
87
|
+
transport = CloudTransport(dsn=dsn, access_token=access_token, device_map_path=device_map_path)
|
|
88
|
+
# After transport initialization, some device attributes might be populated.
|
|
89
|
+
# We pass these to the Device constructor.
|
|
90
|
+
return Device.from_map(
|
|
91
|
+
transport=transport,
|
|
92
|
+
device_map_path=device_map_path,
|
|
93
|
+
dsn=dsn,
|
|
94
|
+
model=getattr(transport, "model", None),
|
|
95
|
+
ip=getattr(transport, "ip", None),
|
|
96
|
+
lan_key=getattr(transport, "lan_key", None),
|
|
97
|
+
is_online=getattr(transport, "is_online", None),
|
|
98
|
+
)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This script is the main entry point for running the local proxy server.
|
|
3
|
+
|
|
4
|
+
The local server acts as an intermediary between the `cremalink` library and a
|
|
5
|
+
coffee machine on the local network. It exposes a simple HTTP API that the
|
|
6
|
+
`LocalTransport` can use, and it handles the complexities of direct device
|
|
7
|
+
communication, including authentication and command formatting.
|
|
8
|
+
|
|
9
|
+
This script can be run directly from the command line, for example:
|
|
10
|
+
`cremalink-server --ip 127.0.0.1 --port 10280`
|
|
11
|
+
"""
|
|
12
|
+
import argparse
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
import uvicorn
|
|
16
|
+
|
|
17
|
+
from cremalink.local_server_app import create_app, ServerSettings
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LocalServer:
|
|
21
|
+
"""A wrapper class that encapsulates the Uvicorn server and the web application."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, settings: ServerSettings) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Initializes the server.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
settings: A `ServerSettings` object containing configuration like
|
|
29
|
+
host and port.
|
|
30
|
+
"""
|
|
31
|
+
self.settings = settings
|
|
32
|
+
self.app = create_app(settings=self.settings)
|
|
33
|
+
|
|
34
|
+
def start(self) -> None:
|
|
35
|
+
"""Starts the Uvicorn server to serve the application."""
|
|
36
|
+
uvicorn.run(
|
|
37
|
+
self.app,
|
|
38
|
+
host=self.settings.server_ip,
|
|
39
|
+
port=self.settings.server_port,
|
|
40
|
+
log_level="info"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main():
|
|
45
|
+
"""
|
|
46
|
+
Parses command-line arguments and starts the local server.
|
|
47
|
+
"""
|
|
48
|
+
parser = argparse.ArgumentParser(description="Start the cremalink local proxy server.")
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--ip",
|
|
51
|
+
type=str,
|
|
52
|
+
default="127.0.0.1",
|
|
53
|
+
help="IP address to bind the local server to."
|
|
54
|
+
)
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
"--port",
|
|
57
|
+
type=int,
|
|
58
|
+
default=10280,
|
|
59
|
+
help="Port to run the local server on."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Manually handle --help to avoid argument parsing errors with unknown args.
|
|
63
|
+
if "--help" in sys.argv:
|
|
64
|
+
parser.print_help()
|
|
65
|
+
sys.exit(0)
|
|
66
|
+
|
|
67
|
+
args = parser.parse_args()
|
|
68
|
+
settings = ServerSettings(server_ip=args.ip, server_port=args.port)
|
|
69
|
+
server = LocalServer(settings)
|
|
70
|
+
print(f"Starting cremalink local server on http://{args.ip}:{args.port}...")
|
|
71
|
+
server.start()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
# This allows the script to be executed directly.
|
|
76
|
+
sys.exit(main())
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This package contains the core implementation of the cremalink local proxy server,
|
|
3
|
+
which is a FastAPI application.
|
|
4
|
+
|
|
5
|
+
It exposes the main application factory `create_app` and the `ServerSettings`
|
|
6
|
+
class for configuration.
|
|
7
|
+
"""
|
|
8
|
+
from cremalink.local_server_app.api import create_app
|
|
9
|
+
from cremalink.local_server_app.config import ServerSettings
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def __getattr__(name):
|
|
13
|
+
if name == "create_app":
|
|
14
|
+
return create_app
|
|
15
|
+
if name == "ServerSettings":
|
|
16
|
+
return ServerSettings
|
|
17
|
+
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = ["create_app", "ServerSettings"]
|