Put the deployment-only dependencies behind a server extra
A data-science environment installing fluksio waited for lxml, aiohttp and the rest of a connector stack it has nothing to talk to. Outbound mail, error reporting and the MQTT and InfluxDB clients moved to `fluksio[server]`, which the image installs; each import is guarded and names the extra. `tenacity` had no import site at all and is gone. 23 fewer packages and the compiled ones among them — a bare `pip install fluksio` still serves, runs every python node, and registers the mqtt and influxdb node types, which only need the library when one is actually built. sentry-sdk arrives anyway underneath `fastapi[standard]`; what changed there is that nothing of ours requires it. The dev environment keeps every extra: the suite exercises the connectors and strict mypy checks their call sites. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Hra4ndWMCLU5F3KjUuVAc
This commit is contained in:
@@ -14,6 +14,22 @@ from fluksio.flow.nodes.base import Node, NodeResult
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _influxdb() -> Any:
|
||||
"""The client library, which is a `fluksio[server]` extra.
|
||||
|
||||
Imported per use rather than at module level, because the node type is
|
||||
registered at boot and an installation with no InfluxDB behind it should
|
||||
not have to carry the library to start.
|
||||
"""
|
||||
try:
|
||||
import influxdb_client
|
||||
except ImportError:
|
||||
raise RuntimeError(
|
||||
"the influxdb node needs the server extra: pip install 'fluksio[server]'"
|
||||
) from None
|
||||
return influxdb_client
|
||||
|
||||
|
||||
class InfluxDbNode(Node):
|
||||
"""
|
||||
InfluxDB node for writing to and reading from InfluxDB.
|
||||
@@ -290,7 +306,7 @@ class InfluxDbNode(Node):
|
||||
:returns: ``{"rows": [...], **echo}``.
|
||||
:rtype: dict
|
||||
"""
|
||||
from influxdb_client import InfluxDBClient
|
||||
InfluxDBClient = _influxdb().InfluxDBClient
|
||||
|
||||
flux = str(request["flux"])
|
||||
echo = {key: value for key, value in request.items() if key != "flux"}
|
||||
@@ -338,8 +354,10 @@ class InfluxDbNode(Node):
|
||||
- Dict with "value" and "tags" keys: Value written with merged tags
|
||||
:type data: dict
|
||||
"""
|
||||
from influxdb_client import InfluxDBClient, Point, WritePrecision
|
||||
from influxdb_client.client.write_api import SYNCHRONOUS
|
||||
influxdb_client = _influxdb()
|
||||
InfluxDBClient = influxdb_client.InfluxDBClient
|
||||
Point, WritePrecision = influxdb_client.Point, influxdb_client.WritePrecision
|
||||
SYNCHRONOUS = influxdb_client.client.write_api.SYNCHRONOUS
|
||||
|
||||
try:
|
||||
with InfluxDBClient(
|
||||
@@ -438,7 +456,7 @@ class InfluxDbNode(Node):
|
||||
:returns: Dict of port name to queried value.
|
||||
:rtype: dict
|
||||
"""
|
||||
from influxdb_client import InfluxDBClient
|
||||
InfluxDBClient = _influxdb().InfluxDBClient
|
||||
|
||||
results = {}
|
||||
|
||||
|
||||
@@ -20,6 +20,22 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _aiomqtt() -> Any:
|
||||
"""The client library, which is a `fluksio[server]` extra.
|
||||
|
||||
Imported per use rather than at module level, because the node type is
|
||||
registered at boot and an installation that talks to no broker should not
|
||||
have to carry the library to start.
|
||||
"""
|
||||
try:
|
||||
import aiomqtt
|
||||
except ImportError:
|
||||
raise RuntimeError(
|
||||
"the mqtt node needs the server extra: pip install 'fluksio[server]'"
|
||||
) from None
|
||||
return aiomqtt
|
||||
|
||||
|
||||
def topic_matches(filter_: str, topic: str) -> bool:
|
||||
"""Does an MQTT topic filter cover this topic?
|
||||
|
||||
@@ -382,7 +398,7 @@ class MqttNode(Node):
|
||||
A dropped connection raises, and the supervisor decides when to
|
||||
reconnect — the same arrangement the subscriber uses.
|
||||
"""
|
||||
import aiomqtt
|
||||
aiomqtt = _aiomqtt()
|
||||
|
||||
queue = self._publish_queue
|
||||
if queue is None:
|
||||
@@ -408,7 +424,7 @@ class MqttNode(Node):
|
||||
|
||||
async def _publish_once(self, data: dict[str, Any]) -> None:
|
||||
"""Connect, publish, disconnect — the unstarted node's path."""
|
||||
import aiomqtt
|
||||
aiomqtt = _aiomqtt()
|
||||
|
||||
async with aiomqtt.Client(
|
||||
hostname=self.broker_host,
|
||||
@@ -570,7 +586,7 @@ class MqttNode(Node):
|
||||
"""
|
||||
import json
|
||||
|
||||
import aiomqtt
|
||||
aiomqtt = _aiomqtt()
|
||||
|
||||
if not (self._stop_event and self._stop_event.is_set()):
|
||||
try:
|
||||
|
||||
@@ -4,7 +4,6 @@ import logging
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import AbstractAsyncContextManager, asynccontextmanager
|
||||
|
||||
import sentry_sdk
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
from fastapi.responses import JSONResponse
|
||||
@@ -51,6 +50,10 @@ def custom_generate_unique_id(route: APIRoute) -> str:
|
||||
|
||||
|
||||
if settings.SENTRY_DSN and settings.ENVIRONMENT != "local":
|
||||
# Imported here rather than at the top: it is a `fluksio[server]` extra, so
|
||||
# a pip install without one has no sentry to import — and no DSN either.
|
||||
import sentry_sdk
|
||||
|
||||
# `enable_tracing` was removed in sentry-sdk 2.x; this is what it meant.
|
||||
sentry_sdk.init(dsn=str(settings.SENTRY_DSN), traces_sample_rate=1.0)
|
||||
|
||||
|
||||
@@ -485,14 +485,14 @@ def _params(definition: dict[str, Any], rest: list[str]) -> dict[str, Any]:
|
||||
f"{', '.join(sorted(types)) or 'none'}){hint}"
|
||||
)
|
||||
params: dict[str, Any] = {}
|
||||
for name, value in raw.items():
|
||||
if value is True:
|
||||
params[name] = True
|
||||
for key, written in raw.items():
|
||||
if isinstance(written, bool):
|
||||
params[key] = written
|
||||
continue
|
||||
try:
|
||||
params[name] = _coerce(value, types[name])
|
||||
params[key] = _coerce(written, types[key])
|
||||
except ValueError as exc:
|
||||
raise SyncError(f"'{name}' takes {types[name]}: {exc}") from exc
|
||||
raise SyncError(f"'{key}' takes {types[key]}: {exc}") from exc
|
||||
return params
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import emails # type: ignore
|
||||
import jwt
|
||||
from jinja2 import Template
|
||||
from jwt.exceptions import InvalidTokenError
|
||||
@@ -37,6 +36,12 @@ def send_email(
|
||||
html_content: str = "",
|
||||
) -> None:
|
||||
assert settings.emails_enabled, "no provided configuration for email variables"
|
||||
try:
|
||||
import emails # type: ignore
|
||||
except ImportError:
|
||||
raise RuntimeError(
|
||||
"sending mail needs the server extra: pip install 'fluksio[server]'"
|
||||
) from None
|
||||
message = emails.Message(
|
||||
subject=subject,
|
||||
html=html_content,
|
||||
|
||||
Reference in New Issue
Block a user