@shortlink-org/portolan 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +387 -0
- package/catalog/enum_test.go +46 -0
- package/catalog/model.go +932 -0
- package/catalog/roundtrip_test.go +185 -0
- package/catalog/via_test.go +38 -0
- package/cli/portolan.mjs +368 -0
- package/go.mod +14 -0
- package/go.sum +14 -0
- package/index.html +13 -0
- package/internal/commands/cargo.go +154 -0
- package/internal/commands/commands.go +143 -0
- package/internal/commands/commands_test.go +323 -0
- package/internal/commands/gradle.go +77 -0
- package/internal/commands/justfile.go +82 -0
- package/internal/commands/makefile.go +106 -0
- package/internal/commands/maven.go +93 -0
- package/internal/commands/packagejson.go +159 -0
- package/internal/commands/pyproject.go +169 -0
- package/internal/commands/taskfile.go +101 -0
- package/internal/commands/testdata/estate/.cargo/config.toml +7 -0
- package/internal/commands/testdata/estate/Makefile +33 -0
- package/internal/commands/testdata/estate/Taskfile.yml +21 -0
- package/internal/commands/testdata/estate/build.gradle.kts +21 -0
- package/internal/commands/testdata/estate/justfile +20 -0
- package/internal/commands/testdata/estate/package.json +13 -0
- package/internal/commands/testdata/estate/pom.xml +29 -0
- package/internal/commands/testdata/estate/pyproject.toml +27 -0
- package/internal/commands/testdata/estate/xtask/src/main.rs +27 -0
- package/internal/commands/testdata/golden/commands.json +305 -0
- package/internal/gohttp/analyze.go +2500 -0
- package/internal/gohttp/endpoints.go +1067 -0
- package/internal/gohttp/roots.go +320 -0
- package/internal/gohttp/typed.go +143 -0
- package/internal/goscan/constants.go +85 -0
- package/internal/goscan/goscan_test.go +227 -0
- package/internal/goscan/names.go +52 -0
- package/internal/goscan/parse_test.go +11 -0
- package/internal/goscan/source.go +36 -0
- package/internal/goscan/tree.go +155 -0
- package/internal/goscan/types.go +99 -0
- package/internal/wsdl/ids.go +127 -0
- package/internal/wsdl/ids_test.go +21 -0
- package/internal/wsdl/model.go +62 -0
- package/internal/wsdl/parse.go +920 -0
- package/internal/wsdl/parse_test.go +133 -0
- package/package.json +117 -0
- package/plugin/describe.go +107 -0
- package/plugin/describe_test.go +114 -0
- package/plugin/protocol.go +117 -0
- package/plugin/schematest/schematest.go +126 -0
- package/plugins/README.md +1160 -0
- package/plugins/extract-adr/describe.go +19 -0
- package/plugins/extract-adr/describe_test.go +11 -0
- package/plugins/extract-adr/extract.go +153 -0
- package/plugins/extract-adr/extract_test.go +350 -0
- package/plugins/extract-adr/history.go +99 -0
- package/plugins/extract-adr/main.go +65 -0
- package/plugins/extract-adr/options.schema.json +26 -0
- package/plugins/extract-adr/parse.go +642 -0
- package/plugins/extract-adr/parse_test.go +405 -0
- package/plugins/extract-asyncapi/describe.go +19 -0
- package/plugins/extract-asyncapi/describe_test.go +11 -0
- package/plugins/extract-asyncapi/extract.go +315 -0
- package/plugins/extract-asyncapi/extract_test.go +197 -0
- package/plugins/extract-asyncapi/main.go +48 -0
- package/plugins/extract-asyncapi/options.schema.json +26 -0
- package/plugins/extract-asyncapi/spec.go +151 -0
- package/plugins/extract-celery/README.md +169 -0
- package/plugins/extract-celery/extract.py +346 -0
- package/plugins/extract-celery/extract_test.py +192 -0
- package/plugins/extract-celery/main.py +66 -0
- package/plugins/extract-celery/options.py +31 -0
- package/plugins/extract-celery/options.schema.json +31 -0
- package/plugins/extract-celery/producers.py +156 -0
- package/plugins/extract-celery/routes_test.py +106 -0
- package/plugins/extract-commands/describe.go +19 -0
- package/plugins/extract-commands/describe_test.go +11 -0
- package/plugins/extract-commands/extract.go +79 -0
- package/plugins/extract-commands/extract_test.go +92 -0
- package/plugins/extract-commands/main.go +55 -0
- package/plugins/extract-commands/options.schema.json +20 -0
- package/plugins/extract-csr/avro.go +258 -0
- package/plugins/extract-csr/describe.go +19 -0
- package/plugins/extract-csr/describe_test.go +11 -0
- package/plugins/extract-csr/extract.go +338 -0
- package/plugins/extract-csr/extract_test.go +374 -0
- package/plugins/extract-csr/jsonschema.go +343 -0
- package/plugins/extract-csr/lock.go +25 -0
- package/plugins/extract-csr/main.go +84 -0
- package/plugins/extract-csr/options.schema.json +43 -0
- package/plugins/extract-csr/subject.go +102 -0
- package/plugins/extract-django/README.md +386 -0
- package/plugins/extract-django/apps.py +100 -0
- package/plugins/extract-django/auth.py +302 -0
- package/plugins/extract-django/auth_test.py +188 -0
- package/plugins/extract-django/clients.py +121 -0
- package/plugins/extract-django/contracts.py +458 -0
- package/plugins/extract-django/database.py +119 -0
- package/plugins/extract-django/domain.py +318 -0
- package/plugins/extract-django/events.py +86 -0
- package/plugins/extract-django/extract.py +601 -0
- package/plugins/extract-django/extract_test.py +397 -0
- package/plugins/extract-django/flows.py +1278 -0
- package/plugins/extract-django/flows_test.py +338 -0
- package/plugins/extract-django/ids.py +42 -0
- package/plugins/extract-django/ids_test.py +30 -0
- package/plugins/extract-django/lifecycle.py +222 -0
- package/plugins/extract-django/lifecycle_test.py +76 -0
- package/plugins/extract-django/main.py +66 -0
- package/plugins/extract-django/openapi.py +229 -0
- package/plugins/extract-django/openapi_test.py +60 -0
- package/plugins/extract-django/operations.py +96 -0
- package/plugins/extract-django/options.py +61 -0
- package/plugins/extract-django/options.schema.json +126 -0
- package/plugins/extract-django/routing.py +214 -0
- package/plugins/extract-django/routing_test.py +198 -0
- package/plugins/extract-django/serializers.py +558 -0
- package/plugins/extract-django/store.py +434 -0
- package/plugins/extract-django/transport.py +242 -0
- package/plugins/extract-flows/describe.go +19 -0
- package/plugins/extract-flows/describe_test.go +11 -0
- package/plugins/extract-flows/extract.go +91 -0
- package/plugins/extract-flows/main.go +45 -0
- package/plugins/extract-flows/options.schema.json +16 -0
- package/plugins/extract-flows/parse.go +593 -0
- package/plugins/extract-flows/parse_test.go +204 -0
- package/plugins/extract-glossary/describe.go +19 -0
- package/plugins/extract-glossary/describe_test.go +11 -0
- package/plugins/extract-glossary/extract.go +115 -0
- package/plugins/extract-glossary/extract_test.go +220 -0
- package/plugins/extract-glossary/main.go +59 -0
- package/plugins/extract-glossary/options.schema.json +21 -0
- package/plugins/extract-glossary/parse.go +214 -0
- package/plugins/extract-glossary/parse_test.go +203 -0
- package/plugins/extract-go/aggregate.go +214 -0
- package/plugins/extract-go/client.go +409 -0
- package/plugins/extract-go/client_test.go +266 -0
- package/plugins/extract-go/describe.go +19 -0
- package/plugins/extract-go/describe_test.go +11 -0
- package/plugins/extract-go/enum.go +195 -0
- package/plugins/extract-go/enum_test.go +82 -0
- package/plugins/extract-go/event.go +99 -0
- package/plugins/extract-go/extract.go +191 -0
- package/plugins/extract-go/extract_test.go +261 -0
- package/plugins/extract-go/flow.go +1441 -0
- package/plugins/extract-go/flow_test.go +609 -0
- package/plugins/extract-go/httpclient.go +174 -0
- package/plugins/extract-go/httpclient_test.go +296 -0
- package/plugins/extract-go/ids.go +92 -0
- package/plugins/extract-go/layout.go +225 -0
- package/plugins/extract-go/layout_test.go +125 -0
- package/plugins/extract-go/lifecycle.go +324 -0
- package/plugins/extract-go/lifecycle_test.go +108 -0
- package/plugins/extract-go/main.go +79 -0
- package/plugins/extract-go/operation.go +157 -0
- package/plugins/extract-go/options.schema.json +66 -0
- package/plugins/extract-go/source.go +316 -0
- package/plugins/extract-go/transport.go +321 -0
- package/plugins/extract-go/transport_test.go +145 -0
- package/plugins/extract-go/wiring.go +434 -0
- package/plugins/extract-go-nats/describe.go +19 -0
- package/plugins/extract-go-nats/describe_test.go +11 -0
- package/plugins/extract-go-nats/extract.go +177 -0
- package/plugins/extract-go-nats/extract_test.go +389 -0
- package/plugins/extract-go-nats/index.go +379 -0
- package/plugins/extract-go-nats/main.go +42 -0
- package/plugins/extract-go-nats/options.schema.json +21 -0
- package/plugins/extract-go-nats/resolve.go +161 -0
- package/plugins/extract-go-nats/sites.go +224 -0
- package/plugins/extract-graphql/describe.go +19 -0
- package/plugins/extract-graphql/describe_test.go +11 -0
- package/plugins/extract-graphql/extract.go +433 -0
- package/plugins/extract-graphql/extract_test.go +256 -0
- package/plugins/extract-graphql/ids.go +49 -0
- package/plugins/extract-graphql/lex.go +237 -0
- package/plugins/extract-graphql/main.go +51 -0
- package/plugins/extract-graphql/options.schema.json +31 -0
- package/plugins/extract-graphql/parse.go +621 -0
- package/plugins/extract-graphql/parse_test.go +122 -0
- package/plugins/extract-http-clients/describe.go +19 -0
- package/plugins/extract-http-clients/describe_test.go +11 -0
- package/plugins/extract-http-clients/extract.go +705 -0
- package/plugins/extract-http-clients/extract_test.go +1263 -0
- package/plugins/extract-http-clients/main.go +42 -0
- package/plugins/extract-http-clients/options.schema.json +33 -0
- package/plugins/extract-java/README.md +190 -0
- package/plugins/extract-java/build/org/portolan/extract/Catalog.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$2.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$Call.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$Client.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$Route.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Domain$Aggregate.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Domain.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Events$Found.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Events.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Binding.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Draft.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Frame.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Options.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Ids.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Json$Reader.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Json.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Lifecycle$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Lifecycle$Move.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Lifecycle.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/OpenApi$Operation.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/OpenApi$Spec.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/OpenApi.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Operations$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Operations$UseCase.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Operations.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Proto$Rpc.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Proto$Service.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Proto.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Source$Project.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Source$Unit.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Source.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Transport$Endpoint.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Transport.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Yaml$Line.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Yaml$Reader.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Yaml.class +0 -0
- package/plugins/extract-java/options.schema.json +70 -0
- package/plugins/extract-openapi/describe.go +19 -0
- package/plugins/extract-openapi/describe_test.go +11 -0
- package/plugins/extract-openapi/discover.go +243 -0
- package/plugins/extract-openapi/extract.go +526 -0
- package/plugins/extract-openapi/extract_test.go +545 -0
- package/plugins/extract-openapi/main.go +75 -0
- package/plugins/extract-openapi/options.schema.json +62 -0
- package/plugins/extract-openapi/spec.go +350 -0
- package/plugins/extract-project/README.md +22 -0
- package/plugins/extract-project/describe.go +19 -0
- package/plugins/extract-project/describe_test.go +11 -0
- package/plugins/extract-project/extract.go +221 -0
- package/plugins/extract-project/extract_test.go +109 -0
- package/plugins/extract-project/main.go +41 -0
- package/plugins/extract-project/options.schema.json +48 -0
- package/plugins/extract-proto/ast.go +125 -0
- package/plugins/extract-proto/consumes.go +77 -0
- package/plugins/extract-proto/describe.go +19 -0
- package/plugins/extract-proto/extract.go +293 -0
- package/plugins/extract-proto/extract_test.go +459 -0
- package/plugins/extract-proto/ids.go +89 -0
- package/plugins/extract-proto/ids_test.go +57 -0
- package/plugins/extract-proto/lex.go +285 -0
- package/plugins/extract-proto/main.go +100 -0
- package/plugins/extract-proto/module.go +120 -0
- package/plugins/extract-proto/options.schema.json +51 -0
- package/plugins/extract-proto/parse.go +720 -0
- package/plugins/extract-proto/parse_test.go +307 -0
- package/plugins/extract-proto/provides.go +236 -0
- package/plugins/extract-proto/resolve.go +222 -0
- package/plugins/extract-proto/resolve_test.go +119 -0
- package/plugins/extract-python-kafka/README.md +36 -0
- package/plugins/extract-python-kafka/extract.py +211 -0
- package/plugins/extract-python-kafka/extract_test.py +68 -0
- package/plugins/extract-python-kafka/main.py +59 -0
- package/plugins/extract-python-kafka/options.py +29 -0
- package/plugins/extract-python-kafka/options.schema.json +31 -0
- package/plugins/extract-redis/README.md +11 -0
- package/plugins/extract-redis/describe.go +19 -0
- package/plugins/extract-redis/describe_test.go +11 -0
- package/plugins/extract-redis/extract.go +183 -0
- package/plugins/extract-redis/extract_test.go +168 -0
- package/plugins/extract-redis/keyspaces.go +469 -0
- package/plugins/extract-redis/main.go +44 -0
- package/plugins/extract-redis/options.schema.json +30 -0
- package/plugins/extract-river/describe.go +19 -0
- package/plugins/extract-river/describe_test.go +11 -0
- package/plugins/extract-river/extract.go +507 -0
- package/plugins/extract-river/extract_test.go +132 -0
- package/plugins/extract-river/main.go +42 -0
- package/plugins/extract-river/options.schema.json +21 -0
- package/plugins/extract-rust/Cargo.lock +133 -0
- package/plugins/extract-rust/Cargo.toml +23 -0
- package/plugins/extract-rust/README.md +204 -0
- package/plugins/extract-rust/options.schema.json +65 -0
- package/plugins/extract-rust/rustfmt.toml +1 -0
- package/plugins/extract-sql/ddl.go +893 -0
- package/plugins/extract-sql/ddl_test.go +401 -0
- package/plugins/extract-sql/describe.go +19 -0
- package/plugins/extract-sql/describe_test.go +11 -0
- package/plugins/extract-sql/layout.go +221 -0
- package/plugins/extract-sql/layout_test.go +55 -0
- package/plugins/extract-sql/lineage.go +107 -0
- package/plugins/extract-sql/main.go +141 -0
- package/plugins/extract-sql/maps.go +564 -0
- package/plugins/extract-sql/maps_java.go +117 -0
- package/plugins/extract-sql/maps_rust.go +333 -0
- package/plugins/extract-sql/maps_rust_test.go +70 -0
- package/plugins/extract-sql/maps_test.go +204 -0
- package/plugins/extract-sql/maps_ts.go +398 -0
- package/plugins/extract-sql/maps_ts_test.go +136 -0
- package/plugins/extract-sql/options.schema.json +53 -0
- package/plugins/extract-sql/projection.go +70 -0
- package/plugins/extract-sql/projection_test.go +50 -0
- package/plugins/extract-sql/store.go +420 -0
- package/plugins/extract-sql/store_test.go +233 -0
- package/plugins/extract-sql/view.go +295 -0
- package/plugins/extract-ts/README.md +306 -0
- package/plugins/extract-ts/ast.test.ts +178 -0
- package/plugins/extract-ts/ast.ts +648 -0
- package/plugins/extract-ts/clients.ts +185 -0
- package/plugins/extract-ts/domain.ts +164 -0
- package/plugins/extract-ts/extract.test.ts +208 -0
- package/plugins/extract-ts/extract.ts +200 -0
- package/plugins/extract-ts/flows.ts +783 -0
- package/plugins/extract-ts/graphql.test.ts +73 -0
- package/plugins/extract-ts/graphql.ts +181 -0
- package/plugins/extract-ts/ids.test.ts +21 -0
- package/plugins/extract-ts/ids.ts +52 -0
- package/plugins/extract-ts/lifecycle.ts +138 -0
- package/plugins/extract-ts/main.ts +48 -0
- package/plugins/extract-ts/openapi.test.ts +64 -0
- package/plugins/extract-ts/openapi.ts +99 -0
- package/plugins/extract-ts/operations.ts +93 -0
- package/plugins/extract-ts/options.schema.json +96 -0
- package/plugins/extract-ts/source.test.ts +174 -0
- package/plugins/extract-ts/source.ts +465 -0
- package/plugins/extract-ts/transport.ts +147 -0
- package/plugins/extract-ts/tsconfig.json +13 -0
- package/plugins/extract-ts/wiring.ts +190 -0
- package/plugins/extract-watermill/describe.go +19 -0
- package/plugins/extract-watermill/describe_test.go +11 -0
- package/plugins/extract-watermill/extract.go +1228 -0
- package/plugins/extract-watermill/extract_test.go +234 -0
- package/plugins/extract-watermill/main.go +41 -0
- package/plugins/extract-watermill/options.schema.json +21 -0
- package/plugins/extract-wsdl/describe.go +19 -0
- package/plugins/extract-wsdl/describe_test.go +11 -0
- package/plugins/extract-wsdl/extract.go +148 -0
- package/plugins/extract-wsdl/extract_test.go +45 -0
- package/plugins/extract-wsdl/main.go +53 -0
- package/plugins/extract-wsdl/options.schema.json +59 -0
- package/plugins/fetch-bsr/auth.go +114 -0
- package/plugins/fetch-bsr/bsr.go +240 -0
- package/plugins/fetch-bsr/cache.go +66 -0
- package/plugins/fetch-bsr/describe.go +19 -0
- package/plugins/fetch-bsr/fetch.go +192 -0
- package/plugins/fetch-bsr/fetch_test.go +465 -0
- package/plugins/fetch-bsr/lock.go +71 -0
- package/plugins/fetch-bsr/main.go +117 -0
- package/plugins/fetch-bsr/options.schema.json +42 -0
- package/plugins/fetch-csr/auth.go +95 -0
- package/plugins/fetch-csr/cache.go +69 -0
- package/plugins/fetch-csr/describe.go +19 -0
- package/plugins/fetch-csr/describe_test.go +11 -0
- package/plugins/fetch-csr/fetch.go +254 -0
- package/plugins/fetch-csr/fetch_test.go +494 -0
- package/plugins/fetch-csr/lock.go +95 -0
- package/plugins/fetch-csr/main.go +122 -0
- package/plugins/fetch-csr/options.schema.json +42 -0
- package/plugins/fetch-csr/registry.go +209 -0
- package/plugins/fetch-git/cache.go +65 -0
- package/plugins/fetch-git/describe.go +19 -0
- package/plugins/fetch-git/describe_test.go +11 -0
- package/plugins/fetch-git/fetch.go +181 -0
- package/plugins/fetch-git/fetch_test.go +332 -0
- package/plugins/fetch-git/git.go +169 -0
- package/plugins/fetch-git/lock.go +72 -0
- package/plugins/fetch-git/main.go +127 -0
- package/plugins/fetch-git/offline.go +39 -0
- package/plugins/fetch-git/options.schema.json +42 -0
- package/plugins/fetch-git/pin.go +90 -0
- package/plugins/fetch-git/pin_test.go +74 -0
- package/plugins/gen-backstage/describe.go +17 -0
- package/plugins/gen-backstage/main.go +22 -0
- package/plugins/gen-backstage/options.schema.json +30 -0
- package/plugins/gen-backstage/plugin.go +473 -0
- package/plugins/gen-backstage/plugin_test.go +145 -0
- package/plugins/gen-backstage.wasm +0 -0
- package/plugins/gen-markdown/adr.go +162 -0
- package/plugins/gen-markdown/aggregate.go +362 -0
- package/plugins/gen-markdown/canonical.go +93 -0
- package/plugins/gen-markdown/context.go +90 -0
- package/plugins/gen-markdown/coverage_test.go +89 -0
- package/plugins/gen-markdown/describe.go +19 -0
- package/plugins/gen-markdown/describe_test.go +11 -0
- package/plugins/gen-markdown/external.go +71 -0
- package/plugins/gen-markdown/flow.go +277 -0
- package/plugins/gen-markdown/glossary.go +76 -0
- package/plugins/gen-markdown/glossary_test.go +148 -0
- package/plugins/gen-markdown/llms.go +302 -0
- package/plugins/gen-markdown/main.go +22 -0
- package/plugins/gen-markdown/markdown.go +254 -0
- package/plugins/gen-markdown/markdown_test.go +100 -0
- package/plugins/gen-markdown/module.go +107 -0
- package/plugins/gen-markdown/options.schema.json +16 -0
- package/plugins/gen-markdown/plugin.go +39 -0
- package/plugins/gen-markdown/quality_test.go +176 -0
- package/plugins/gen-markdown/redis_test.go +38 -0
- package/plugins/gen-markdown/render.go +344 -0
- package/plugins/gen-markdown/render_test.go +186 -0
- package/plugins/gen-markdown/service.go +456 -0
- package/plugins/gen-markdown/source.go +177 -0
- package/plugins/gen-markdown/store.go +201 -0
- package/plugins/gen-markdown.wasm +0 -0
- package/plugins/gen-mermaid/describe.go +17 -0
- package/plugins/gen-mermaid/main.go +22 -0
- package/plugins/gen-mermaid/options.schema.json +10 -0
- package/plugins/gen-mermaid/plugin.go +104 -0
- package/plugins/gen-mermaid/plugin_test.go +33 -0
- package/plugins/gen-mermaid.wasm +0 -0
- package/plugins/openapi/ids.go +261 -0
- package/plugins/openapi/ids_test.go +98 -0
- package/plugins/pyplugin/catalog.py +209 -0
- package/plugins/pyplugin/celery_conf.py +436 -0
- package/plugins/pyplugin/celery_tasks.py +76 -0
- package/plugins/pyplugin/kafka.py +570 -0
- package/plugins/pyplugin/names.py +38 -0
- package/plugins/pyplugin/protocol.py +79 -0
- package/plugins/pyplugin/source.py +285 -0
- package/plugins/verify-codeowners/describe.go +19 -0
- package/plugins/verify-codeowners/describe_test.go +11 -0
- package/plugins/verify-codeowners/main.go +75 -0
- package/plugins/verify-codeowners/match.go +85 -0
- package/plugins/verify-codeowners/match_test.go +47 -0
- package/plugins/verify-codeowners/options.schema.json +15 -0
- package/plugins/verify-codeowners/owners.go +164 -0
- package/plugins/verify-codeowners/owners_test.go +225 -0
- package/plugins/verify-codeowners/parse.go +90 -0
- package/plugins/verify-codeowners/parse_test.go +62 -0
- package/plugins/verify-otel/describe.go +19 -0
- package/plugins/verify-otel/describe_test.go +11 -0
- package/plugins/verify-otel/main.go +53 -0
- package/plugins/verify-otel/match.go +336 -0
- package/plugins/verify-otel/options.schema.json +30 -0
- package/plugins/verify-otel/otlp.go +200 -0
- package/plugins/verify-otel/verify.go +734 -0
- package/plugins/verify-otel/verify_test.go +460 -0
- package/portolan.json +1073 -0
- package/public/favicon.svg +12 -0
- package/schema/portolan.schema.json +2558 -0
- package/scripts/README.md +53 -0
- package/scripts/build-report.mjs +60 -0
- package/scripts/build-report.test.mjs +44 -0
- package/scripts/builtin-plugins.mjs +49 -0
- package/scripts/catalog-sources.mjs +67 -0
- package/scripts/diff.mjs +315 -0
- package/scripts/diff.test.mjs +151 -0
- package/scripts/forge-comment.mjs +144 -0
- package/scripts/forge-comment.test.mjs +151 -0
- package/scripts/forge-release.mjs +138 -0
- package/scripts/forge-release.test.mjs +106 -0
- package/scripts/forge.mjs +61 -0
- package/scripts/gen-likec4.mjs +854 -0
- package/scripts/gen-likec4.test.mjs +258 -0
- package/scripts/gen.mjs +603 -0
- package/scripts/generated-docs.test.mjs +66 -0
- package/scripts/local-api.mjs +1192 -0
- package/scripts/local-api.test.mjs +387 -0
- package/scripts/manifest.mjs +214 -0
- package/scripts/manifest.test.mjs +148 -0
- package/scripts/output-path.mjs +79 -0
- package/scripts/output-path.test.mjs +45 -0
- package/scripts/package-smoke.mjs +37 -0
- package/scripts/plugin-host.mjs +419 -0
- package/scripts/plugin-host.test.mjs +109 -0
- package/scripts/plugin-wasm-worker.mjs +34 -0
- package/scripts/run-builtin.mjs +81 -0
- package/scripts/schema.mjs +357 -0
- package/scripts/site-docs.mjs +295 -0
- package/scripts/site-docs.test.mjs +181 -0
- package/scripts/vendor-lock.mjs +58 -0
- package/scripts/vendor-lock.test.mjs +69 -0
- package/src/README.md +38 -0
- package/src/app/App.tsx +448 -0
- package/src/app/BranchPicker.test.ts +46 -0
- package/src/app/BranchPicker.tsx +153 -0
- package/src/app/Breadcrumbs.test.ts +38 -0
- package/src/app/Breadcrumbs.tsx +223 -0
- package/src/app/BuildStamp.tsx +146 -0
- package/src/app/CommandPalette.tsx +292 -0
- package/src/app/HashScroll.tsx +45 -0
- package/src/app/Sidebar.tsx +1742 -0
- package/src/app/TopBar.tsx +393 -0
- package/src/app/density.tsx +64 -0
- package/src/app/forge-access.test.ts +27 -0
- package/src/app/forge-access.tsx +80 -0
- package/src/app/panels.tsx +141 -0
- package/src/app/pins.tsx +242 -0
- package/src/app/query-client.ts +24 -0
- package/src/app/responsive.ts +55 -0
- package/src/app/shortcuts.test.ts +39 -0
- package/src/app/shortcuts.tsx +282 -0
- package/src/app/theme.tsx +51 -0
- package/src/app/title.test.ts +14 -0
- package/src/app/title.ts +30 -0
- package/src/app/toast.tsx +59 -0
- package/src/app/tree-keys.test.ts +80 -0
- package/src/app/tree-keys.ts +87 -0
- package/src/app/ui-store.ts +52 -0
- package/src/catalog-error.test.ts +105 -0
- package/src/catalog-profile.test.ts +59 -0
- package/src/catalog-profile.ts +101 -0
- package/src/catalog-stores.test.ts +431 -0
- package/src/catalog.test.ts +1215 -0
- package/src/catalog.ts +3125 -0
- package/src/chat/Cards.tsx +408 -0
- package/src/chat/ChatPanel.tsx +79 -0
- package/src/chat/ChatSettings.tsx +219 -0
- package/src/chat/Composer.tsx +99 -0
- package/src/chat/Conversation.tsx +281 -0
- package/src/chat/Header.tsx +46 -0
- package/src/chat/Message.tsx +49 -0
- package/src/chat/Notice.tsx +195 -0
- package/src/chat/Starter.tsx +67 -0
- package/src/chat/Steps.tsx +45 -0
- package/src/chat/Waiting.tsx +87 -0
- package/src/chat/flags.test.ts +59 -0
- package/src/chat/flags.ts +155 -0
- package/src/chat/linkify.test.ts +30 -0
- package/src/chat/linkify.ts +45 -0
- package/src/chat/prefs.ts +56 -0
- package/src/chat/prompt.test.ts +43 -0
- package/src/chat/prompt.ts +157 -0
- package/src/chat/store.ts +35 -0
- package/src/chat/tools.ts +58 -0
- package/src/chat/transport.ts +70 -0
- package/src/components/AdrRow.tsx +29 -0
- package/src/components/AnchorLink.tsx +67 -0
- package/src/components/ApiReference.tsx +134 -0
- package/src/components/AsyncApiReference.tsx +120 -0
- package/src/components/CatalogStamp.tsx +155 -0
- package/src/components/ChannelRows.tsx +132 -0
- package/src/components/CommandRows.tsx +139 -0
- package/src/components/DiagramSkeleton.tsx +71 -0
- package/src/components/EditorLink.tsx +85 -0
- package/src/components/FieldTree.tsx +305 -0
- package/src/components/FlowTrigger.tsx +34 -0
- package/src/components/Ident.tsx +89 -0
- package/src/components/Integrations.tsx +243 -0
- package/src/components/LifecycleDiagram.tsx +110 -0
- package/src/components/MachineDocs.tsx +42 -0
- package/src/components/Markdown.tsx +173 -0
- package/src/components/MarkdownTable.tsx +206 -0
- package/src/components/Mermaid.tsx +211 -0
- package/src/components/MethodRows.tsx +309 -0
- package/src/components/Overlay.tsx +125 -0
- package/src/components/PageHeader.tsx +189 -0
- package/src/components/RowActions.tsx +97 -0
- package/src/components/SchemaDocument.tsx +89 -0
- package/src/components/Select.tsx +126 -0
- package/src/components/ShapeRows.tsx +118 -0
- package/src/components/SourceDoc.tsx +82 -0
- package/src/components/SourcePreview.tsx +441 -0
- package/src/components/TabRow.tsx +182 -0
- package/src/components/TechIcon.tsx +34 -0
- package/src/components/Toc.tsx +132 -0
- package/src/components/WhatLinksHere.tsx +273 -0
- package/src/components/WsdlReference.tsx +84 -0
- package/src/components/ddd-icons.tsx +107 -0
- package/src/components/kind.tsx +157 -0
- package/src/components/logo.tsx +61 -0
- package/src/components/primitives.tsx +264 -0
- package/src/data.test.ts +18 -0
- package/src/data.ts +202 -0
- package/src/enrich.test.ts +1150 -0
- package/src/enrich.ts +672 -0
- package/src/er/ErCanvas.tsx +533 -0
- package/src/er/RedisSchema.tsx +74 -0
- package/src/er/RelationCard.tsx +267 -0
- package/src/er/StoreHeader.tsx +155 -0
- package/src/er/TableNode.tsx +84 -0
- package/src/er/ViewNode.tsx +83 -0
- package/src/er/layout.test.ts +115 -0
- package/src/er/layout.ts +77 -0
- package/src/er/lineage.test.ts +140 -0
- package/src/er/lineage.ts +107 -0
- package/src/er/markers.tsx +79 -0
- package/src/er/spec.test.ts +344 -0
- package/src/er/spec.ts +426 -0
- package/src/flow/ChainList.tsx +162 -0
- package/src/flow/FlowTable.tsx +123 -0
- package/src/flow/FlowToolbar.tsx +372 -0
- package/src/flow/StepDetail.tsx +380 -0
- package/src/flow/StepRail.tsx +428 -0
- package/src/flow/answers.test.ts +50 -0
- package/src/flow/answers.ts +58 -0
- package/src/flow/chain.test.ts +383 -0
- package/src/flow/chain.ts +353 -0
- package/src/flow/chapters.test.ts +185 -0
- package/src/flow/chapters.ts +225 -0
- package/src/flow/continues.test.ts +102 -0
- package/src/flow/continues.ts +65 -0
- package/src/flow/cross-context.test.ts +97 -0
- package/src/flow/cross-context.ts +35 -0
- package/src/flow/mermaid.test.ts +22 -0
- package/src/flow/mermaid.ts +141 -0
- package/src/flow/outline.test.ts +212 -0
- package/src/flow/outline.ts +170 -0
- package/src/flow/paths.test.ts +129 -0
- package/src/flow/paths.ts +147 -0
- package/src/flow/prefs.test.ts +80 -0
- package/src/flow/prefs.ts +104 -0
- package/src/graph/DependencyGraph.tsx +283 -0
- package/src/graph/DependencyNodes.tsx +162 -0
- package/src/graph/EdgeLabel.tsx +61 -0
- package/src/graph/FocusedEventGraph.tsx +211 -0
- package/src/graph/GraphToolbar.tsx +187 -0
- package/src/graph/RoutedEdge.tsx +80 -0
- package/src/graph/dependency-layout.test.ts +360 -0
- package/src/graph/dependency-layout.ts +444 -0
- package/src/graph/elk.test.ts +70 -0
- package/src/graph/elk.ts +287 -0
- package/src/graph/nodes.tsx +97 -0
- package/src/graph/theme.ts +81 -0
- package/src/graph/useElkFlow.ts +125 -0
- package/src/index.css +1547 -0
- package/src/language/InLanguage.tsx +40 -0
- package/src/language/TermCard.tsx +132 -0
- package/src/language/cards.test.ts +114 -0
- package/src/language/cards.ts +111 -0
- package/src/lib/admonition.test.ts +49 -0
- package/src/lib/admonition.ts +48 -0
- package/src/lib/adr.test.ts +131 -0
- package/src/lib/adr.ts +68 -0
- package/src/lib/api.test.ts +122 -0
- package/src/lib/api.ts +135 -0
- package/src/lib/backlinks.test.ts +330 -0
- package/src/lib/backlinks.ts +567 -0
- package/src/lib/branch-compare.ts +16 -0
- package/src/lib/build-info.test.ts +76 -0
- package/src/lib/build-info.ts +80 -0
- package/src/lib/catalog-diff.test.ts +275 -0
- package/src/lib/catalog-diff.ts +655 -0
- package/src/lib/clipboard.ts +43 -0
- package/src/lib/comparison-memory.test.ts +40 -0
- package/src/lib/comparison-memory.ts +39 -0
- package/src/lib/context-color.ts +27 -0
- package/src/lib/context-map.test.ts +148 -0
- package/src/lib/context-map.ts +399 -0
- package/src/lib/data-model.test.ts +245 -0
- package/src/lib/data-model.ts +399 -0
- package/src/lib/data-problems.test.ts +178 -0
- package/src/lib/data-problems.ts +314 -0
- package/src/lib/derive.test.ts +251 -0
- package/src/lib/derive.ts +515 -0
- package/src/lib/editor-link.test.ts +125 -0
- package/src/lib/editor-link.ts +96 -0
- package/src/lib/editor-prefs.ts +27 -0
- package/src/lib/event-graph.test.ts +237 -0
- package/src/lib/event-graph.ts +327 -0
- package/src/lib/export-canvas.ts +37 -0
- package/src/lib/export-file.test.ts +30 -0
- package/src/lib/export-file.ts +20 -0
- package/src/lib/flow-tree.test.ts +177 -0
- package/src/lib/flow-tree.ts +160 -0
- package/src/lib/forge-refs.test.ts +50 -0
- package/src/lib/forge-refs.ts +60 -0
- package/src/lib/format.test.ts +35 -0
- package/src/lib/format.ts +64 -0
- package/src/lib/github-catalog.test.ts +301 -0
- package/src/lib/github-catalog.ts +610 -0
- package/src/lib/integrations.test.ts +119 -0
- package/src/lib/integrations.ts +133 -0
- package/src/lib/kinds.test.ts +91 -0
- package/src/lib/kinds.ts +204 -0
- package/src/lib/lifecycle.test.ts +93 -0
- package/src/lib/lifecycle.ts +165 -0
- package/src/lib/local-api.ts +194 -0
- package/src/lib/motion-features.ts +11 -0
- package/src/lib/motion.test.ts +120 -0
- package/src/lib/motion.tsx +269 -0
- package/src/lib/palette-recent.test.ts +80 -0
- package/src/lib/palette-recent.ts +90 -0
- package/src/lib/palette.test.ts +365 -0
- package/src/lib/palette.ts +593 -0
- package/src/lib/pins.test.ts +139 -0
- package/src/lib/pins.ts +130 -0
- package/src/lib/problems.test.ts +112 -0
- package/src/lib/project-preview.ts +5 -0
- package/src/lib/proto-problems.test.ts +121 -0
- package/src/lib/proto-problems.ts +65 -0
- package/src/lib/queries.test.ts +113 -0
- package/src/lib/queries.ts +131 -0
- package/src/lib/registry.test.ts +219 -0
- package/src/lib/registry.ts +208 -0
- package/src/lib/scenarios.ts +547 -0
- package/src/lib/setup-info.test.ts +199 -0
- package/src/lib/setup-info.ts +415 -0
- package/src/lib/shape.test.ts +289 -0
- package/src/lib/shape.ts +454 -0
- package/src/lib/sidebar-prefs.test.ts +32 -0
- package/src/lib/sidebar-prefs.ts +75 -0
- package/src/lib/source-code.test.ts +159 -0
- package/src/lib/source-code.ts +286 -0
- package/src/lib/source-doc.test.ts +131 -0
- package/src/lib/source-doc.ts +103 -0
- package/src/lib/source-highlight.test.ts +29 -0
- package/src/lib/source-highlight.ts +24 -0
- package/src/lib/source-link.test.ts +153 -0
- package/src/lib/source-link.ts +201 -0
- package/src/lib/spec-files.test.ts +24 -0
- package/src/lib/spec-files.ts +23 -0
- package/src/lib/table-directive.test.ts +139 -0
- package/src/lib/table-directive.ts +106 -0
- package/src/lib/tech.test.ts +120 -0
- package/src/lib/tech.ts +421 -0
- package/src/lib/terms.test.ts +67 -0
- package/src/lib/terms.ts +179 -0
- package/src/lib/wire-problems.test.ts +263 -0
- package/src/lib/wire-problems.ts +281 -0
- package/src/likec4/C4View.tsx +55 -0
- package/src/likec4/CanvasBridge.tsx +187 -0
- package/src/likec4/FlowView.tsx +235 -0
- package/src/likec4/InteractiveView.tsx +144 -0
- package/src/likec4/canvas-viewport.test.ts +149 -0
- package/src/likec4/canvas-viewport.ts +135 -0
- package/src/likec4/flow-edges.test.ts +51 -0
- package/src/likec4/flow-edges.ts +89 -0
- package/src/likec4/frame-theme.test.ts +76 -0
- package/src/likec4/frame-theme.ts +101 -0
- package/src/likec4/highlight-css.test.ts +49 -0
- package/src/likec4/highlight-css.ts +52 -0
- package/src/likec4/ids.test.ts +151 -0
- package/src/likec4/ids.ts +115 -0
- package/src/likec4/levels.tsx +70 -0
- package/src/likec4/mapping.test.ts +66 -0
- package/src/likec4/mapping.ts +54 -0
- package/src/likec4/reserved.json +141 -0
- package/src/likec4/view-index.ts +78 -0
- package/src/main.tsx +13 -0
- package/src/map/ContextMapGraph.tsx +274 -0
- package/src/map/ContextNode.tsx +70 -0
- package/src/map/FloatingEdge.tsx +86 -0
- package/src/merge.test.ts +1038 -0
- package/src/merge.ts +1004 -0
- package/src/pages/AdrDetail.tsx +242 -0
- package/src/pages/AdrIndex.tsx +114 -0
- package/src/pages/AggregatePage.tsx +727 -0
- package/src/pages/BlockPage.tsx +478 -0
- package/src/pages/CatalogFailure.tsx +111 -0
- package/src/pages/Changes.tsx +313 -0
- package/src/pages/ContextMap.tsx +534 -0
- package/src/pages/ContextPage.tsx +346 -0
- package/src/pages/EnumPage.tsx +282 -0
- package/src/pages/EventPage.tsx +686 -0
- package/src/pages/ExternalPage.tsx +204 -0
- package/src/pages/FlowDetail.tsx +663 -0
- package/src/pages/FlowIndex.tsx +303 -0
- package/src/pages/GraphPage.tsx +217 -0
- package/src/pages/Language.tsx +257 -0
- package/src/pages/ModulePage.tsx +366 -0
- package/src/pages/NotFound.tsx +54 -0
- package/src/pages/Overview.tsx +344 -0
- package/src/pages/Problems.tsx +360 -0
- package/src/pages/RegistryIndex.tsx +118 -0
- package/src/pages/ServicePage.tsx +746 -0
- package/src/pages/Settings.tsx +975 -0
- package/src/pages/StorePage.tsx +114 -0
- package/src/peek/PeekCard.test.ts +35 -0
- package/src/peek/PeekCard.tsx +118 -0
- package/src/peek/PeekLayer.tsx +147 -0
- package/src/peek/model.test.ts +156 -0
- package/src/peek/model.ts +253 -0
- package/src/routes.test.ts +222 -0
- package/src/routes.ts +449 -0
- package/src/selection/DetailPanel.tsx +1407 -0
- package/src/selection/hash.test.ts +73 -0
- package/src/selection/hash.ts +58 -0
- package/src/selection/model.test.ts +199 -0
- package/src/selection/model.ts +473 -0
- package/src/selection/pages.test.ts +115 -0
- package/src/selection/pages.ts +268 -0
- package/src/selection/store.test.ts +62 -0
- package/src/selection/store.ts +46 -0
- package/src/selection/sync.tsx +99 -0
- package/src/table/ColumnsMenu.tsx +72 -0
- package/src/table/DataTable.tsx +780 -0
- package/src/table/Facets.tsx +54 -0
- package/src/table/GroupMenu.tsx +69 -0
- package/src/table/Toolbar.tsx +190 -0
- package/src/table/cells.tsx +117 -0
- package/src/table/compare.test.ts +117 -0
- package/src/table/compare.ts +196 -0
- package/src/table/export.test.ts +88 -0
- package/src/table/export.ts +66 -0
- package/src/table/facet-groups.test.ts +74 -0
- package/src/table/facet-groups.ts +56 -0
- package/src/table/grouping.test.ts +84 -0
- package/src/table/grouping.ts +82 -0
- package/src/table/infer.test.ts +57 -0
- package/src/table/infer.ts +50 -0
- package/src/table/persist.test.ts +76 -0
- package/src/table/persist.ts +112 -0
- package/src/table/sort-url.test.ts +75 -0
- package/src/table/sort-url.ts +52 -0
- package/src/table/types.ts +80 -0
- package/src/table/useTableState.ts +220 -0
- package/src/test-catalog.ts +7 -0
- package/src/testing/estate/data__catalog.json +2847 -0
- package/src/testing/estate/data__flows.json +1724 -0
- package/src/testing/estate/examples__auth__portolan__api.json +212 -0
- package/src/testing/estate/examples__auth__portolan__domain.json +1204 -0
- package/src/testing/estate/examples__auth__portolan__glossary.json +226 -0
- package/src/testing/estate/examples__auth__portolan__observed.json +757 -0
- package/src/testing/estate/examples__auth__portolan__stores.json +195 -0
- package/src/testing/estate/examples__shop__cart__portolan__api.json +234 -0
- package/src/testing/estate/examples__shop__cart__portolan__commands.json +86 -0
- package/src/testing/estate/examples__shop__cart__portolan__domain.json +966 -0
- package/src/testing/estate/examples__shop__cart__portolan__observed.json +498 -0
- package/src/testing/estate/examples__shop__cart__portolan__stores.json +208 -0
- package/src/testing/estate/examples__shop__oms__portolan__glossary.json +98 -0
- package/src/testing/estate/examples__shop__oms__portolan__proto.json +186 -0
- package/src/testing/estate.ts +49 -0
- package/src/trail/Trail.tsx +81 -0
- package/src/trail/model.test.ts +157 -0
- package/src/trail/model.ts +189 -0
- package/src/trail/record.tsx +32 -0
- package/src/trail/store.test.ts +66 -0
- package/src/trail/store.ts +102 -0
- package/tsconfig.json +35 -0
- package/vite.config.ts +171 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""The reader, held to two fixtures.
|
|
2
|
+
|
|
3
|
+
`testdata/billing` is a Django project with Celery in it, each shape this
|
|
4
|
+
plugin claims to read present once; `testdata/drift` is the tree where every
|
|
5
|
+
diagnostic the reader is meant to raise is raised once. `expected.json` beside
|
|
6
|
+
each is what it comes out as. Set UPDATE_GOLDEN=1 to write them again after a
|
|
7
|
+
deliberate change, and read the diff.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import unittest
|
|
14
|
+
|
|
15
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
16
|
+
sys.path.insert(1, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "pyplugin"))
|
|
17
|
+
|
|
18
|
+
import main # noqa: E402
|
|
19
|
+
from extract import extract # noqa: E402
|
|
20
|
+
from options import Options # noqa: E402
|
|
21
|
+
from protocol import Builder, Input # noqa: E402
|
|
22
|
+
|
|
23
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
24
|
+
ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def run(fixture, options):
|
|
28
|
+
b = Builder()
|
|
29
|
+
extract(
|
|
30
|
+
Input(root=os.path.relpath(os.path.join(HERE, "testdata", fixture), ROOT), commit="abc1234", generated_at="2026-09-05T00:00:00Z"),
|
|
31
|
+
Options.of(options),
|
|
32
|
+
b,
|
|
33
|
+
cwd=ROOT,
|
|
34
|
+
)
|
|
35
|
+
return {f.name: f.contents for f in b.files}, b.warnings
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def golden(test, fixture, contents):
|
|
39
|
+
path = os.path.join(HERE, "testdata", fixture, "expected.json")
|
|
40
|
+
if os.environ.get("UPDATE_GOLDEN"):
|
|
41
|
+
with open(path, "w", encoding="utf-8") as handle:
|
|
42
|
+
handle.write(contents)
|
|
43
|
+
with open(path, "r", encoding="utf-8") as handle:
|
|
44
|
+
test.assertEqual(json.loads(handle.read()), json.loads(contents))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Billing(unittest.TestCase):
|
|
48
|
+
def setUp(self):
|
|
49
|
+
self.files, self.warnings = run("billing", {"context": "shop", "service": "billing"})
|
|
50
|
+
self.fragment = json.loads(self.files["celery.json"])
|
|
51
|
+
self.service = self.fragment["contexts"][0]["services"][0]
|
|
52
|
+
self.channels = {c["address"]: c for c in self.service["channels"]}
|
|
53
|
+
self.flows = {f["slug"]: f for f in self.fragment["flows"]}
|
|
54
|
+
|
|
55
|
+
def test_the_fragment_it_reads(self):
|
|
56
|
+
golden(self, "billing", self.files["celery.json"])
|
|
57
|
+
|
|
58
|
+
def test_a_tree_where_everything_joins_reports_nothing(self):
|
|
59
|
+
self.assertEqual(self.warnings, [])
|
|
60
|
+
|
|
61
|
+
def test_a_queue_is_a_job_channel_with_a_send_and_a_receive_per_task(self):
|
|
62
|
+
self.assertEqual(sorted(self.channels), ["billing", "billing.mail", "billing.slow"])
|
|
63
|
+
mail = self.channels["billing.mail"]
|
|
64
|
+
self.assertEqual(mail["kind"], "job")
|
|
65
|
+
self.assertEqual([(m["name"], m["direction"]) for m in mail["messages"]], [("invoices.tasks.send_invoice_email", "send"), ("invoices.tasks.send_invoice_email", "receive")])
|
|
66
|
+
self.assertIn("over redis", mail["doc"])
|
|
67
|
+
|
|
68
|
+
def test_a_queue_is_decided_the_way_celery_decides_it(self):
|
|
69
|
+
receive = {c: [m for m in ch["messages"] if m["direction"] == "receive"][0]["doc"] for c, ch in self.channels.items()}
|
|
70
|
+
self.assertIn("routed by task_routes", receive["billing.mail"])
|
|
71
|
+
self.assertIn("named on the task", receive["billing.slow"])
|
|
72
|
+
self.assertIn("the default queue", receive["billing"])
|
|
73
|
+
|
|
74
|
+
def test_a_name_given_on_the_decorator_is_the_wire_name(self):
|
|
75
|
+
self.assertEqual(self.channels["billing"]["messages"][0]["name"], "billing.archive_invoice")
|
|
76
|
+
|
|
77
|
+
def test_an_enqueue_inside_on_commit_is_noted_and_a_countdown_too(self):
|
|
78
|
+
self.assertTrue(self.flows["billing-celery-send-invoice-email"]["steps"][0]["note"].startswith("Enqueued after the transaction commits."))
|
|
79
|
+
self.assertTrue(self.flows["billing-celery-remind-unpaid-invoice"]["steps"][0]["note"].startswith("Enqueued for later, with a countdown or an eta."))
|
|
80
|
+
|
|
81
|
+
def test_the_partial_form_and_the_module_import_resolve_to_the_task(self):
|
|
82
|
+
step = self.flows["billing-celery-archive-invoice"]["steps"][0]
|
|
83
|
+
self.assertTrue(step["line"].endswith("invoices/handlers.py:15"))
|
|
84
|
+
self.assertIn("Also enqueued at plugins/extract-celery/testdata/billing/invoices/services.py:23.", step["note"])
|
|
85
|
+
|
|
86
|
+
def test_the_clock_is_a_producer_and_opens_a_flow_of_its_own(self):
|
|
87
|
+
# Nothing in the code enqueues close_stale_drafts: add_periodic_task does.
|
|
88
|
+
flow = self.flows["billing-celery-close-stale-drafts"]
|
|
89
|
+
self.assertEqual([p["id"] for p in flow["participants"]], ["celery-beat", "shop.billing", "celery-billing"])
|
|
90
|
+
self.assertEqual([(s["from"], s["to"], s["label"]) for s in flow["steps"]], [("celery-beat", "celery-billing", "enqueue close_stale_drafts"), ("celery-billing", "shop.billing", "close_stale_drafts")])
|
|
91
|
+
self.assertEqual(flow["trigger"], {"kind": "scheduled", "label": "cron 0 3 * * *", "confidence": "high"})
|
|
92
|
+
self.assertTrue(flow["steps"][0]["line"].endswith("config/celery.py:22"))
|
|
93
|
+
self.assertTrue(flow["steps"][0]["note"].startswith("Scheduled cron 0 3 * * * (`close-stale-drafts`)."))
|
|
94
|
+
sent = [m for m in self.channels["billing"]["messages"] if m["name"] == "invoices.tasks.close_stale_drafts"]
|
|
95
|
+
self.assertEqual([m["direction"] for m in sent], ["send", "receive"])
|
|
96
|
+
self.assertTrue(sent[0]["doc"].startswith("Sent by Celery beat, cron 0 3 * * * (`close-stale-drafts`)."))
|
|
97
|
+
self.assertNotIn("Nothing in this tree enqueues it.", sent[1]["doc"])
|
|
98
|
+
|
|
99
|
+
def test_a_task_the_code_and_the_clock_both_enqueue_has_two_flows(self):
|
|
100
|
+
self.assertIn("billing-celery-remind-unpaid-invoice", self.flows)
|
|
101
|
+
beat = self.flows["billing-celery-remind-unpaid-invoice-beat"]
|
|
102
|
+
self.assertEqual(beat["trigger"]["label"], "every 6h")
|
|
103
|
+
self.assertEqual(beat["steps"][1]["handoff"], self.flows["billing-celery-remind-unpaid-invoice"]["steps"][1]["handoff"])
|
|
104
|
+
|
|
105
|
+
def test_the_settings_entries_are_read_too(self):
|
|
106
|
+
archive = self.flows["billing-celery-archive-invoice-beat"]
|
|
107
|
+
self.assertEqual(archive["participants"][2]["id"], "celery-billing")
|
|
108
|
+
self.assertEqual(archive["trigger"]["label"], "cron 30 2 * * *")
|
|
109
|
+
self.assertTrue(archive["source"].endswith("config/settings.py"))
|
|
110
|
+
|
|
111
|
+
def test_a_flow_goes_out_to_the_broker_and_comes_back(self):
|
|
112
|
+
flow = self.flows["billing-celery-send-invoice-email"]
|
|
113
|
+
self.assertEqual([p["id"] for p in flow["participants"]], ["shop.billing", "celery-billing-mail"])
|
|
114
|
+
self.assertEqual([(s["from"], s["to"], s["label"]) for s in flow["steps"]], [("shop.billing", "celery-billing-mail", "enqueue send_invoice_email"), ("celery-billing-mail", "shop.billing", "send_invoice_email")])
|
|
115
|
+
self.assertTrue(flow["steps"][1]["line"].endswith("invoices/tasks.py:13"))
|
|
116
|
+
self.assertEqual(flow["trigger"], {"kind": "job", "label": "Celery · billing.mail", "confidence": "high"})
|
|
117
|
+
self.assertEqual(
|
|
118
|
+
[(step["handoff"]["direction"], step["handoff"]["message"]) for step in flow["steps"]],
|
|
119
|
+
[("send", "invoices.tasks.send_invoice_email"), ("receive", "invoices.tasks.send_invoice_email")],
|
|
120
|
+
)
|
|
121
|
+
self.assertEqual(flow["steps"][1]["continuesAt"], "python:invoices.tasks:send_invoice_email")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class Drift(unittest.TestCase):
|
|
125
|
+
def setUp(self):
|
|
126
|
+
self.files, self.warnings = run("drift", {"context": "ops", "service": "reports"})
|
|
127
|
+
self.fragment = json.loads(self.files["celery.json"])
|
|
128
|
+
self.channels = {c["address"]: c for c in self.fragment["contexts"][0]["services"][0]["channels"]}
|
|
129
|
+
|
|
130
|
+
def test_the_fragment_it_reads(self):
|
|
131
|
+
golden(self, "drift", self.files["celery.json"])
|
|
132
|
+
|
|
133
|
+
def test_every_diagnostic_once(self):
|
|
134
|
+
self.assertEqual(
|
|
135
|
+
[(w.ref.split("/")[-1], w.message.split(",")[0].split(":")[0]) for w in self.warnings],
|
|
136
|
+
[
|
|
137
|
+
("ops.reports", "settings module `config.settings` not found"),
|
|
138
|
+
("ops.reports", "task_routes pattern `reports.tasks.*` matches no task in this tree"),
|
|
139
|
+
("services.py:19", "`mail.tasks.send_receipt` is sent as a task no function in this tree declares; recorded as a send with no handler here"),
|
|
140
|
+
("services.py:23", "`helper` is enqueued"),
|
|
141
|
+
("celery.py:15", "beat entry `nightly` schedules `reports.tasks.nightly`"),
|
|
142
|
+
("celery.py:16", "beat entry `rebuild` has a schedule this reader cannot read as syntax"),
|
|
143
|
+
("tasks.py:19", "task `jobs.tasks.orphan` is declared"),
|
|
144
|
+
],
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def test_the_app_module_configures_the_queue_when_the_settings_are_missing(self):
|
|
148
|
+
self.assertEqual(sorted(self.channels), ["mail", "rebuild", "reports.default"])
|
|
149
|
+
self.assertIn("over amqp", self.channels["mail"]["doc"])
|
|
150
|
+
|
|
151
|
+
def test_a_chain_is_one_enqueue_per_signature_noted_as_such(self):
|
|
152
|
+
notes = [f["steps"][0]["note"] for f in self.fragment["flows"] if f["trigger"]["kind"] == "job"]
|
|
153
|
+
self.assertTrue(all(n.startswith("Enqueued in a chain.") for n in notes))
|
|
154
|
+
self.assertEqual(len(notes), 2)
|
|
155
|
+
|
|
156
|
+
def test_a_task_sent_by_name_to_another_tree_is_a_send_with_no_handler(self):
|
|
157
|
+
self.assertEqual([m["direction"] for m in self.channels["mail"]["messages"]], ["send"])
|
|
158
|
+
self.assertEqual([f["slug"] for f in self.fragment["flows"]], ["reports-celery-build-report", "reports-celery-build-report-beat", "reports-celery-deliver-report"])
|
|
159
|
+
|
|
160
|
+
def test_an_opaque_schedule_is_kept_as_written_and_the_entry_options_place_the_task(self):
|
|
161
|
+
beat = [f for f in self.fragment["flows"] if f["slug"] == "reports-celery-build-report-beat"][0]
|
|
162
|
+
self.assertEqual(beat["trigger"], {"kind": "scheduled", "label": "every_other_day()", "confidence": "high"})
|
|
163
|
+
# The code enqueues build_report on the default queue; the entry's
|
|
164
|
+
# options put the scheduled run on one of its own.
|
|
165
|
+
self.assertEqual(beat["participants"][2]["id"], "celery-rebuild")
|
|
166
|
+
self.assertEqual([m["direction"] for m in self.channels["rebuild"]["messages"]], ["send", "receive"])
|
|
167
|
+
|
|
168
|
+
def test_a_task_nothing_enqueues_is_still_worked(self):
|
|
169
|
+
orphan = [m for m in self.channels["reports.default"]["messages"] if m["name"] == "jobs.tasks.orphan"]
|
|
170
|
+
self.assertEqual([m["direction"] for m in orphan], ["receive"])
|
|
171
|
+
self.assertIn("Nothing in this tree enqueues it.", orphan[0]["doc"])
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class Protocol(unittest.TestCase):
|
|
175
|
+
def test_describe_answers_with_the_options_schema(self):
|
|
176
|
+
out = json.loads(main.serve(json.dumps({"portolanVersion": "0.1.0", "kind": "describe"})))
|
|
177
|
+
self.assertEqual(out["files"], [])
|
|
178
|
+
self.assertEqual(out["describe"]["name"], "extract-celery")
|
|
179
|
+
self.assertEqual(out["describe"]["options"]["additionalProperties"], False)
|
|
180
|
+
self.assertIn("settings", out["describe"]["options"]["properties"])
|
|
181
|
+
|
|
182
|
+
def test_an_option_nobody_reads_is_refused_rather_than_dropped(self):
|
|
183
|
+
with self.assertRaises(ValueError):
|
|
184
|
+
Options.of({"context": "shop", "setings": "config.settings"})
|
|
185
|
+
|
|
186
|
+
def test_no_tasks_is_said_rather_than_an_empty_fragment_nobody_questions(self):
|
|
187
|
+
_, warnings = run("billing", {"context": "shop", "service": "billing", "source": "config"})
|
|
188
|
+
self.assertIn("no Celery tasks under", warnings[0].message)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
if __name__ == "__main__":
|
|
192
|
+
unittest.main()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""portolan-extract-celery: a tree with Celery in it in, work queues and job
|
|
2
|
+
flows out.
|
|
3
|
+
|
|
4
|
+
The same protocol as every other extractor: one JSON request on stdin, one
|
|
5
|
+
JSON response on stdout, and a `describe` that answers with what the plugin is
|
|
6
|
+
and what it can be told. Python 3.9 or newer, and nothing but the standard
|
|
7
|
+
library: the project is never imported, and neither is Celery.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
17
|
+
sys.path.insert(1, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "pyplugin"))
|
|
18
|
+
|
|
19
|
+
from extract import extract # noqa: E402
|
|
20
|
+
from options import Options # noqa: E402
|
|
21
|
+
from protocol import Builder, Input # noqa: E402
|
|
22
|
+
|
|
23
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
24
|
+
|
|
25
|
+
DESCRIPTOR = {
|
|
26
|
+
"name": "extract-celery",
|
|
27
|
+
"summary": "Reads Celery task declarations, the calls that enqueue them and the routes that place them into work queues and source-backed job flows.",
|
|
28
|
+
"phases": ["extract"],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def descriptor():
|
|
33
|
+
with open(os.path.join(HERE, "options.schema.json"), "r", encoding="utf-8") as handle:
|
|
34
|
+
out = dict(DESCRIPTOR)
|
|
35
|
+
out["options"] = json.load(handle)
|
|
36
|
+
return out
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def serve(raw: str) -> str:
|
|
40
|
+
request = json.loads(raw)
|
|
41
|
+
version = request.get("portolanVersion", "")
|
|
42
|
+
if version and version != "0.1.0":
|
|
43
|
+
raise ValueError("unsupported portolan protocol %r (plugin supports 0.1.0)" % version)
|
|
44
|
+
if request.get("kind") == "describe":
|
|
45
|
+
return json.dumps({"files": [], "describe": descriptor()})
|
|
46
|
+
input_ = Input.of(request.get("input"))
|
|
47
|
+
if not input_.root:
|
|
48
|
+
raise ValueError("no input root: an extractor has nothing to read")
|
|
49
|
+
builder = Builder()
|
|
50
|
+
extract(input_, Options.of(request.get("options")), builder)
|
|
51
|
+
for warning in builder.warnings:
|
|
52
|
+
sys.stderr.write("warning: %s%s\n" % ((warning.ref + ": ") if warning.ref else "", warning.message))
|
|
53
|
+
return json.dumps(builder.response(), indent=2)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def main() -> int:
|
|
57
|
+
try:
|
|
58
|
+
sys.stdout.write(serve(sys.stdin.read()))
|
|
59
|
+
except Exception as err: # the host reads a non-zero exit and the message
|
|
60
|
+
sys.stderr.write("portolan-extract-celery: %s\n" % err)
|
|
61
|
+
return 1
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if __name__ == "__main__":
|
|
66
|
+
sys.exit(main())
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""What the manifest tells the extractor: the things a tree with Celery in it
|
|
2
|
+
does not say about the estate it belongs to."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from protocol import read_options
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Options:
|
|
14
|
+
context: str = ""
|
|
15
|
+
service: str = ""
|
|
16
|
+
source: str = "."
|
|
17
|
+
settings: str = ""
|
|
18
|
+
out: str = "celery.json"
|
|
19
|
+
|
|
20
|
+
# The option as the manifest spells it, against the field that holds it.
|
|
21
|
+
KEYS = {
|
|
22
|
+
"context": "context",
|
|
23
|
+
"service": "service",
|
|
24
|
+
"source": "source",
|
|
25
|
+
"settings": "settings",
|
|
26
|
+
"out": "out",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def of(raw: Any) -> "Options":
|
|
31
|
+
return read_options(Options(), Options.KEYS, raw)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "object",
|
|
3
|
+
"additionalProperties": false,
|
|
4
|
+
"properties": {
|
|
5
|
+
"context": {
|
|
6
|
+
"type": "string",
|
|
7
|
+
"description": "Slug of the bounded context the service belongs to. Defaults to the name of the input directory.",
|
|
8
|
+
"examples": ["shop"]
|
|
9
|
+
},
|
|
10
|
+
"service": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "Slug of the service that enqueues and works the tasks. Defaults to the name of the input directory.",
|
|
13
|
+
"examples": ["billing"]
|
|
14
|
+
},
|
|
15
|
+
"source": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"description": "The directory the tasks and their callers are looked for in, relative to the input root.",
|
|
18
|
+
"default": "."
|
|
19
|
+
},
|
|
20
|
+
"settings": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "The Django settings module the Celery app is configured from, as an import spells it. Defaults to what manage.py sets DJANGO_SETTINGS_MODULE to.",
|
|
23
|
+
"examples": ["config.settings"]
|
|
24
|
+
},
|
|
25
|
+
"out": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"description": "Name of the fragment file, inside the step's output directory.",
|
|
28
|
+
"default": "celery.json"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""The calls that put a task on a queue.
|
|
2
|
+
|
|
3
|
+
`t.delay(...)` and `t.apply_async(...)` are the two ways a task is enqueued,
|
|
4
|
+
and `t.s(...).apply_async()` is the same call through a signature. A
|
|
5
|
+
`chain`, `group` or `chord` holds several signatures, each of which is an
|
|
6
|
+
enqueue and is recorded as one, noted with the canvas it sits in; the order
|
|
7
|
+
the canvas imposes is not drawn. `app.send_task("name")` enqueues by wire
|
|
8
|
+
name, for a task that may live in another tree. `transaction.on_commit(lambda:
|
|
9
|
+
t.delay(...))` - and the `partial` form - is the same enqueue with a note that
|
|
10
|
+
it waits for the commit, which is the one fact about *when* a message leaves
|
|
11
|
+
that the code states plainly.
|
|
12
|
+
|
|
13
|
+
Names are resolved by import and by file, so `tasks.send_invoice_email.delay`
|
|
14
|
+
and `send_invoice_email.delay` after `from .tasks import send_invoice_email`
|
|
15
|
+
land on the same function.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import ast
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import List, Optional, Tuple
|
|
23
|
+
|
|
24
|
+
from source import Module, Project, const_str, dotted, keyword, keyword_str
|
|
25
|
+
from celery_tasks import package_of
|
|
26
|
+
|
|
27
|
+
ENQUEUE = ("delay", "apply_async")
|
|
28
|
+
SIGNATURE = ("s", "si", "signature", "subtask")
|
|
29
|
+
CANVAS = {"chain": "in a chain", "group": "in a group", "chord": "in a chord"}
|
|
30
|
+
AFTER_COMMIT = "after the transaction commits"
|
|
31
|
+
LATER = "for later, with a countdown or an eta"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Producer:
|
|
36
|
+
module: Module
|
|
37
|
+
node: ast.Call
|
|
38
|
+
key: Optional[Tuple[str, str]] # the task's, when the call resolves to one
|
|
39
|
+
wire: str # the name `send_task` was given, "" otherwise
|
|
40
|
+
label: str # the callee as written, last segment
|
|
41
|
+
queue: str # `queue=` at the call, "" when none
|
|
42
|
+
notes: List[str]
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def line(self) -> str:
|
|
46
|
+
return self.module.where(self.node)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def read_producers(project: Project) -> List[Producer]:
|
|
50
|
+
out: List[Producer] = []
|
|
51
|
+
for module in sorted(project.modules.values(), key=lambda m: m.rel):
|
|
52
|
+
Visitor(project, module, out).visit(module.tree)
|
|
53
|
+
return out
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Visitor(ast.NodeVisitor):
|
|
57
|
+
def __init__(self, project: Project, module: Module, out: List[Producer]):
|
|
58
|
+
self.project = project
|
|
59
|
+
self.module = module
|
|
60
|
+
self.out = out
|
|
61
|
+
self.pending: List[str] = [] # notes the enclosing calls add
|
|
62
|
+
|
|
63
|
+
def visit_Call(self, node: ast.Call) -> None:
|
|
64
|
+
name = dotted(node.func)
|
|
65
|
+
last = name.split(".")[-1]
|
|
66
|
+
if last == "on_commit":
|
|
67
|
+
self.pending.append(AFTER_COMMIT)
|
|
68
|
+
self.generic_visit(node)
|
|
69
|
+
self.pending.pop()
|
|
70
|
+
return
|
|
71
|
+
if last == "partial" and node.args and isinstance(node.args[0], ast.Attribute) and node.args[0].attr in ENQUEUE:
|
|
72
|
+
self.enqueue(node.args[0].value, node)
|
|
73
|
+
for arg in node.args[1:]:
|
|
74
|
+
self.visit(arg)
|
|
75
|
+
for kw in node.keywords:
|
|
76
|
+
self.visit(kw.value)
|
|
77
|
+
return
|
|
78
|
+
if last in ENQUEUE and isinstance(node.func, ast.Attribute):
|
|
79
|
+
self.enqueue(node.func.value, node)
|
|
80
|
+
self.generic_visit(node)
|
|
81
|
+
return
|
|
82
|
+
if last == "send_task" and node.args and const_str(node.args[0]):
|
|
83
|
+
wire = const_str(node.args[0])
|
|
84
|
+
self.out.append(Producer(self.module, node, None, wire, wire, keyword_str(node, "queue"), self.notes(node)))
|
|
85
|
+
self.generic_visit(node)
|
|
86
|
+
return
|
|
87
|
+
# A canvas called outright: `chain(a.s(), b.s())()`.
|
|
88
|
+
if isinstance(node.func, ast.Call) and dotted(node.func.func).split(".")[-1] in CANVAS:
|
|
89
|
+
self.enqueue(node.func, node)
|
|
90
|
+
return
|
|
91
|
+
self.generic_visit(node)
|
|
92
|
+
|
|
93
|
+
def enqueue(self, receiver: ast.AST, call: ast.Call, extra: str = "") -> None:
|
|
94
|
+
"""`receiver` is what `.delay` or `.apply_async` was called on: the task,
|
|
95
|
+
a signature of it, or a canvas of signatures."""
|
|
96
|
+
if isinstance(receiver, ast.Call) and isinstance(receiver.func, ast.Attribute) and receiver.func.attr in SIGNATURE:
|
|
97
|
+
self.record(receiver.func.value, call, extra)
|
|
98
|
+
return
|
|
99
|
+
if isinstance(receiver, ast.Call) and dotted(receiver.func).split(".")[-1] in CANVAS:
|
|
100
|
+
how = CANVAS[dotted(receiver.func).split(".")[-1]]
|
|
101
|
+
for arg in flatten(receiver.args):
|
|
102
|
+
if isinstance(arg, ast.Call):
|
|
103
|
+
self.enqueue(arg, call, how)
|
|
104
|
+
return
|
|
105
|
+
self.record(receiver, call, extra)
|
|
106
|
+
|
|
107
|
+
def record(self, target: ast.AST, call: ast.Call, extra: str) -> None:
|
|
108
|
+
name = dotted(target)
|
|
109
|
+
if not name:
|
|
110
|
+
return
|
|
111
|
+
key = resolve(self.project, self.module, name)
|
|
112
|
+
self.out.append(Producer(self.module, call, key, "", name.split(".")[-1], keyword_str(call, "queue"), self.notes(call, extra)))
|
|
113
|
+
|
|
114
|
+
def notes(self, call: ast.Call, extra: str = "") -> List[str]:
|
|
115
|
+
out = list(self.pending)
|
|
116
|
+
if extra:
|
|
117
|
+
out.append(extra)
|
|
118
|
+
if keyword(call, "countdown") is not None or keyword(call, "eta") is not None:
|
|
119
|
+
out.append(LATER)
|
|
120
|
+
return out
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def flatten(args: List[ast.AST]) -> List[ast.AST]:
|
|
124
|
+
out: List[ast.AST] = []
|
|
125
|
+
for arg in args:
|
|
126
|
+
if isinstance(arg, ast.Starred):
|
|
127
|
+
arg = arg.value
|
|
128
|
+
if isinstance(arg, (ast.List, ast.Tuple)):
|
|
129
|
+
out += arg.elts
|
|
130
|
+
else:
|
|
131
|
+
out.append(arg)
|
|
132
|
+
return out
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def resolve(project: Project, module: Module, name: str) -> Optional[Tuple[str, str]]:
|
|
136
|
+
"""`send_invoice_email` through the import that brought it here,
|
|
137
|
+
`tasks.send_invoice_email` through the module import, to (module, function).
|
|
138
|
+
None when the head of the name is nothing this module imported or defined."""
|
|
139
|
+
parts = name.split(".")
|
|
140
|
+
if len(parts) == 1:
|
|
141
|
+
hit = project.resolve(module, name)
|
|
142
|
+
if hit is None:
|
|
143
|
+
return None
|
|
144
|
+
target, local = hit
|
|
145
|
+
return package_of(target), local
|
|
146
|
+
head, rest = parts[0], parts[1:]
|
|
147
|
+
imported = module.imports.get(head)
|
|
148
|
+
if imported is None:
|
|
149
|
+
return None
|
|
150
|
+
if imported.name == "*":
|
|
151
|
+
# `import a.b` binds `a` and is used as `a.b.fn`; `import a.b as z` is `z.fn`.
|
|
152
|
+
base = [head] if imported.module == head or imported.module.startswith(head + ".") else imported.module.split(".")
|
|
153
|
+
else:
|
|
154
|
+
base = imported.module.split(".") + [imported.name]
|
|
155
|
+
full = base + rest
|
|
156
|
+
return ".".join(full[:-1]), full[-1]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""The order a queue is decided in, and the configuration it is read from."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
8
|
+
sys.path.insert(1, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "pyplugin"))
|
|
9
|
+
|
|
10
|
+
import celery_conf as conf # noqa: E402
|
|
11
|
+
import celery_conf as routes # noqa: E402
|
|
12
|
+
from source import Project # noqa: E402
|
|
13
|
+
|
|
14
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def project(fixture):
|
|
18
|
+
root = os.path.join(HERE, "testdata", fixture)
|
|
19
|
+
return Project(root, root, lambda p: os.path.relpath(p, root))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Order(unittest.TestCase):
|
|
23
|
+
def setUp(self):
|
|
24
|
+
self.cfg = conf.Config(routes=[("app.tasks.send", "exact"), ("app.tasks.*", "glob")], default_queue="dflt")
|
|
25
|
+
|
|
26
|
+
def test_the_call_first_then_the_task_then_the_routes_then_the_default(self):
|
|
27
|
+
self.assertEqual(routes.queue_for("app.tasks.send", "here", "there", self.cfg), ("here", routes.AT_CALL))
|
|
28
|
+
self.assertEqual(routes.queue_for("app.tasks.send", "", "there", self.cfg), ("there", routes.ON_TASK))
|
|
29
|
+
self.assertEqual(routes.queue_for("app.tasks.send", "", "", self.cfg), ("exact", routes.BY_ROUTES))
|
|
30
|
+
self.assertEqual(routes.queue_for("app.tasks.other", "", "", self.cfg), ("glob", routes.BY_ROUTES))
|
|
31
|
+
self.assertEqual(routes.queue_for("elsewhere.task", "", "", self.cfg), ("dflt", routes.BY_DEFAULT))
|
|
32
|
+
|
|
33
|
+
def test_past_everything_celery_has_a_queue_of_its_own(self):
|
|
34
|
+
self.assertEqual(routes.queue_for("x", "", "", conf.Config()), ("celery", routes.BY_CELERY))
|
|
35
|
+
|
|
36
|
+
def test_a_route_that_names_no_task_is_found(self):
|
|
37
|
+
self.assertEqual(routes.unmatched(self.cfg, ["app.tasks.send"]), [])
|
|
38
|
+
self.assertEqual(routes.unmatched(self.cfg, ["other.task"]), ["app.tasks.send", "app.tasks.*"])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Schedules(unittest.TestCase):
|
|
42
|
+
"""A schedule object, as the sentence the page says."""
|
|
43
|
+
|
|
44
|
+
def text(self, source):
|
|
45
|
+
import ast
|
|
46
|
+
|
|
47
|
+
from source import Module
|
|
48
|
+
|
|
49
|
+
module = Module.__new__(Module)
|
|
50
|
+
module.tree = ast.parse("x = " + source)
|
|
51
|
+
return conf.schedule_text(module.tree.body[0].value, module)
|
|
52
|
+
|
|
53
|
+
def test_crontab_in_its_own_field_order_with_stars_for_what_is_not_said(self):
|
|
54
|
+
self.assertEqual(self.text("crontab(minute=0, hour='*/3')"), ("cron 0 */3 * * *", True))
|
|
55
|
+
self.assertEqual(self.text("crontab(hour=9, minute=30, day_of_week='mon-fri')"), ("cron 30 9 * * mon-fri", True))
|
|
56
|
+
self.assertEqual(self.text("crontab()"), ("cron * * * * *", True))
|
|
57
|
+
|
|
58
|
+
def test_an_interval_in_seconds_or_a_timedelta(self):
|
|
59
|
+
self.assertEqual(self.text("30.0"), ("every 30s", True))
|
|
60
|
+
self.assertEqual(self.text("90"), ("every 1m30s", True))
|
|
61
|
+
self.assertEqual(self.text("timedelta(hours=6)"), ("every 6h", True))
|
|
62
|
+
self.assertEqual(self.text("timedelta(days=1, hours=12)"), ("every 1d12h", True))
|
|
63
|
+
self.assertEqual(self.text("schedule(run_every=timedelta(minutes=5))"), ("every 5m", True))
|
|
64
|
+
|
|
65
|
+
def test_a_solar_event_and_anything_else_as_written(self):
|
|
66
|
+
self.assertEqual(self.text("solar('sunset', 59.9, 10.7)"), ("solar sunset", True))
|
|
67
|
+
self.assertEqual(self.text("crontab(minute=MINUTE)"), ("crontab(minute=MINUTE)", False))
|
|
68
|
+
self.assertEqual(self.text("every_other_day()"), ("every_other_day()", False))
|
|
69
|
+
|
|
70
|
+
def test_the_entries_are_read_off_the_settings_and_the_app_module(self):
|
|
71
|
+
cfg = conf.read_config(project("billing"), "")
|
|
72
|
+
self.assertEqual(
|
|
73
|
+
[(s.name, s.task or s.ref, s.when, s.queue, s.opaque) for s in cfg.schedules],
|
|
74
|
+
[
|
|
75
|
+
("remind-unpaid-invoices", "invoices.tasks.remind_unpaid_invoice", "every 6h", "", False),
|
|
76
|
+
("archive-closed-invoices", "billing.archive_invoice", "cron 30 2 * * *", "", False),
|
|
77
|
+
("close-stale-drafts", "close_stale_drafts", "cron 0 3 * * *", "", False),
|
|
78
|
+
],
|
|
79
|
+
)
|
|
80
|
+
self.assertFalse(cfg.beat_in_database)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Reading(unittest.TestCase):
|
|
84
|
+
def test_the_settings_under_the_namespace_the_app_names(self):
|
|
85
|
+
cfg = conf.read_config(project("billing"), "")
|
|
86
|
+
self.assertEqual(cfg.settings, "config.settings")
|
|
87
|
+
self.assertTrue(cfg.settings_found)
|
|
88
|
+
self.assertEqual(cfg.routes, [("invoices.tasks.send_invoice_email", "billing.mail")])
|
|
89
|
+
self.assertEqual(cfg.default_queue, "billing")
|
|
90
|
+
self.assertEqual(cfg.broker_scheme, "redis")
|
|
91
|
+
self.assertEqual([a.name for a in cfg.apps], ["app"])
|
|
92
|
+
|
|
93
|
+
def test_the_app_module_over_settings_that_are_not_there(self):
|
|
94
|
+
cfg = conf.read_config(project("drift"), "")
|
|
95
|
+
self.assertFalse(cfg.settings_found)
|
|
96
|
+
self.assertEqual(cfg.routes, [("reports.tasks.*", "reports")])
|
|
97
|
+
self.assertEqual(cfg.default_queue, "reports.default")
|
|
98
|
+
self.assertEqual(cfg.broker_scheme, "amqp")
|
|
99
|
+
|
|
100
|
+
def test_an_environment_lookup_is_its_default_and_no_further(self):
|
|
101
|
+
cfg = conf.read_config(project("billing"), "")
|
|
102
|
+
self.assertEqual(cfg.broker, "redis://localhost:6379/0")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
unittest.main()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
_ "embed"
|
|
5
|
+
|
|
6
|
+
"github.com/shortlink-org/portolan/plugin"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
//go:embed options.schema.json
|
|
10
|
+
var optionsSchema []byte
|
|
11
|
+
|
|
12
|
+
func descriptor() plugin.Descriptor {
|
|
13
|
+
return plugin.Descriptor{
|
|
14
|
+
Name: "extract-commands",
|
|
15
|
+
Summary: "Reads the make targets, npm scripts, just recipes and task-runner tasks a repository declares onto its service.",
|
|
16
|
+
Phases: []string{plugin.PhaseExtract},
|
|
17
|
+
Options: optionsSchema,
|
|
18
|
+
}
|
|
19
|
+
}
|