pyflowstep 0.2.0__tar.gz → 0.4.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.
- {pyflowstep-0.2.0 → pyflowstep-0.4.0}/PKG-INFO +91 -3
- {pyflowstep-0.2.0 → pyflowstep-0.4.0}/README.md +90 -2
- {pyflowstep-0.2.0 → pyflowstep-0.4.0}/pyproject.toml +3 -1
- {pyflowstep-0.2.0 → pyflowstep-0.4.0}/pyproject.toml.orig +8 -3
- {pyflowstep-0.2.0 → pyflowstep-0.4.0}/src/pyflowstep/__init__.py +6 -0
- {pyflowstep-0.2.0 → pyflowstep-0.4.0}/src/pyflowstep/annotations.py +90 -59
- {pyflowstep-0.2.0 → pyflowstep-0.4.0}/src/pyflowstep/dependencies.py +326 -296
- {pyflowstep-0.2.0 → pyflowstep-0.4.0}/src/pyflowstep/exceptions.py +13 -0
- {pyflowstep-0.2.0 → pyflowstep-0.4.0}/src/pyflowstep/flow.py +12 -2
- pyflowstep-0.4.0/src/pyflowstep/inputs.py +231 -0
- {pyflowstep-0.2.0 → pyflowstep-0.4.0}/src/pyflowstep/steps.py +29 -10
- {pyflowstep-0.2.0 → pyflowstep-0.4.0}/src/pyflowstep/compilers.py +0 -0
- {pyflowstep-0.2.0 → pyflowstep-0.4.0}/src/pyflowstep/json_schema.py +0 -0
- {pyflowstep-0.2.0 → pyflowstep-0.4.0}/src/pyflowstep/parsers.py +0 -0
- {pyflowstep-0.2.0 → pyflowstep-0.4.0}/src/pyflowstep/py.typed +0 -0
- {pyflowstep-0.2.0 → pyflowstep-0.4.0}/src/pyflowstep/registry.py +0 -0
- {pyflowstep-0.2.0 → pyflowstep-0.4.0}/src/pyflowstep/validators.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyflowstep
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A lightweight, typed Python library for composing functions into readable, reusable flows that can be defined as JSON.
|
|
5
5
|
Keywords: flow,pipeline,composition,functional,fluent-interface,json,json-schema,workflow,dsl
|
|
6
6
|
Author: aaltatan
|
|
@@ -32,6 +32,7 @@ A lightweight, typed Python library for composing functions into readable, reusa
|
|
|
32
32
|
- step arguments are validated and parsed when a flow is **built**, not halfway through running it
|
|
33
33
|
- raw JSON values become typed arguments with a `Parse` marker next to the parameter
|
|
34
34
|
- steps get external objects (a mailer, a database session) through FastAPI-style `Depends`
|
|
35
|
+
- the caller hands per-run values to a flow by name: `flow(page, user=user)` fills every `user: User = Input()`
|
|
35
36
|
- registry-based registration keeps steps organized and discoverable
|
|
36
37
|
- flow definitions can be compiled from dictionaries or JSON
|
|
37
38
|
- every registry can describe its flow language as a JSON Schema
|
|
@@ -181,7 +182,7 @@ pipeline # Flow(add >> multiply >> add)
|
|
|
181
182
|
pipeline(1) # 31
|
|
182
183
|
```
|
|
183
184
|
|
|
184
|
-
`step` and `tap` do one thing: turn a function into a flow factory. Naming a step belongs to the [registry](#registry).
|
|
185
|
+
`step` and `tap` do one thing: turn a function into a flow factory. Naming a step belongs to the [registry](#registry). Three markers can sit on a parameter: [`Parse`](#parsing-arguments) to convert the value passed for it, [`Depends`](#dependencies) to inject an object nobody passes, and [`Input`](#run-inputs) to receive a value from whoever runs the flow.
|
|
185
186
|
|
|
186
187
|
Arguments are bound against the function signature **immediately**, so mistakes fail fast:
|
|
187
188
|
|
|
@@ -442,6 +443,8 @@ def send_email(order: Order, template: str, mailer: MailerDep) -> None: ...
|
|
|
442
443
|
def send_invoice(order: Order, mailer: MailerDep) -> None: ...
|
|
443
444
|
```
|
|
444
445
|
|
|
446
|
+
Both forms behave the same when the flow runs. Type checkers differ: with the `Annotated` form they still see `mailer` as a required argument and report `send_email("receipt")` as missing it. Use the default-value form for steps you call from Python; the `Annotated` form suits steps that are only used from JSON.
|
|
447
|
+
|
|
445
448
|
### One object per flow run
|
|
446
449
|
|
|
447
450
|
A flow run is one scope, like one request in a web framework. A provider is called **at most once per run**, and every step of that run receives the same object. The next run starts fresh.
|
|
@@ -532,6 +535,89 @@ See [`examples/dependencies.py`](examples/dependencies.py) for a complete flow w
|
|
|
532
535
|
|
|
533
536
|
---
|
|
534
537
|
|
|
538
|
+
## Run inputs
|
|
539
|
+
|
|
540
|
+
Some values exist only where the flow is run: the logged-in user, the record being processed, credentials read from a prompt. No provider can build them and JSON must not hold them. Give the parameter the default `Input()` and the caller supplies it, by name, when it runs the flow:
|
|
541
|
+
|
|
542
|
+
```python
|
|
543
|
+
from pyflowstep import Input, StepsRegistry
|
|
544
|
+
|
|
545
|
+
steps = StepsRegistry[Page]()
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
@steps.tap()
|
|
549
|
+
def login(page: Page, url: str, credentials: Credentials = Input()) -> None:
|
|
550
|
+
page.navigate(url)
|
|
551
|
+
page.fill("#user", credentials.username)
|
|
552
|
+
page.fill("#password", credentials.password)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
@steps.tap()
|
|
556
|
+
def fill_year(page: Page, selector: str, voucher: Voucher = Input()) -> None:
|
|
557
|
+
page.fill(selector, voucher.year)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
flow = login("https://example.com/login") >> fill_year("#year") # inputs are not arguments
|
|
561
|
+
|
|
562
|
+
flow(page, credentials=credentials, voucher=voucher) # they are given to the run
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
```json
|
|
566
|
+
[
|
|
567
|
+
{"name": "login", "args": ["https://example.com/login"]},
|
|
568
|
+
{"name": "fill_year", "args": ["#year"]}
|
|
569
|
+
]
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
The name of the input is the name of the parameter, and every step that declares it receives the same value.
|
|
573
|
+
|
|
574
|
+
`Input()` is written as the default value, never inside `Annotated`, so that type checkers see the parameter as optional and accept `fill_year("#year")`.
|
|
575
|
+
|
|
576
|
+
### Checked before anything runs
|
|
577
|
+
|
|
578
|
+
A flow knows the inputs its steps require, and checks them before the first step runs. A forgotten input never fails halfway through:
|
|
579
|
+
|
|
580
|
+
```python
|
|
581
|
+
flow.inputs # frozenset({'credentials', 'voucher'})
|
|
582
|
+
|
|
583
|
+
flow(page, credentials=credentials)
|
|
584
|
+
# MissingInputError: missing run input 'voucher' for step 'fill_year'; run the flow as flow(subject, voucher=...)
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### Rules
|
|
588
|
+
|
|
589
|
+
- **Invisible to JSON**, like a dependency: an input is absent from the JSON schema, cannot be parsed, and passing one while building the flow raises `UnexpectedKeywordArgumentError` (or `TooManyArgumentsError`).
|
|
590
|
+
- **`Input(default=...)` makes the input optional**: with `note: str = Input(default="")`, `""` is used when the caller passes no `note`. Optional inputs are not listed in `flow.inputs`.
|
|
591
|
+
- **Extra inputs are ignored**, so one caller can run different flows, each using the inputs it needs.
|
|
592
|
+
- **Nested flows see the inputs of the run they join.** A flow called from inside a step can be given inputs of its own, `inner(page, voucher=other)`; they are laid over the outer ones for that call only.
|
|
593
|
+
- **Declarations are checked early**, when the step is created, with `InvalidInputError`: an input on the subject, on a positional-only parameter, on a parameter that is also a dependency, or written inside `Annotated`. A provider cannot take an input.
|
|
594
|
+
|
|
595
|
+
Using ruff? Add `Input` next to `Depends` for its `B008` rule:
|
|
596
|
+
|
|
597
|
+
```toml
|
|
598
|
+
[tool.ruff.lint.flake8-bugbear]
|
|
599
|
+
extend-immutable-calls = ["pyflowstep.Depends", "pyflowstep.Input"]
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### Upgrading from 0.3
|
|
603
|
+
|
|
604
|
+
`Input[T]` is gone, because a type checker reported `fill_year("#year")` as missing its `voucher` argument. Move the marker to the default value:
|
|
605
|
+
|
|
606
|
+
| 0.3 | 0.4 |
|
|
607
|
+
| ---------------------------------- | ---------------------------------------- |
|
|
608
|
+
| `voucher: Input[Voucher]` | `voucher: Voucher = Input()` |
|
|
609
|
+
| `note: Input[str] = ""` | `note: str = Input(default="")` |
|
|
610
|
+
|
|
611
|
+
### `Input` or `Depends`?
|
|
612
|
+
|
|
613
|
+
| The object… | Use |
|
|
614
|
+
| ------------------------------------------------------------ | ------------------------------------- |
|
|
615
|
+
| is written in the flow definition (a selector, a limit) | a plain argument, with `Parse` if needed |
|
|
616
|
+
| can be built by a function, the same way for every caller (a mailer, a session) | `Depends(provider)` |
|
|
617
|
+
| is only known to whoever runs the flow (a user, a record, credentials) | `Input()` |
|
|
618
|
+
|
|
619
|
+
---
|
|
620
|
+
|
|
535
621
|
## Compiling flows from JSON
|
|
536
622
|
|
|
537
623
|
`FlowCompiler` turns a flow definition — a list of step dictionaries — into a `Flow`.
|
|
@@ -738,13 +824,15 @@ checkout = note_if(is_large(Decimal(100)), "needs approval") >> apply_tax(round(
|
|
|
738
824
|
| `Parse(fn)` | marker | Apply `fn` to the value passed for a parameter, see [Parsing arguments](#parsing-arguments) |
|
|
739
825
|
| `Depends(provider)` | marker | Inject a parameter by calling `provider`, see [Dependencies](#dependencies) |
|
|
740
826
|
| `override_dependencies(mapping)` | context manager | Replace providers inside a `with` block, for tests |
|
|
827
|
+
| `Input(default=...)` | marker | Receive a parameter from the caller of the flow, `flow(subject, name=value)`, see [Run inputs](#run-inputs) |
|
|
828
|
+
| `Flow.inputs` | property | The names of the inputs a flow requires |
|
|
741
829
|
| `FlowCompiler[T](steps)` | class | `compile(definition)` turns parsed step dicts into a flow |
|
|
742
830
|
| `validate_step_dict(item, path)` | function | Validate one step dictionary |
|
|
743
831
|
| `get_json_schema(annotation)` | function | JSON Schema of a type annotation |
|
|
744
832
|
| `get_step_json_schema(name, step)` | function | JSON Schema of one step dictionary |
|
|
745
833
|
| `get_flow_json_schema(steps)` | function | JSON Schema of a whole flow definition |
|
|
746
834
|
|
|
747
|
-
All exceptions derive from `PyflowstepError`; argument errors, `InvalidParserError` and `
|
|
835
|
+
All exceptions derive from `PyflowstepError`; argument errors, `InvalidParserError`, `InvalidDependencyError`, `InvalidInputError` and `MissingInputError` also derive from `TypeError`, definition errors from `ValueError`, and `StepDoesNotExistError` from `LookupError`.
|
|
748
836
|
|
|
749
837
|
---
|
|
750
838
|
|
|
@@ -9,6 +9,7 @@ A lightweight, typed Python library for composing functions into readable, reusa
|
|
|
9
9
|
- step arguments are validated and parsed when a flow is **built**, not halfway through running it
|
|
10
10
|
- raw JSON values become typed arguments with a `Parse` marker next to the parameter
|
|
11
11
|
- steps get external objects (a mailer, a database session) through FastAPI-style `Depends`
|
|
12
|
+
- the caller hands per-run values to a flow by name: `flow(page, user=user)` fills every `user: User = Input()`
|
|
12
13
|
- registry-based registration keeps steps organized and discoverable
|
|
13
14
|
- flow definitions can be compiled from dictionaries or JSON
|
|
14
15
|
- every registry can describe its flow language as a JSON Schema
|
|
@@ -158,7 +159,7 @@ pipeline # Flow(add >> multiply >> add)
|
|
|
158
159
|
pipeline(1) # 31
|
|
159
160
|
```
|
|
160
161
|
|
|
161
|
-
`step` and `tap` do one thing: turn a function into a flow factory. Naming a step belongs to the [registry](#registry).
|
|
162
|
+
`step` and `tap` do one thing: turn a function into a flow factory. Naming a step belongs to the [registry](#registry). Three markers can sit on a parameter: [`Parse`](#parsing-arguments) to convert the value passed for it, [`Depends`](#dependencies) to inject an object nobody passes, and [`Input`](#run-inputs) to receive a value from whoever runs the flow.
|
|
162
163
|
|
|
163
164
|
Arguments are bound against the function signature **immediately**, so mistakes fail fast:
|
|
164
165
|
|
|
@@ -419,6 +420,8 @@ def send_email(order: Order, template: str, mailer: MailerDep) -> None: ...
|
|
|
419
420
|
def send_invoice(order: Order, mailer: MailerDep) -> None: ...
|
|
420
421
|
```
|
|
421
422
|
|
|
423
|
+
Both forms behave the same when the flow runs. Type checkers differ: with the `Annotated` form they still see `mailer` as a required argument and report `send_email("receipt")` as missing it. Use the default-value form for steps you call from Python; the `Annotated` form suits steps that are only used from JSON.
|
|
424
|
+
|
|
422
425
|
### One object per flow run
|
|
423
426
|
|
|
424
427
|
A flow run is one scope, like one request in a web framework. A provider is called **at most once per run**, and every step of that run receives the same object. The next run starts fresh.
|
|
@@ -509,6 +512,89 @@ See [`examples/dependencies.py`](examples/dependencies.py) for a complete flow w
|
|
|
509
512
|
|
|
510
513
|
---
|
|
511
514
|
|
|
515
|
+
## Run inputs
|
|
516
|
+
|
|
517
|
+
Some values exist only where the flow is run: the logged-in user, the record being processed, credentials read from a prompt. No provider can build them and JSON must not hold them. Give the parameter the default `Input()` and the caller supplies it, by name, when it runs the flow:
|
|
518
|
+
|
|
519
|
+
```python
|
|
520
|
+
from pyflowstep import Input, StepsRegistry
|
|
521
|
+
|
|
522
|
+
steps = StepsRegistry[Page]()
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
@steps.tap()
|
|
526
|
+
def login(page: Page, url: str, credentials: Credentials = Input()) -> None:
|
|
527
|
+
page.navigate(url)
|
|
528
|
+
page.fill("#user", credentials.username)
|
|
529
|
+
page.fill("#password", credentials.password)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
@steps.tap()
|
|
533
|
+
def fill_year(page: Page, selector: str, voucher: Voucher = Input()) -> None:
|
|
534
|
+
page.fill(selector, voucher.year)
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
flow = login("https://example.com/login") >> fill_year("#year") # inputs are not arguments
|
|
538
|
+
|
|
539
|
+
flow(page, credentials=credentials, voucher=voucher) # they are given to the run
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
```json
|
|
543
|
+
[
|
|
544
|
+
{"name": "login", "args": ["https://example.com/login"]},
|
|
545
|
+
{"name": "fill_year", "args": ["#year"]}
|
|
546
|
+
]
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
The name of the input is the name of the parameter, and every step that declares it receives the same value.
|
|
550
|
+
|
|
551
|
+
`Input()` is written as the default value, never inside `Annotated`, so that type checkers see the parameter as optional and accept `fill_year("#year")`.
|
|
552
|
+
|
|
553
|
+
### Checked before anything runs
|
|
554
|
+
|
|
555
|
+
A flow knows the inputs its steps require, and checks them before the first step runs. A forgotten input never fails halfway through:
|
|
556
|
+
|
|
557
|
+
```python
|
|
558
|
+
flow.inputs # frozenset({'credentials', 'voucher'})
|
|
559
|
+
|
|
560
|
+
flow(page, credentials=credentials)
|
|
561
|
+
# MissingInputError: missing run input 'voucher' for step 'fill_year'; run the flow as flow(subject, voucher=...)
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
### Rules
|
|
565
|
+
|
|
566
|
+
- **Invisible to JSON**, like a dependency: an input is absent from the JSON schema, cannot be parsed, and passing one while building the flow raises `UnexpectedKeywordArgumentError` (or `TooManyArgumentsError`).
|
|
567
|
+
- **`Input(default=...)` makes the input optional**: with `note: str = Input(default="")`, `""` is used when the caller passes no `note`. Optional inputs are not listed in `flow.inputs`.
|
|
568
|
+
- **Extra inputs are ignored**, so one caller can run different flows, each using the inputs it needs.
|
|
569
|
+
- **Nested flows see the inputs of the run they join.** A flow called from inside a step can be given inputs of its own, `inner(page, voucher=other)`; they are laid over the outer ones for that call only.
|
|
570
|
+
- **Declarations are checked early**, when the step is created, with `InvalidInputError`: an input on the subject, on a positional-only parameter, on a parameter that is also a dependency, or written inside `Annotated`. A provider cannot take an input.
|
|
571
|
+
|
|
572
|
+
Using ruff? Add `Input` next to `Depends` for its `B008` rule:
|
|
573
|
+
|
|
574
|
+
```toml
|
|
575
|
+
[tool.ruff.lint.flake8-bugbear]
|
|
576
|
+
extend-immutable-calls = ["pyflowstep.Depends", "pyflowstep.Input"]
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
### Upgrading from 0.3
|
|
580
|
+
|
|
581
|
+
`Input[T]` is gone, because a type checker reported `fill_year("#year")` as missing its `voucher` argument. Move the marker to the default value:
|
|
582
|
+
|
|
583
|
+
| 0.3 | 0.4 |
|
|
584
|
+
| ---------------------------------- | ---------------------------------------- |
|
|
585
|
+
| `voucher: Input[Voucher]` | `voucher: Voucher = Input()` |
|
|
586
|
+
| `note: Input[str] = ""` | `note: str = Input(default="")` |
|
|
587
|
+
|
|
588
|
+
### `Input` or `Depends`?
|
|
589
|
+
|
|
590
|
+
| The object… | Use |
|
|
591
|
+
| ------------------------------------------------------------ | ------------------------------------- |
|
|
592
|
+
| is written in the flow definition (a selector, a limit) | a plain argument, with `Parse` if needed |
|
|
593
|
+
| can be built by a function, the same way for every caller (a mailer, a session) | `Depends(provider)` |
|
|
594
|
+
| is only known to whoever runs the flow (a user, a record, credentials) | `Input()` |
|
|
595
|
+
|
|
596
|
+
---
|
|
597
|
+
|
|
512
598
|
## Compiling flows from JSON
|
|
513
599
|
|
|
514
600
|
`FlowCompiler` turns a flow definition — a list of step dictionaries — into a `Flow`.
|
|
@@ -715,13 +801,15 @@ checkout = note_if(is_large(Decimal(100)), "needs approval") >> apply_tax(round(
|
|
|
715
801
|
| `Parse(fn)` | marker | Apply `fn` to the value passed for a parameter, see [Parsing arguments](#parsing-arguments) |
|
|
716
802
|
| `Depends(provider)` | marker | Inject a parameter by calling `provider`, see [Dependencies](#dependencies) |
|
|
717
803
|
| `override_dependencies(mapping)` | context manager | Replace providers inside a `with` block, for tests |
|
|
804
|
+
| `Input(default=...)` | marker | Receive a parameter from the caller of the flow, `flow(subject, name=value)`, see [Run inputs](#run-inputs) |
|
|
805
|
+
| `Flow.inputs` | property | The names of the inputs a flow requires |
|
|
718
806
|
| `FlowCompiler[T](steps)` | class | `compile(definition)` turns parsed step dicts into a flow |
|
|
719
807
|
| `validate_step_dict(item, path)` | function | Validate one step dictionary |
|
|
720
808
|
| `get_json_schema(annotation)` | function | JSON Schema of a type annotation |
|
|
721
809
|
| `get_step_json_schema(name, step)` | function | JSON Schema of one step dictionary |
|
|
722
810
|
| `get_flow_json_schema(steps)` | function | JSON Schema of a whole flow definition |
|
|
723
811
|
|
|
724
|
-
All exceptions derive from `PyflowstepError`; argument errors, `InvalidParserError` and `
|
|
812
|
+
All exceptions derive from `PyflowstepError`; argument errors, `InvalidParserError`, `InvalidDependencyError`, `InvalidInputError` and `MissingInputError` also derive from `TypeError`, definition errors from `ValueError`, and `StepDoesNotExistError` from `LookupError`.
|
|
725
813
|
|
|
726
814
|
---
|
|
727
815
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pyflowstep"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "A lightweight, typed Python library for composing functions into readable, reusable flows that can be defined as JSON."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "GPL-3.0-only"
|
|
@@ -101,6 +101,8 @@ ignore = [
|
|
|
101
101
|
extend-immutable-calls = [
|
|
102
102
|
"pyflowstep.Depends",
|
|
103
103
|
"pyflowstep.dependencies.Depends",
|
|
104
|
+
"pyflowstep.Input",
|
|
105
|
+
"pyflowstep.inputs.Input",
|
|
104
106
|
]
|
|
105
107
|
|
|
106
108
|
[tool.ruff.lint.per-file-ignores]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pyflowstep"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "A lightweight, typed Python library for composing functions into readable, reusable flows that can be defined as JSON."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -80,8 +80,13 @@ ignore = [
|
|
|
80
80
|
]
|
|
81
81
|
|
|
82
82
|
[tool.ruff.lint.flake8-bugbear]
|
|
83
|
-
# `x: T = Depends(provider)`
|
|
84
|
-
extend-immutable-calls = [
|
|
83
|
+
# `x: T = Depends(provider)` and `x: T = Input()` are markers, not mutable defaults
|
|
84
|
+
extend-immutable-calls = [
|
|
85
|
+
"pyflowstep.Depends",
|
|
86
|
+
"pyflowstep.dependencies.Depends",
|
|
87
|
+
"pyflowstep.Input",
|
|
88
|
+
"pyflowstep.inputs.Input",
|
|
89
|
+
]
|
|
85
90
|
|
|
86
91
|
[tool.ruff.lint.per-file-ignores]
|
|
87
92
|
"tests/**" = ["S101", "PLR2004", "ANN", "ARG", "INP001", "E501"]
|
|
@@ -27,10 +27,12 @@ from .exceptions import (
|
|
|
27
27
|
ArgumentError,
|
|
28
28
|
InvalidDependencyError,
|
|
29
29
|
InvalidFlowDefinitionError,
|
|
30
|
+
InvalidInputError,
|
|
30
31
|
InvalidParserError,
|
|
31
32
|
InvalidStepError,
|
|
32
33
|
InvalidStepNameError,
|
|
33
34
|
MissingArgumentError,
|
|
35
|
+
MissingInputError,
|
|
34
36
|
MultipleValuesArgumentError,
|
|
35
37
|
ParseArgumentError,
|
|
36
38
|
PositionalOnlyArgumentError,
|
|
@@ -41,6 +43,7 @@ from .exceptions import (
|
|
|
41
43
|
UnexpectedKeywordArgumentError,
|
|
42
44
|
)
|
|
43
45
|
from .flow import Action, Flow, compose
|
|
46
|
+
from .inputs import Input
|
|
44
47
|
from .json_schema import get_flow_json_schema, get_json_schema, get_step_json_schema
|
|
45
48
|
from .parsers import Parse
|
|
46
49
|
from .registry import StepsRegistry
|
|
@@ -53,12 +56,15 @@ __all__ = [
|
|
|
53
56
|
"Flow",
|
|
54
57
|
"FlowCompiler",
|
|
55
58
|
"FlowDefinition",
|
|
59
|
+
"Input",
|
|
56
60
|
"InvalidDependencyError",
|
|
57
61
|
"InvalidFlowDefinitionError",
|
|
62
|
+
"InvalidInputError",
|
|
58
63
|
"InvalidParserError",
|
|
59
64
|
"InvalidStepError",
|
|
60
65
|
"InvalidStepNameError",
|
|
61
66
|
"MissingArgumentError",
|
|
67
|
+
"MissingInputError",
|
|
62
68
|
"MultipleValuesArgumentError",
|
|
63
69
|
"Parse",
|
|
64
70
|
"ParseArgumentError",
|
|
@@ -1,59 +1,90 @@
|
|
|
1
|
-
"""Read the annotations that carry markers such as `Parse` and `Depends`."""
|
|
2
|
-
|
|
3
|
-
from collections.abc import Callable
|
|
4
|
-
from functools import partial
|
|
5
|
-
from inspect import get_annotations, isroutine
|
|
6
|
-
from typing import Annotated, Any, TypeAliasType, get_args, get_origin
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def resolved_annotations(fn: Callable[..., Any]) -> dict[str, Any]:
|
|
10
|
-
"""Return the annotations describing a call to `fn`, with strings evaluated when possible.
|
|
11
|
-
|
|
12
|
-
For a class this is its `__init__`, for a callable object its `__call__`,
|
|
13
|
-
and for a `functools.partial` the function it wraps.
|
|
14
|
-
|
|
15
|
-
Example:
|
|
16
|
-
```python
|
|
17
|
-
>>> def wait(page, selector: "str", timeout: float = 5.0): ...
|
|
18
|
-
>>> resolved_annotations(wait)
|
|
19
|
-
{'selector': <class 'str'>, 'timeout': <class 'float'>}
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
"""
|
|
24
|
-
target = _annotated_function(fn)
|
|
25
|
-
|
|
26
|
-
try:
|
|
27
|
-
return get_annotations(target, eval_str=True)
|
|
28
|
-
except NameError:
|
|
29
|
-
return get_annotations(target)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def annotated_metadata(annotation: Any) -> tuple[Any, ...]:
|
|
33
|
-
"""Return the extra arguments of `Annotated[...]`, looking through `type` aliases.
|
|
34
|
-
|
|
35
|
-
Example:
|
|
36
|
-
```python
|
|
37
|
-
>>> type Port = Annotated[int, "tcp", 8080]
|
|
38
|
-
>>> annotated_metadata(Port)
|
|
39
|
-
('tcp', 8080)
|
|
40
|
-
>>> annotated_metadata(int)
|
|
41
|
-
()
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
""
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if
|
|
57
|
-
return
|
|
58
|
-
|
|
59
|
-
|
|
1
|
+
"""Read the annotations that carry markers such as `Parse` and `Depends`."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from functools import partial
|
|
5
|
+
from inspect import get_annotations, isroutine
|
|
6
|
+
from typing import Annotated, Any, TypeAliasType, get_args, get_origin
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def resolved_annotations(fn: Callable[..., Any]) -> dict[str, Any]:
|
|
10
|
+
"""Return the annotations describing a call to `fn`, with strings evaluated when possible.
|
|
11
|
+
|
|
12
|
+
For a class this is its `__init__`, for a callable object its `__call__`,
|
|
13
|
+
and for a `functools.partial` the function it wraps.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
```python
|
|
17
|
+
>>> def wait(page, selector: "str", timeout: float = 5.0): ...
|
|
18
|
+
>>> resolved_annotations(wait)
|
|
19
|
+
{'selector': <class 'str'>, 'timeout': <class 'float'>}
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
target = _annotated_function(fn)
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
return get_annotations(target, eval_str=True)
|
|
28
|
+
except NameError:
|
|
29
|
+
return get_annotations(target)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def annotated_metadata(annotation: Any) -> tuple[Any, ...]:
|
|
33
|
+
"""Return the extra arguments of `Annotated[...]`, looking through `type` aliases.
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
```python
|
|
37
|
+
>>> type Port = Annotated[int, "tcp", 8080]
|
|
38
|
+
>>> annotated_metadata(Port)
|
|
39
|
+
('tcp', 8080)
|
|
40
|
+
>>> annotated_metadata(int)
|
|
41
|
+
()
|
|
42
|
+
>>> type Tagged[T] = Annotated[T, "tag"]
|
|
43
|
+
>>> annotated_metadata(Tagged[int])
|
|
44
|
+
('tag',)
|
|
45
|
+
>>> annotated_metadata(Annotated[Tagged[Port], "outer"])
|
|
46
|
+
('tcp', 8080, 'tag', 'outer')
|
|
47
|
+
>>> type Fixed[T] = Annotated[int, "fixed"]
|
|
48
|
+
>>> annotated_metadata(Fixed[str])
|
|
49
|
+
('fixed',)
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
annotation = _alias_value(annotation)
|
|
55
|
+
|
|
56
|
+
if get_origin(annotation) is not Annotated:
|
|
57
|
+
return ()
|
|
58
|
+
|
|
59
|
+
inner, *metadata = get_args(annotation)
|
|
60
|
+
return (*annotated_metadata(inner), *metadata)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _alias_value(annotation: Any) -> Any:
|
|
64
|
+
"""Return what a `type` alias stands for, also when it is subscripted (`Alias[int]`)."""
|
|
65
|
+
while True:
|
|
66
|
+
origin = get_origin(annotation)
|
|
67
|
+
|
|
68
|
+
if isinstance(annotation, TypeAliasType):
|
|
69
|
+
annotation = annotation.__value__
|
|
70
|
+
elif isinstance(origin, TypeAliasType):
|
|
71
|
+
annotation = _substituted(origin.__value__, get_args(annotation))
|
|
72
|
+
else:
|
|
73
|
+
return annotation
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _substituted(value: Any, arguments: tuple[Any, ...]) -> Any:
|
|
77
|
+
try:
|
|
78
|
+
return value[arguments]
|
|
79
|
+
except TypeError: # the alias does not use its type parameters
|
|
80
|
+
return value
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _annotated_function(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
84
|
+
if isinstance(fn, partial):
|
|
85
|
+
return _annotated_function(fn.func)
|
|
86
|
+
|
|
87
|
+
if isinstance(fn, type):
|
|
88
|
+
return fn.__init__
|
|
89
|
+
|
|
90
|
+
return fn if isroutine(fn) else type(fn).__call__
|