lange-python 0.3.31__tar.gz → 0.5.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.
Files changed (51) hide show
  1. lange_python-0.5.0/PKG-INFO +74 -0
  2. lange_python-0.5.0/README.md +53 -0
  3. lange_python-0.5.0/lange/__init__.py +4 -0
  4. {lange_python-0.3.31 → lange_python-0.5.0}/lange/cli/__init__.py +4 -0
  5. lange_python-0.5.0/lange/cli/_create.py +61 -0
  6. lange_python-0.5.0/lange/cli/_init.py +22 -0
  7. lange_python-0.5.0/lange/contracts/__init__.py +7 -0
  8. lange_python-0.5.0/lange/contracts/mesh/__init__.py +29 -0
  9. lange_python-0.5.0/lange/contracts/mesh/_message.py +32 -0
  10. lange_python-0.5.0/lange/contracts/mesh/ai/__init__.py +23 -0
  11. lange_python-0.5.0/lange/contracts/mesh/ai/ai_model.py +73 -0
  12. lange_python-0.5.0/lange/contracts/mesh/ai/chat.py +12 -0
  13. lange_python-0.5.0/lange/contracts/mesh/ai/embeddings.py +9 -0
  14. lange_python-0.5.0/lange/contracts/mesh/ai/worker_config.py +16 -0
  15. lange_python-0.5.0/lange/contracts/mesh/relay/__init__.py +9 -0
  16. lange_python-0.5.0/lange/contracts/mesh/relay/_rest.py +41 -0
  17. lange_python-0.5.0/lange/contracts/mesh/relay/_worker_config.py +31 -0
  18. {lange_python-0.3.31 → lange_python-0.5.0}/lange/distribution/__init__.py +0 -4
  19. lange_python-0.5.0/lange/mesh/__init__.py +5 -0
  20. lange_python-0.5.0/lange/mesh/relay/__init__.py +18 -0
  21. lange_python-0.5.0/lange/mesh/relay/client/__init__.py +5 -0
  22. lange_python-0.5.0/lange/mesh/relay/client/_client.py +593 -0
  23. {lange_python-0.3.31 → lange_python-0.5.0}/pyproject.toml +5 -3
  24. lange_python-0.3.31/PKG-INFO +0 -95
  25. lange_python-0.3.31/README.md +0 -76
  26. lange_python-0.3.31/lange/__init__.py +0 -6
  27. lange_python-0.3.31/lange/tunnel/__init__.py +0 -7
  28. lange_python-0.3.31/lange/tunnel/_client.py +0 -685
  29. lange_python-0.3.31/lange/tunnel/_util.py +0 -29
  30. {lange_python-0.3.31 → lange_python-0.5.0}/lange/__main__.py +0 -0
  31. {lange_python-0.3.31 → lange_python-0.5.0}/lange/_util/__init__.py +0 -0
  32. {lange_python-0.3.31 → lange_python-0.5.0}/lange/_util/_base_client.py +0 -0
  33. {lange_python-0.3.31 → lange_python-0.5.0}/lange/_util/_key_handling.py +0 -0
  34. {lange_python-0.3.31 → lange_python-0.5.0}/lange/cli/build/__init__.py +0 -0
  35. {lange_python-0.3.31 → lange_python-0.5.0}/lange/cli/build/_command.py +0 -0
  36. {lange_python-0.3.31 → lange_python-0.5.0}/lange/cli/build/_discovery.py +0 -0
  37. {lange_python-0.3.31 → lange_python-0.5.0}/lange/cli/build/_docker.py +0 -0
  38. {lange_python-0.3.31 → lange_python-0.5.0}/lange/cli/build/_poetry.py +0 -0
  39. {lange_python-0.3.31 → lange_python-0.5.0}/lange/cli/build/_types.py +0 -0
  40. {lange_python-0.3.31 → lange_python-0.5.0}/lange/cli/code/__init__.py +0 -0
  41. {lange_python-0.3.31 → lange_python-0.5.0}/lange/cli/code/_stats.py +0 -0
  42. {lange_python-0.3.31 → lange_python-0.5.0}/lange/cli/code/audit/__init__.py +0 -0
  43. {lange_python-0.3.31 → lange_python-0.5.0}/lange/cli/code/audit/_command.py +0 -0
  44. {lange_python-0.3.31 → lange_python-0.5.0}/lange/cli/code/audit/_discovery.py +0 -0
  45. {lange_python-0.3.31 → lange_python-0.5.0}/lange/cli/code/audit/_runner.py +0 -0
  46. {lange_python-0.3.31 → lange_python-0.5.0}/lange/cli/code/audit/_types.py +0 -0
  47. {lange_python-0.3.31 → lange_python-0.5.0}/lange/cli/distribution/__init__.py +0 -0
  48. {lange_python-0.3.31 → lange_python-0.5.0}/lange/cli/distribution/_command.py +0 -0
  49. {lange_python-0.3.31 → lange_python-0.5.0}/lange/distribution/_client.py +0 -0
  50. {lange_python-0.3.31 → lange_python-0.5.0}/lange/distribution/_update_macos.py +0 -0
  51. {lange_python-0.3.31 → lange_python-0.5.0}/lange/distribution/_util.py +0 -0
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: lange-python
3
+ Version: 0.5.0
4
+ Summary: A bundeld set of tools, clients for the lange-suite of tools and more.
5
+ Author: contact@robertlange.me
6
+ Requires-Python: >=3.10
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.10
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Requires-Dist: certifi (>=2026.0.0)
14
+ Requires-Dist: click (>=8.0.0,<9.0.0)
15
+ Requires-Dist: httpx (>=0.28.1,<0.29.0)
16
+ Requires-Dist: idna (>=3.15)
17
+ Requires-Dist: pydantic (>=2.0.0,<3.0.0)
18
+ Requires-Dist: websockets (>=12.0,<20.0)
19
+ Description-Content-Type: text/markdown
20
+
21
+ # lange-python
22
+
23
+ Python helpers for Lange services.
24
+
25
+ ## Mesh Relay Worker
26
+
27
+ `MeshRelay` connects a local HTTP service to the Lange mesh relay and forwards
28
+ public relay requests to your local target.
29
+
30
+ ```bash
31
+ pip install lange-python
32
+ ```
33
+
34
+ ```python
35
+ from lange.mesh.relay import MeshRelay
36
+
37
+ relay = MeshRelay(
38
+ host="wss://api.lange-labs.com",
39
+ key="default",
40
+ target="http://localhost:3000",
41
+ )
42
+
43
+ relay.start()
44
+
45
+ try:
46
+ print(relay.status)
47
+ print(relay.remote_relay_address)
48
+ finally:
49
+ relay.stop()
50
+ ```
51
+
52
+ By default, `MeshRelay` reads authentication from the `LANGE_LABS_API_KEY`
53
+ environment variable. Keep API keys in the environment or a local secret store
54
+ instead of hardcoding them in application code.
55
+
56
+ ```bash
57
+ export LANGE_LABS_API_KEY="your-api-key"
58
+ ```
59
+
60
+ The relay exposes lifecycle state for integrations:
61
+
62
+ - `status`: one of `unauthenticated`, `off`, `pending`, `connected`, or `failed`
63
+ - `connected`: whether the relay websocket is currently connected
64
+ - `remote_relay_address`: public REST relay address returned by the API
65
+ - `worker_config`: full worker configuration returned by the API
66
+
67
+ Reload authentication or reconnect the worker without rebuilding it:
68
+
69
+ ```python
70
+ relay.reload()
71
+ relay.reload(api_key=None)
72
+ relay.reconnect()
73
+ ```
74
+
@@ -0,0 +1,53 @@
1
+ # lange-python
2
+
3
+ Python helpers for Lange services.
4
+
5
+ ## Mesh Relay Worker
6
+
7
+ `MeshRelay` connects a local HTTP service to the Lange mesh relay and forwards
8
+ public relay requests to your local target.
9
+
10
+ ```bash
11
+ pip install lange-python
12
+ ```
13
+
14
+ ```python
15
+ from lange.mesh.relay import MeshRelay
16
+
17
+ relay = MeshRelay(
18
+ host="wss://api.lange-labs.com",
19
+ key="default",
20
+ target="http://localhost:3000",
21
+ )
22
+
23
+ relay.start()
24
+
25
+ try:
26
+ print(relay.status)
27
+ print(relay.remote_relay_address)
28
+ finally:
29
+ relay.stop()
30
+ ```
31
+
32
+ By default, `MeshRelay` reads authentication from the `LANGE_LABS_API_KEY`
33
+ environment variable. Keep API keys in the environment or a local secret store
34
+ instead of hardcoding them in application code.
35
+
36
+ ```bash
37
+ export LANGE_LABS_API_KEY="your-api-key"
38
+ ```
39
+
40
+ The relay exposes lifecycle state for integrations:
41
+
42
+ - `status`: one of `unauthenticated`, `off`, `pending`, `connected`, or `failed`
43
+ - `connected`: whether the relay websocket is currently connected
44
+ - `remote_relay_address`: public REST relay address returned by the API
45
+ - `worker_config`: full worker configuration returned by the API
46
+
47
+ Reload authentication or reconnect the worker without rebuilding it:
48
+
49
+ ```python
50
+ relay.reload()
51
+ relay.reload(api_key=None)
52
+ relay.reconnect()
53
+ ```
@@ -0,0 +1,4 @@
1
+ """Public package exports for lange-python."""
2
+ from . import contracts, mesh
3
+
4
+ __all__ = ["contracts", "mesh"]
@@ -6,7 +6,9 @@ import click
6
6
 
