Skip to content

Commit adcb9eb

Browse files
authored
Refactor/pypi preparation (#14)
* Reafctor for the folder schemas + defined import for public api * Defined public api, improved docs
1 parent c900708 commit adcb9eb

File tree

76 files changed

+2336
-1024
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+2336
-1024
lines changed

docs/api/components.md

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
# AsyncFlow — Public API Reference: `components`
2+
3+
This page documents the **public topology components** you can import from
4+
`asyncflow.components` to construct a simulation scenario in Python.
5+
These classes are Pydantic models with strict validation and are the
6+
**only pieces you need** to define the *structure* of your system: nodes
7+
(client/servers/LB), endpoints (steps), and network edges.
8+
9+
> The builder (`AsyncFlow`) will assemble these into the internal graph for you.
10+
> You **do not** need to import internal graph classes.
11+
12+
---
13+
14+
## Imports
15+
16+
```python
17+
from asyncflow.components import (
18+
Client,
19+
Server,
20+
ServerResources,
21+
LoadBalancer,
22+
Endpoint,
23+
Edge,
24+
)
25+
# Optional enums (strings are also accepted):
26+
from asyncflow.enums import Distribution
27+
```
28+
29+
---
30+
31+
## Quick example
32+
33+
```python
34+
from asyncflow.components import (
35+
Client, Server, ServerResources, LoadBalancer, Endpoint, Edge
36+
)
37+
38+
# Nodes
39+
client = Client(id="client-1")
40+
41+
endpoint = Endpoint(
42+
endpoint_name="/predict",
43+
steps=[
44+
{"kind": "ram", "step_operation": {"necessary_ram": 64}},
45+
{"kind": "initial_parsing", "step_operation": {"cpu_time": 0.002}},
46+
{"kind": "io_wait", "step_operation": {"io_waiting_time": 0.010}},
47+
],
48+
)
49+
50+
server = Server(
51+
id="srv-1",
52+
server_resources=ServerResources(cpu_cores=2, ram_mb=2048),
53+
endpoints=[endpoint],
54+
)
55+
56+
lb = LoadBalancer(id="lb-1", algorithms="round_robin", server_covered={"srv-1"})
57+
58+
# Edges (directed)
59+
edges = [
60+
Edge(
61+
id="gen-to-client",
62+
source="rqs-1", # external sources allowed (e.g., generator id)
63+
target="client-1", # targets must be declared nodes
64+
latency={"mean": 0.003, "distribution": "exponential"},
65+
),
66+
Edge(
67+
id="client-to-lb",
68+
source="client-1",
69+
target="lb-1",
70+
latency={"mean": 0.002, "distribution": "exponential"},
71+
),
72+
Edge(
73+
id="lb-to-srv1",
74+
source="lb-1",
75+
target="srv-1",
76+
latency={"mean": 0.002, "distribution": "exponential"},
77+
),
78+
Edge(
79+
id="srv1-to-client",
80+
source="srv-1",
81+
target="client-1",
82+
latency={"mean": 0.003, "distribution": "exponential"},
83+
),
84+
]
85+
```
86+
87+
You can then feed these to the `AsyncFlow` builder (not shown here) along with
88+
workload and settings.
89+
90+
---
91+
92+
## Component reference
93+
94+
### `Client`
95+
96+
```python
97+
Client(id: str)
98+
```
99+
100+
* Represents the client node.
101+
* `type` is fixed internally to `"client"`.
102+
* **Validation:** any non-standard `type` is rejected (guardrail).
103+
104+
---
105+
106+
### `ServerResources`
107+
108+
```python
109+
ServerResources(
110+
cpu_cores: int = 1, # ≥ 1 NOW MUST BE FIXED TO ONE
111+
ram_mb: int = 1024, # ≥ 256
112+
db_connection_pool: int | None = None,
113+
)
114+
```
115+
116+
* Server capacity knobs used by the runtime (CPU tokens, RAM reservoir, optional DB pool).
117+
* You may pass a **dict** instead of `ServerResources`; Pydantic will coerce it.
118+
119+
**Bounds & defaults**
120+
121+
* `cpu_cores ≥ 1`
122+
* `ram_mb ≥ 256`
123+
* `db_connection_pool` optional
124+
125+
---
126+
127+
### `Endpoint`
128+
129+
```python
130+
Endpoint(
131+
endpoint_name: str, # normalized to lowercase
132+
steps: list[dict], # or Pydantic Step objects (dict is simpler)
133+
)
134+
```
135+
136+
Each step is a dict with **exactly one** operation:
137+
138+
```python
139+
{"kind": <step-kind>, "step_operation": { <op-key>: <positive number> }}
140+
```
141+
142+
**Valid step kinds and operation keys**
143+
144+
| Kind (enum string) | Operation dict (exactly 1 key) | Units / constraints | |
145+
| --------------------- | -------------------------------- | ------------------- | ------- |
146+
| `initial_parsing` | `{ "cpu_time": <float> }` | seconds, > 0 | |
147+
| `cpu_bound_operation` | `{ "cpu_time": <float> }` | seconds, > 0 | |
148+
| `ram` | \`{ "necessary\_ram": \<int | float> }\` | MB, > 0 |
149+
| `io_task_spawn` | `{ "io_waiting_time": <float> }` | seconds, > 0 | |
150+
| `io_llm` | `{ "io_waiting_time": <float> }` | seconds, > 0 | |
151+
| `io_wait` | `{ "io_waiting_time": <float> }` | seconds, > 0 | |
152+
| `io_db` | `{ "io_waiting_time": <float> }` | seconds, > 0 | |
153+
| `io_cache` | `{ "io_waiting_time": <float> }` | seconds, > 0 | |
154+
155+
**Validation**
156+
157+
* `endpoint_name` is lowercased automatically.
158+
* `step_operation` must have **one and only one** entry.
159+
* The operation **must match** the step kind (CPU ↔ `cpu_time`, RAM ↔ `necessary_ram`, IO ↔ `io_waiting_time`).
160+
* All numeric values must be **strictly positive**.
161+
162+
---
163+
164+
### `Server`
165+
166+
```python
167+
Server(
168+
id: str,
169+
server_resources: ServerResources | dict,
170+
endpoints: list[Endpoint],
171+
)
172+
```
173+
174+
* Represents a server node hosting one or more endpoints.
175+
* `type` is fixed internally to `"server"`.
176+
* **Validation:** any non-standard `type` is rejected.
177+
178+
---
179+
180+
### `LoadBalancer` (optional)
181+
182+
```python
183+
LoadBalancer(
184+
id: str,
185+
algorithms: Literal["round_robin", "least_connection"] = "round_robin",
186+
server_covered: set[str] = set(),
187+
)
188+
```
189+
190+
* Declares a logical load balancer and the set of server IDs it can route to.
191+
* **Graph-level rules** (checked when the payload is built):
192+
193+
* `server_covered` must be a subset of declared server IDs.
194+
* There must be an **edge from the LB to each covered server** (e.g., `lb-1 → srv-1`).
195+
196+
---
197+
198+
### `Edge`
199+
200+
```python
201+
Edge(
202+
id: str,
203+
source: str,
204+
target: str,
205+
latency: dict | RVConfig, # recommend dict: {"mean": <float>, "distribution": <enum>, "variance": <float?>}
206+
edge_type: Literal["network_connection"] = "network_connection",
207+
dropout_rate: float = 0.01, # in [0.0, 1.0]
208+
)
209+
```
210+
211+
* Directed link between two nodes.
212+
* **Latency** is a random variable; most users pass a dict:
213+
214+
* `mean: float` (required)
215+
* `distribution: "poisson" | "normal" | "log_normal" | "exponential" | "uniform"` (default: `"poisson"`)
216+
* `variance: float?` (for `normal`/`log_normal`, defaults to `mean` if omitted)
217+
218+
**Validation**
219+
220+
* `mean > 0`
221+
* if provided, `variance ≥ 0`
222+
* `dropout_rate ∈ [0.0, 1.0]`
223+
* `source != target`
224+
225+
**Graph-level rules** (enforced when the full payload is validated)
226+
227+
* Every **target** must be a **declared node** (`client`, `server`, or `load_balancer`).
228+
* **External IDs** (e.g., `"rqs-1"`) are allowed **only** as **sources**; they cannot appear as targets.
229+
* **Unique edge IDs**.
230+
* **No fan-out except LB**: only the load balancer is allowed to have multiple outgoing edges among declared nodes.
231+
232+
---
233+
234+
## Type coercion & enums
235+
236+
* You may pass strings for enums (`kind`, `distribution`, etc.); they will be validated against the allowed values.
237+
* For `ServerResources` and `Edge.latency` you can pass dictionaries; Pydantic will coerce them to typed models.
238+
* If you prefer, you can import and use the enums:
239+
240+
```python
241+
from asyncflow.enums import Distribution
242+
Edge(..., latency={"mean": 0.003, "distribution": Distribution.EXPONENTIAL})
243+
```
244+
245+
---
246+
247+
## Best practices & pitfalls
248+
249+
**Do**
250+
251+
* Keep IDs unique across nodes of the same category and across edges.
252+
* Ensure LB coverage and LB→server edges are in sync.
253+
* Use small, measurable step values first; iterate once you see where queues and delays form.
254+
255+
**Don’t**
256+
257+
* Create multiple outgoing edges from non-LB nodes (graph validator will fail).
258+
* Use zero/negative times or RAM (validators will raise).
259+
* Target external IDs (only sources may be external).
260+
261+
---
262+
263+
## Where these components fit
264+
265+
You will typically combine these **components** with:
266+
267+
* **workload** (`RqsGenerator`) from `asyncflow.workload`
268+
* **settings** (`SimulationSettings`) from `asyncflow.settings`
269+
* the **builder** (`AsyncFlow`) and **runner** (`SimulationRunner`) from the root package
270+
271+
Example (wiring, abbreviated):
272+
273+
```python
274+
from asyncflow import AsyncFlow, SimulationRunner
275+
from asyncflow.workload import RqsGenerator
276+
from asyncflow.settings import SimulationSettings
277+
278+
flow = (
279+
AsyncFlow()
280+
.add_generator(RqsGenerator(...))
281+
.add_client(client)
282+
.add_servers(server)
283+
.add_edges(*edges)
284+
.add_load_balancer(lb) # optional
285+
.add_simulation_settings(SimulationSettings(...))
286+
)
287+
payload = flow.build_payload() # validates graph-level rules
288+
SimulationRunner(..., simulation_input=payload).run()
289+
```
290+
291+
---
292+
293+
With these `components`, you can model any topology supported by AsyncFlow—
294+
cleanly, type-checked, and with **clear, early** validation errors when something
295+
is inconsistent.

0 commit comments

Comments
 (0)