Skip to content

Commit 725ac45

Browse files
authored
Add pants plugin (#11)
* Add pants codebase * Add pants plugin to exports * Set typed for pants plugin * Add version to pants plugin bin loading * Update README * Drop unused plugin code * Increment version
1 parent 5e8cadd commit 725ac45

File tree

7 files changed

+264
-1
lines changed

7 files changed

+264
-1
lines changed

README.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Easily generate type-safe and async Python applications from AsyncAPI 3 specific
2525
- [x] Supports publish-subscribe pattern
2626
- [ ] AsyncAPI trait support
2727
- [ ] Customizable message encoder/decoder
28+
- [x] Works as a plugin for [pantsbuild](https://pantsbuild.org) (see [instructions](#usage-as-a-pants-plugin) below)
2829

2930
## Requirements
3031

@@ -55,6 +56,102 @@ pip install asyncapi-python[amqp]
5556

5657
You can replace `amqp` with any other supported protocols. For more info, see [Supported Protocols](#supported-protocols--use-cases) section.
5758

59+
### Usage as a Pants plugin
60+
61+
> The following method was tested with pants version 2.23.1.
62+
> Pleas note that Pants plugin API is still in development and things might break.
63+
64+
This library can act as a plugin for [Pants](https://pantsbuild.org). More specifically, it creates a new target type: `asyncapi_python_service` -- which can be used like:
65+
66+
```python
67+
# BUILD
68+
asyncapi_python_service(
69+
name="asyncapi_app",
70+
service="app.asyncapi.yaml",
71+
sources=[
72+
"app.asyncapi.yaml",
73+
"lib.asyncapi.yaml",
74+
"commons.*.asyncapi.yaml"
75+
]
76+
)
77+
```
78+
79+
This will be generating python module named `asyncapi_app` on `codegen-export` and `export` goals.
80+
This target can later be used as a dependency of `python_sources`.
81+
82+
```python
83+
# BUILD
84+
python_sources(
85+
dependencies=[
86+
":asyncapi_app",
87+
":reqs",
88+
],
89+
)
90+
91+
python_requirements(
92+
name="reqs",
93+
)
94+
```
95+
96+
Note that this plugin does not do dependency injection, so asyncapi-python must be a dependency
97+
98+
```text
99+
# requirements.txt
100+
asyncapi-python[amqp]
101+
```
102+
103+
### Deploying this plugin into pants monorepo
104+
105+
In order to deploy this plugin into your pants monorepo, create the following structure inside your plugins folder:
106+
107+
```bash
108+
pants-plugins/
109+
└── asyncapi_python_plugin
110+
├── BUILD
111+
├── __init__.py
112+
├── register.py
113+
└── requirements.txt
114+
```
115+
116+
`requirements.txt` must contain:
117+
118+
```text
119+
asyncapi-python
120+
```
121+
122+
`register.py` should have:
123+
124+
```python
125+
from asyncapi_python_pants.register import *
126+
```
127+
128+
`BUILD` must include:
129+
130+
```python
131+
python_sources(
132+
dependencies=[":reqs"],
133+
)
134+
135+
python_requirements(
136+
name="reqs",
137+
)
138+
```
139+
140+
`__init__.py` can be empty, but it has to exist.
141+
142+
Finally, add `pants-plugins` to your `PYTHONPATH`, and add the created folder as a backend package:
143+
144+
```toml
145+
# pants.toml
146+
backend_packages = [
147+
"asyncapi_python_plugin",
148+
...
149+
]
150+
pythonpath = ["%(buildroot)s/pants-plugins"]
151+
```
152+
153+
When everything is done, test if the plugin works by running `pants export-codegen ::`
154+
58155
## Supported Protocols / Use Cases
59156

60157
Below, you may see the table of protocols and the supported use cases. The tick signs (✅) contain links to the examples for each implemented protocol-use case pair, while the hammer signs (🔨) contain links to the Issues tracking the progress for protocol-use case pairs. The list of protocols and use cases is expected to increase over the progress of development.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
[tool.poetry]
22
name = "asyncapi-python"
3-
version = "0.2.2"
3+
version = "0.2.3"
44
license = "Apache-2.0"
55
description = "Easily generate type-safe and async Python applications from AsyncAPI 3 specifications."
66
authors = ["Yaroslav Petrov <yaroslav.v.petrov@gmail.com>"]
77
readme = "README.md"
88
packages = [
9+
{ include = "asyncapi_python_pants", from = "src" },
910
{ include = "asyncapi_python_codegen", from = "src" },
1011
{ include = "asyncapi_python", from = "src" },
1112
]

src/asyncapi_python_pants/__init__.py

Whitespace-only changes.

src/asyncapi_python_pants/py.typed

Whitespace-only changes.

src/asyncapi_python_pants/register.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from pants.engine.rules import collect_rules
2+
from pants.engine.unions import UnionRule
3+
from pants.backend.python.util_rules import pex
4+
from pants.core.goals.resolves import ExportableTool
5+
6+
from .rules import *
7+
from .targets import *
8+
9+
10+
def rules():
11+
return [
12+
*collect_rules(),
13+
*pex.rules(),
14+
UnionRule(GenerateSourcesRequest, GeneratePythonFromAsyncapiRequest),
15+
]
16+
17+
18+
def target_types():
19+
return [AsyncapiServiceTarget]

src/asyncapi_python_pants/rules.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from importlib.metadata import version
2+
from pants.engine.internals.native_engine import (
3+
Digest,
4+
MergeDigests,
5+
RemovePrefix,
6+
AddPrefix,
7+
Snapshot,
8+
)
9+
from pants.core.util_rules.stripped_source_files import StrippedSourceFiles
10+
from pants.core.util_rules.source_files import SourceFilesRequest
11+
from pants.engine.target import (
12+
GeneratedSources,
13+
TransitiveTargets,
14+
TransitiveTargetsRequest,
15+
)
16+
from pants.engine.rules import rule, Get, MultiGet
17+
from pants.engine.process import ProcessResult
18+
from pants.source.source_root import SourceRoot, SourceRootRequest
19+
from pants.backend.python.target_types import ConsoleScript
20+
from pants.backend.python.util_rules.interpreter_constraints import (
21+
InterpreterConstraints,
22+
)
23+
from pants.backend.python.util_rules.pex import (
24+
Pex,
25+
PexProcess,
26+
PexRequest,
27+
PexRequirements,
28+
)
29+
from .targets import *
30+
31+
32+
@rule
33+
async def generate_python_from_asyncapi(
34+
request: GeneratePythonFromAsyncapiRequest,
35+
) -> GeneratedSources:
36+
pex = await Get(
37+
Pex,
38+
PexRequest(
39+
output_filename="asyncapi-python-codegen.pex",
40+
internal_only=True,
41+
requirements=PexRequirements(
42+
[f"asyncapi-python[codegen]=={version('asyncapi-python')}"]
43+
),
44+
interpreter_constraints=InterpreterConstraints([">=3.9"]),
45+
main=ConsoleScript("asyncapi-python-codegen"),
46+
),
47+
)
48+
transitive_targets = await Get(
49+
TransitiveTargets,
50+
TransitiveTargetsRequest([request.protocol_target.address]),
51+
)
52+
all_sources_stripped = await Get(
53+
StrippedSourceFiles,
54+
SourceFilesRequest(
55+
(tgt.get(AsyncapiSourcesField) for tgt in transitive_targets.closure),
56+
for_sources_types=(AsyncapiSourcesField,),
57+
),
58+
)
59+
input_digest = await Get(
60+
Digest,
61+
MergeDigests(
62+
(
63+
all_sources_stripped.snapshot.digest,
64+
pex.digest,
65+
)
66+
),
67+
)
68+
output_dir = "_generated_files"
69+
module_name = request.protocol_target.address.target_name
70+
result = await Get(
71+
ProcessResult,
72+
PexProcess(
73+
pex,
74+
argv=[
75+
request.protocol_target[AsyncapiServiceField].value or "",
76+
f"{output_dir}/{module_name}",
77+
],
78+
description=f"Generating Python sources from {request.protocol_target.address}.",
79+
output_directories=(output_dir,),
80+
input_digest=input_digest,
81+
),
82+
)
83+
source_root_request = SourceRootRequest.for_target(request.protocol_target)
84+
normalized_digest, source_root = await MultiGet(
85+
Get(Digest, RemovePrefix(result.output_digest, output_dir)),
86+
Get(SourceRoot, SourceRootRequest, source_root_request),
87+
)
88+
source_root_restored = (
89+
await Get(Snapshot, AddPrefix(normalized_digest, source_root.path))
90+
if source_root.path != "."
91+
else await Get(Snapshot, Digest, normalized_digest)
92+
)
93+
return GeneratedSources(source_root_restored)

src/asyncapi_python_pants/targets.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from pants.engine.target import GenerateSourcesRequest
2+
from pants.engine.target import (
3+
COMMON_TARGET_FIELDS,
4+
Dependencies,
5+
Target,
6+
AsyncFieldMixin,
7+
MultipleSourcesField,
8+
StringField,
9+
)
10+
from pants.backend.python.target_types import PythonSourceField
11+
from pants.backend.python.target_types import (
12+
InterpreterConstraintsField,
13+
PythonResolveField,
14+
)
15+
16+
17+
class AsyncapiPythonInterpreterConstraints(InterpreterConstraintsField): ...
18+
19+
20+
class AsyncapiPythonResolveField(PythonResolveField): ...
21+
22+
23+
class AsyncapiServiceField(StringField):
24+
alias = "service"
25+
26+
27+
class AsyncapiSourcesField(MultipleSourcesField, AsyncFieldMixin):
28+
expected_file_extensions = (".yaml", ".asyncapi.yaml")
29+
30+
31+
class AsyncapiDependencies(Dependencies): ...
32+
33+
34+
class AsyncapiServiceTarget(Target):
35+
alias = "asyncapi_python_service"
36+
help = "A single AsyncAPI file."
37+
core_fields = (
38+
*COMMON_TARGET_FIELDS,
39+
AsyncapiDependencies,
40+
AsyncapiSourcesField,
41+
AsyncapiServiceField,
42+
AsyncapiPythonInterpreterConstraints,
43+
AsyncapiPythonResolveField,
44+
)
45+
46+
47+
class InjectAsyncapiDependencies(Target):
48+
inject_for = AsyncapiDependencies
49+
50+
51+
class GeneratePythonFromAsyncapiRequest(GenerateSourcesRequest):
52+
input = AsyncapiSourcesField
53+
output = PythonSourceField

0 commit comments

Comments
 (0)