7
7
  from .build import build_command
8
8
  from .code import code_group
9
+ from ._create import create_command
9
10
  from .distribution import distribution_group
11
+ from ._init import init_command
10
12
 
11
13
 
12
14
 
@@ -21,4 +23,6 @@ def cli() -> None:
21
23
 
22
24
  cli.add_command(code_group, "code")
23
25
  cli.add_command(build_command, "build")
26
+ cli.add_command(create_command, "create")
24
27
  cli.add_command(distribution_group, "distribution")
28
+ cli.add_command(init_command, "init")
@@ -0,0 +1,61 @@
1
+ """Implementation for the ``lange create`` command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import click
10
+
11
+
12
+ SERVICES_FILE = Path("services.json")
13
+
14
+
15
+ def _load_services(path: Path) -> list[dict[str, Any]]:
16
+ """Load existing service definitions from disk.
17
+
18
+ :param path: JSON file containing service definitions.
19
+ :returns: Existing service list or an empty list when the file is absent.
20
+ """
21
+ if not path.exists():
22
+ return []
23
+
24
+ raw_services = json.loads(path.read_text(encoding="utf-8"))
25
+ if isinstance(raw_services, list):
26
+ return [item for item in raw_services if isinstance(item, dict)]
27
+ if isinstance(raw_services, dict) and isinstance(raw_services.get("services"), list):
28
+ return [
29
+ item for item in raw_services["services"] if isinstance(item, dict)
30
+ ]
31
+ raise click.ClickException("services.json must contain a list of services.")
32
+
33
+
34
+ def _write_services(path: Path, services: list[dict[str, Any]]) -> None:
35
+ """Persist service definitions with stable formatting.
36
+
37
+ :param path: JSON file to write.
38
+ :param services: Service definitions to persist.
39
+ """
40
+ path.write_text(
41
+ json.dumps(services, indent=2, sort_keys=True) + "\n",
42
+ encoding="utf-8",
43
+ )
44
+
45
+
46
+ @click.command("create")
47
+ def create_command() -> None:
48
+ """Interactively create one service definition.
49
+
50
+ :returns: ``None``.
51
+ """
52
+ service = {
53
+ "name": click.prompt("Service name", type=str),
54
+ "path": click.prompt("Service path", type=str),
55
+ "build_type": click.prompt("Build type", type=str),
56
+ "publish_path": click.prompt("Publish path", type=str),
57
+ }
58
+ services = _load_services(SERVICES_FILE)
59
+ services.append(service)
60
+ _write_services(SERVICES_FILE, services)
61
+ click.echo(f"Created service {service['name']}.")
@@ -0,0 +1,22 @@
1
+ """Implementation for the ``lange init`` command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import click
8
+
9
+
10
+ @click.command("init")
11
+ def init_command() -> None:
12
+ """Create local ``.lange`` bootstrap files.
13
+
14
+ :returns: ``None``.
15
+ """
16
+ lange_dir = Path(".lange")
17
+ lange_dir.mkdir(parents=True, exist_ok=True)
18
+ lange_dir.joinpath(".gitignore").write_text("*\n", encoding="utf-8")
19
+ secrets_file = lange_dir / "secrets.json"
20
+ if not secrets_file.exists():
21
+ secrets_file.write_text("{}\n", encoding="utf-8")
22
+ click.echo("Initialized .lange.")
@@ -0,0 +1,7 @@
1
+ from . import mesh
2
+ from .mesh.ai import AiModelConfig
3
+
4
+ __all__ = [
5
+ "AiModelConfig",
6
+ "mesh",
7
+ ]
@@ -0,0 +1,29 @@
1
+ from ._message import MeshMessage
2
+ from .ai import (
3
+ AIModelSpecs,
4
+ AiModelConfig,
5
+ AiModelRegistration,
6
+ AiModelVirtualEnvironment,
7
+ ChatRequest,
8
+ ChatResponse,
9
+ EmbeddingRequest,
10
+ EmbeddingResponse,
11
+ MeshAiWorkerConfig,
12
+ )
13
+ from .relay import MeshRelayRequest, MeshRelayResponse, MeshRelayWorkerConfig
14
+
15
+ __all__ = [
16
+ "AIModelSpecs",
17
+ "AiModelConfig",
18
+ "AiModelRegistration",
19
+ "AiModelVirtualEnvironment",
20
+ "ChatRequest",
21
+ "ChatResponse",
22
+ "EmbeddingRequest",
23
+ "EmbeddingResponse",
24
+ "MeshAiWorkerConfig",
25
+ "MeshMessage",
26
+ "MeshRelayRequest",
27
+ "MeshRelayResponse",
28
+ "MeshRelayWorkerConfig",
29
+ ]
@@ -0,0 +1,32 @@
1
+ import uuid
2
+ from typing import Literal
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+ from .ai import EmbeddingResponse, EmbeddingRequest, ChatResponse, ChatRequest
7
+ from lange.contracts.mesh.relay import (
8
+ MeshRelayRequest,
9
+ MeshRelayResponse,
10
+ MeshRelayWorkerConfig,
11
+ MeshRelayWorkerRegistration,
12
+ )
13
+ from .ai import MeshAiWorkerConfig
14
+
15
+
16
+ class MeshMessage(BaseModel):
17
+ id: uuid.UUID = Field(default_factory=uuid.uuid4)
18
+ status: Literal["hello", "ping", "ready", "request", "response"]
19
+
20
+ # data type
21
+ data: (
22
+ MeshAiWorkerConfig
23
+ | MeshRelayRequest
24
+ | MeshRelayResponse
25
+ | MeshRelayWorkerConfig
26
+ | MeshRelayWorkerRegistration
27
+ | EmbeddingRequest
28
+ | EmbeddingResponse
29
+ | ChatRequest
30
+ | ChatResponse
31
+ | None
32
+ )
@@ -0,0 +1,23 @@
1
+ from .embeddings import EmbeddingRequest, EmbeddingResponse
2
+ from .chat import ChatResponse, ChatRequest
3
+ from .ai_model import (
4
+ AiModelConfig,
5
+ AIModelSpecs,
6
+ AiModelRegistration,
7
+ AiModelVirtualEnvironment,
8
+ )
9
+ from .worker_config import MeshAiWorkerConfig
10
+
11
+ __all__ = [
12
+ "MeshAiWorkerConfig",
13
+
14
+ "EmbeddingRequest",
15
+ "EmbeddingResponse",
16
+ "AiModelConfig",
17
+ "AIModelSpecs",
18
+ "AiModelRegistration",
19
+ "AiModelVirtualEnvironment",
20
+
21
+ "ChatRequest",
22
+ "ChatResponse",
23
+ ]
@@ -0,0 +1,73 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class AIModelSpecs(BaseModel):
7
+ model_format: Literal["mlx", "vLLM"]
8
+ model_size_in_billions: int
9
+ quantization: str
10
+ model_id: str
11
+ model_hub: Literal["huggingface"]
12
+ model_revision: None
13
+ model_uri:str|None
14
+ activated_size_in_billions: int|None
15
+
16
+
17
+ class AiModelVirtualEnvironment(BaseModel):
18
+ packages: list[str]
19
+ inherit_pip_config: bool
20
+ index_url: str | None
21
+ extra_index_url: str | None
22
+ find_links: bool | None
23
+ trusted_host: None
24
+ no_build_isolation: None
25
+
26
+
27
+ class AiModelRegistration(BaseModel):
28
+ version: int
29
+ context_length: int
30
+ model_name: str
31
+ model_lang: list[str]
32
+ model_ability: list[Literal["generate", "chat"]]
33
+ model_description: str
34
+ model_family: str
35
+ model_specs: list[AIModelSpecs]
36
+ chat_template: str|None
37
+ stop_token_ids: None
38
+ stop: None
39
+ cache_config: None
40
+ virtualenv: AiModelVirtualEnvironment
41
+ is_builtin: bool
42
+
43
+ reasoning_start_tag: str|None
44
+ reasoning_end_tag: str|None
45
+
46
+
47
+ class AiModelConfig(BaseModel):
48
+ # The name of the model to launch
49
+ model_name: str
50
+
51
+ # An easy alias for the model
52
+ model_alias: str
53
+
54
+ # The type of the model.
55
+ model_type: Literal["LLM", "embedding", "image", "audio", "video"]
56
+
57
+ # The size of the model in case multiple are available. e.G. gemma4 with 12b vs 31b
58
+ size: str | int | None = None
59
+
60
+ # The quantization of the model to use. Keep None for default.
61
+ quantization: str | None = None
62
+
63
+ # the context window of the model to use. if None it chooses default
64
+ context_window: int | None = None
65
+
66
+ # thinking
67
+ enable_thinking: bool | None = None
68
+
69
+ # engine configuration
70
+ model_engine: Literal["MLX", "vLLM"] | None = None
71
+ model_format: Literal["mlx", "vLLM"] | None = None
72
+
73
+ registration: AiModelRegistration | None
@@ -0,0 +1,12 @@
1
+ from pydantic import BaseModel
2
+ from typing import Literal
3
+
4
+ class ChatMessage(BaseModel):
5
+ role: Literal["user", "assistant"]
6
+ content: str
7
+
8
+ class ChatRequest(BaseModel):
9
+ messages: list[ChatMessage]
10
+
11
+ class ChatResponse(BaseModel):
12
+ message: ChatMessage
@@ -0,0 +1,9 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class EmbeddingRequest(BaseModel):
5
+ data: list[str]
6
+ model: str
7
+
8
+ class EmbeddingResponse(BaseModel):
9
+ embeddings: list[list[float]]
@@ -0,0 +1,16 @@
1
+ from pydantic import BaseModel
2
+
3
+ from .ai_model import AiModelConfig
4
+
5
+
6
+ class MeshAiWorkerConfig(BaseModel):
7
+ host: str = "0.0.0.0"
8
+ port: int = 8001
9
+
10
+ # inference config
11
+ chat_model: AiModelConfig | None = None
12
+ image_model: AiModelConfig | None = None
13
+ embedding_model: AiModelConfig | None = None
14
+
15
+
16
+
@@ -0,0 +1,9 @@
1
+ from ._rest import MeshRelayRequest, MeshRelayResponse
2
+ from ._worker_config import MeshRelayWorkerConfig, MeshRelayWorkerRegistration
3
+
4
+ __all__ = [
5
+ "MeshRelayRequest",
6
+ "MeshRelayResponse",
7
+ "MeshRelayWorkerConfig",
8
+ "MeshRelayWorkerRegistration",
9
+ ]
@@ -0,0 +1,41 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+
5
+
6
+ class MeshRelayRequest(BaseModel):
7
+ """REST request payload sent to a mesh relay worker."""
8
+
9
+ model_config = ConfigDict(populate_by_name=True)
10
+
11
+ method: str
12
+ path: str
13
+ headers: dict[str, str] = Field(default_factory=dict)
14
+ body: str | None = None
15
+ body_encoding: Literal["base64"] | None = Field(
16
+ default=None,
17
+ validation_alias="bodyEncoding",
18
+ serialization_alias="bodyEncoding",
19
+ )
20
+ query_params: dict[str, list[str]] = Field(
21
+ default_factory=dict,
22
+ validation_alias="queryParams",
23
+ serialization_alias="queryParams",
24
+ )
25
+ query_string: str | None = None
26
+
27
+
28
+ class MeshRelayResponse(BaseModel):
29
+ """REST response payload returned by a mesh compute worker."""
30
+
31
+ model_config = ConfigDict(populate_by_name=True)
32
+
33
+ status: int
34
+ headers: dict[str, str] = Field(default_factory=dict)
35
+ body: str | None = None
36
+ body_encoding: Literal["base64"] | None = Field(
37
+ default=None,
38
+ validation_alias="bodyEncoding",
39
+ serialization_alias="bodyEncoding",
40
+ )
41
+ error: str | None = None
@@ -0,0 +1,31 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+
5
+
6
+ class MeshRelayWorkerRegistration(BaseModel):
7
+ """Registration payload sent by a mesh relay worker during hello.
8
+
9
+ :param key: Public relay key owned by the worker.
10
+ :param request_timeout_seconds: Maximum seconds the relay should wait for
11
+ one worker-owned downstream request.
12
+ """
13
+
14
+ model_config = ConfigDict(populate_by_name=True)
15
+
16
+ key: str
17
+ request_timeout_seconds: float = Field(
18
+ validation_alias="requestTimeoutSeconds",
19
+ serialization_alias="requestTimeoutSeconds",
20
+ )
21
+
22
+
23
+ class MeshRelayWorkerConfig(BaseModel):
24
+ """Runtime relay configuration returned by the API to a registered worker.
25
+
26
+ :param remote_relay_address: Public relay URL clients can call.
27
+ :param type: Relay protocol type.
28
+ """
29
+
30
+ remote_relay_address: str
31
+ type: Literal["REST"]
@@ -1,7 +1,3 @@
1
- import logging
2
-
3
- logger = logging.getLogger("lange.distribution")
4
-
5
1
  from ._client import DistributionClient
6
2
 
7
3
  __all__ = ["DistributionClient"]
@@ -0,0 +1,5 @@
1
+ """Mesh clients and helpers."""
2
+
3
+ from . import relay
4
+
5
+ __all__ = ["relay"]
@@ -0,0 +1,18 @@
1
+ """Mesh relay client package."""
2
+
3
+ from .client import MeshRelay
4
+
5
+ from lange.contracts.mesh.relay import (
6
+ MeshRelayRequest,
7
+ MeshRelayResponse,
8
+ MeshRelayWorkerConfig,
9
+ MeshRelayWorkerRegistration,
10
+ )
11
+
12
+ __all__ = [
13
+ "MeshRelay",
14
+ "MeshRelayRequest",
15
+ "MeshRelayResponse",
16
+ "MeshRelayWorkerConfig",
17
+ "MeshRelayWorkerRegistration",
18
+ ]
@@ -0,0 +1,5 @@
1
+ """Mesh relay worker client implementation."""
2
+
3
+ from ._client import MeshRelay
4
+
5
+ __all__ = ["MeshRelay"]