@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,1278 @@
|
|
|
1
|
+
"""What happens when somebody calls in, or when an event arrives.
|
|
2
|
+
|
|
3
|
+
Statements are read in source order. A call into the ORM is a hop to the store,
|
|
4
|
+
an event handed to anything is a hop to the bus, a call on a vendored client is
|
|
5
|
+
a hop to the peer. An `if` becomes an alt whose branch is terminal when it ends
|
|
6
|
+
in a return or a raise; a loop, a transaction and an except are a note on the
|
|
7
|
+
steps inside them. Every step is `declared`: this reads code, and code is a
|
|
8
|
+
claim about behaviour, not a record of it.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import ast
|
|
14
|
+
from dataclasses import dataclass, field as dc_field
|
|
15
|
+
from typing import Dict, List, Optional, Tuple
|
|
16
|
+
from urllib.parse import urlparse
|
|
17
|
+
|
|
18
|
+
import catalog
|
|
19
|
+
from clients import Client
|
|
20
|
+
from domain import Aggregate, ModelDef
|
|
21
|
+
from ids import pascal, slug
|
|
22
|
+
from operations import UseCase
|
|
23
|
+
import celery_conf
|
|
24
|
+
import celery_tasks
|
|
25
|
+
import kafka
|
|
26
|
+
from source import Module, Project, assigned, doc, dotted, keyword, keyword_str, methods
|
|
27
|
+
|
|
28
|
+
LANE_CLIENT = "client"
|
|
29
|
+
LANE_BUS = "bus"
|
|
30
|
+
|
|
31
|
+
# What an enqueue looks like: the call, and the signature it may go through.
|
|
32
|
+
# The queue it lands on is decided the way Celery decides it, by the same
|
|
33
|
+
# reader `extract-celery` uses, so the lane here is the one its flow draws.
|
|
34
|
+
ENQUEUE = {"delay", "apply_async"}
|
|
35
|
+
SIGNATURE = {"s", "si", "signature", "subtask"}
|
|
36
|
+
|
|
37
|
+
# A call on the ORM that goes to the database. `objects.<anything>` does too,
|
|
38
|
+
# and is caught by the manager rather than by this list.
|
|
39
|
+
STORE_METHODS = {"save", "delete", "refresh_from_db", "update", "full_clean_and_save"}
|
|
40
|
+
# What a publish looks like. A signal's `send` is one; so is anything handed an
|
|
41
|
+
# event, which is the rule that catches a project's own `publish()` helper.
|
|
42
|
+
SEND_METHODS = {"send", "send_robust", "publish"}
|
|
43
|
+
# A publish with an address: the topic, subject or channel goes first, and is
|
|
44
|
+
# read as far as syntax carries it - a literal, a module constant, a setting.
|
|
45
|
+
# `KafkaProducer.send`, confluent's `produce`, NATS and Redis `publish`,
|
|
46
|
+
# Channels' `group_send`.
|
|
47
|
+
PRODUCE_METHODS = {"send", "produce", "publish", "group_send"}
|
|
48
|
+
# Django's own model signals: a hook on the row, not an event. A receiver on
|
|
49
|
+
# one is reported, not drawn.
|
|
50
|
+
ORM_SIGNALS = {"pre_init", "post_init", "pre_save", "post_save", "pre_delete", "post_delete", "m2m_changed", "pre_migrate", "post_migrate"}
|
|
51
|
+
HTTP_LIBRARIES = {"requests", "httpx", "aiohttp"}
|
|
52
|
+
HTTP_METHODS = {"get", "post", "put", "patch", "delete", "head", "options"}
|
|
53
|
+
MAX_CALL_DEPTH = 5
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def sender_of(decorator: ast.Call) -> str:
|
|
57
|
+
"""`sender=Invoice`, or the models listed, or every model when none is named."""
|
|
58
|
+
value = keyword(decorator, "sender")
|
|
59
|
+
if value is None:
|
|
60
|
+
return "every model"
|
|
61
|
+
if isinstance(value, (ast.List, ast.Tuple)):
|
|
62
|
+
return ", ".join(dotted(v) for v in value.elts if dotted(v)) or "every model"
|
|
63
|
+
return dotted(value) or "every model"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class Options:
|
|
68
|
+
context: str
|
|
69
|
+
svc_id: str
|
|
70
|
+
service: str
|
|
71
|
+
store: str
|
|
72
|
+
stores: Dict[str, str] = dc_field(default_factory=dict)
|
|
73
|
+
peers: Dict[str, str] = dc_field(default_factory=dict)
|
|
74
|
+
events: Dict[str, str] = dc_field(default_factory=dict)
|
|
75
|
+
flow_wrappers: Dict[str, object] = dc_field(default_factory=dict)
|
|
76
|
+
settings: str = "" # the Django settings module Celery is configured from
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Draft:
|
|
80
|
+
"""One flow being built: the lanes in the order they are first used, and
|
|
81
|
+
the steps, which nest when a branch is open."""
|
|
82
|
+
|
|
83
|
+
def __init__(self) -> None:
|
|
84
|
+
self.lanes: List[Dict[str, object]] = []
|
|
85
|
+
self.steps: List[Dict[str, object]] = []
|
|
86
|
+
self.sinks: List[List[Dict[str, object]]] = []
|
|
87
|
+
self.n = 0
|
|
88
|
+
self.notes: List[str] = []
|
|
89
|
+
self.technical_wrappers: Dict[str, Tuple[Dict[str, object], int]] = {}
|
|
90
|
+
|
|
91
|
+
def lane(self, id_: str, kind: str, context: Optional[str], label: str = "") -> str:
|
|
92
|
+
if not any(l["id"] == id_ for l in self.lanes):
|
|
93
|
+
self.lanes.append(catalog.participant(id_, kind, context, label))
|
|
94
|
+
return id_
|
|
95
|
+
|
|
96
|
+
def sink(self) -> List[Dict[str, object]]:
|
|
97
|
+
return self.sinks[-1] if self.sinks else self.steps
|
|
98
|
+
|
|
99
|
+
def push(self) -> None:
|
|
100
|
+
self.sinks.append([])
|
|
101
|
+
|
|
102
|
+
def pop(self) -> List[Dict[str, object]]:
|
|
103
|
+
return self.sinks.pop() if self.sinks else []
|
|
104
|
+
|
|
105
|
+
def enter(self, note: str) -> None:
|
|
106
|
+
self.notes.append(note)
|
|
107
|
+
|
|
108
|
+
def leave(self) -> None:
|
|
109
|
+
self.notes.pop()
|
|
110
|
+
|
|
111
|
+
def note(self, own: str) -> str:
|
|
112
|
+
outer = []
|
|
113
|
+
for note in self.notes:
|
|
114
|
+
if note and note not in outer:
|
|
115
|
+
outer.append(note)
|
|
116
|
+
prefix = ", ".join(outer) + "." if outer else ""
|
|
117
|
+
return (prefix + " " + own).strip()
|
|
118
|
+
|
|
119
|
+
def add(
|
|
120
|
+
self,
|
|
121
|
+
from_: str,
|
|
122
|
+
to: str,
|
|
123
|
+
kind: str,
|
|
124
|
+
label: str,
|
|
125
|
+
status: str = catalog.DECLARED,
|
|
126
|
+
ref: str = "",
|
|
127
|
+
note: str = "",
|
|
128
|
+
line: str = "",
|
|
129
|
+
continues_at: str = "",
|
|
130
|
+
handoff: Optional[Dict[str, str]] = None,
|
|
131
|
+
) -> None:
|
|
132
|
+
self.n += 1
|
|
133
|
+
self.sink().append(
|
|
134
|
+
catalog.step(
|
|
135
|
+
"s%d" % self.n,
|
|
136
|
+
from_,
|
|
137
|
+
to,
|
|
138
|
+
kind,
|
|
139
|
+
label,
|
|
140
|
+
status,
|
|
141
|
+
ref,
|
|
142
|
+
self.note(note),
|
|
143
|
+
line,
|
|
144
|
+
continues_at,
|
|
145
|
+
handoff,
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def add_alt(self, branches: List[Dict[str, object]]) -> None:
|
|
150
|
+
self.n += 1
|
|
151
|
+
self.sink().append(catalog.alt("alt%d" % self.n, branches))
|
|
152
|
+
|
|
153
|
+
def add_wrapper(self, key: str, label: str, target: str, technical: bool, service: str, line: str) -> None:
|
|
154
|
+
"""Keep a configured project boundary semantic and compact repeated
|
|
155
|
+
infrastructure calls without discarding the fact that they happen."""
|
|
156
|
+
if technical and key in self.technical_wrappers:
|
|
157
|
+
step, count = self.technical_wrappers[key]
|
|
158
|
+
count += 1
|
|
159
|
+
self.technical_wrappers[key] = (step, count)
|
|
160
|
+
step["label"] = "%s ×%d" % (label, count)
|
|
161
|
+
step["note"] = "Project wrapper `%s`; %d call sites are collapsed in this flow." % (key, count)
|
|
162
|
+
return
|
|
163
|
+
to = target or service
|
|
164
|
+
kind = "rpc" if target and target != service else "call"
|
|
165
|
+
self.add(
|
|
166
|
+
service,
|
|
167
|
+
to,
|
|
168
|
+
kind,
|
|
169
|
+
label,
|
|
170
|
+
note="Project wrapper `%s`.%s" % (key, " Technical calls are collapsed." if technical else ""),
|
|
171
|
+
line=line,
|
|
172
|
+
)
|
|
173
|
+
if technical:
|
|
174
|
+
self.technical_wrappers[key] = (self.sink()[-1], 1)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def sentence(text: str) -> str:
|
|
178
|
+
words = text.replace("-", " ").replace("_", " ").strip()
|
|
179
|
+
return words[0].upper() + words[1:] if words else ""
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def condition(node: ast.AST) -> str:
|
|
183
|
+
try:
|
|
184
|
+
text = ast.unparse(node) # type: ignore[attr-defined]
|
|
185
|
+
except Exception:
|
|
186
|
+
return ""
|
|
187
|
+
text = " ".join(text.split())
|
|
188
|
+
return text if len(text) <= 72 else text[:69] + "..."
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@dataclass(frozen=True)
|
|
192
|
+
class ClassTarget:
|
|
193
|
+
module: Module
|
|
194
|
+
node: ast.ClassDef
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@dataclass(frozen=True)
|
|
198
|
+
class CallableTarget:
|
|
199
|
+
module: Module
|
|
200
|
+
node: ast.AST
|
|
201
|
+
owner: Optional[ClassTarget] = None
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def key(self) -> Tuple[str, str, str]:
|
|
205
|
+
return (self.module.dotted, self.owner.node.name if self.owner is not None else "", getattr(self.node, "name", ""))
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def qualified(self) -> str:
|
|
209
|
+
parts = [self.module.dotted]
|
|
210
|
+
if self.owner is not None:
|
|
211
|
+
parts.append(self.owner.node.name)
|
|
212
|
+
parts.append(getattr(self.node, "name", ""))
|
|
213
|
+
return ".".join(part for part in parts if part)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class Frame:
|
|
217
|
+
"""What a name means inside one body being walked."""
|
|
218
|
+
|
|
219
|
+
def __init__(
|
|
220
|
+
self,
|
|
221
|
+
module: Module,
|
|
222
|
+
variables: Optional[Dict[str, Tuple[str, object]]] = None,
|
|
223
|
+
owner: Optional[ClassTarget] = None,
|
|
224
|
+
active: Tuple[Tuple[str, str, str], ...] = (),
|
|
225
|
+
):
|
|
226
|
+
self.module = module
|
|
227
|
+
self.vars: Dict[str, Tuple[str, object]] = dict(variables or {})
|
|
228
|
+
self.returned: Optional[Tuple[str, object]] = None
|
|
229
|
+
self.owner = owner
|
|
230
|
+
self.active = active
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class FlowReader:
|
|
234
|
+
def __init__(self, opts: Options, project: Project, aggregates: List[Aggregate], models: List[ModelDef], use_cases: List[UseCase], clients: List[Client], events: Dict[str, object], rel, b):
|
|
235
|
+
self.opts = opts
|
|
236
|
+
self.project = project
|
|
237
|
+
self.aggregates = aggregates
|
|
238
|
+
self.rel = rel
|
|
239
|
+
self.b = b
|
|
240
|
+
self.calls: Dict[str, Dict[str, object]] = {}
|
|
241
|
+
self.referenced = set()
|
|
242
|
+
self.models: Dict[str, ModelDef] = {}
|
|
243
|
+
self.models_by_name: Dict[str, List[ModelDef]] = {}
|
|
244
|
+
self.emitters: Dict[Tuple[str, str], str] = {} # (model, method) -> event id
|
|
245
|
+
seen_models = set()
|
|
246
|
+
for model in models:
|
|
247
|
+
key = (model.module.dotted, model.name)
|
|
248
|
+
if key in seen_models:
|
|
249
|
+
continue
|
|
250
|
+
seen_models.add(key)
|
|
251
|
+
self.models_by_name.setdefault(model.name, []).append(model)
|
|
252
|
+
for agg in aggregates:
|
|
253
|
+
for model in agg.models:
|
|
254
|
+
key = (model.module.dotted, model.name)
|
|
255
|
+
if key in seen_models:
|
|
256
|
+
continue
|
|
257
|
+
seen_models.add(key)
|
|
258
|
+
self.models_by_name.setdefault(model.name, []).append(model)
|
|
259
|
+
for name, candidates in self.models_by_name.items():
|
|
260
|
+
self.models[name] = candidates[0]
|
|
261
|
+
self.use_cases: Dict[str, UseCase] = {}
|
|
262
|
+
for use_case in use_cases:
|
|
263
|
+
self.use_cases[use_case.key] = use_case
|
|
264
|
+
self.clients: Dict[str, Client] = {c.name: c for c in clients}
|
|
265
|
+
self.events = events # class or signal name -> EventDef
|
|
266
|
+
self._warned_store = False
|
|
267
|
+
self._warned_peers = set()
|
|
268
|
+
self._celery: Optional[Tuple[celery_conf.Config, Dict[Tuple[str, str], celery_tasks.Task]]] = None
|
|
269
|
+
self.produced: Dict[str, List[Tuple[str, str]]] = {} # event id -> (address, line) it was put on the wire at
|
|
270
|
+
self._settings: Optional[Module] = None
|
|
271
|
+
self._warned_kafka_topics = set()
|
|
272
|
+
self.classes: Dict[Tuple[str, str], ClassTarget] = {}
|
|
273
|
+
self.functions: Dict[Tuple[str, str], CallableTarget] = {}
|
|
274
|
+
for module in project.modules.values():
|
|
275
|
+
for node in module.classes():
|
|
276
|
+
target = ClassTarget(module, node)
|
|
277
|
+
self.classes[(module.dotted, node.name)] = target
|
|
278
|
+
for node in module.functions():
|
|
279
|
+
self.functions[(module.dotted, node.name)] = CallableTarget(module, node)
|
|
280
|
+
|
|
281
|
+
# --- lanes ---------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
def service_lane(self, d: Draft) -> str:
|
|
284
|
+
return d.lane(self.opts.svc_id, "service", self.opts.context)
|
|
285
|
+
|
|
286
|
+
def store_lane(self, d: Draft, alias: str = "default") -> str:
|
|
287
|
+
store = self.opts.stores.get(alias, self.opts.store if alias == "default" else "")
|
|
288
|
+
if not store:
|
|
289
|
+
if not self._warned_store:
|
|
290
|
+
self.b.warn(self.opts.svc_id, "no readable database configuration or `store` option, so calls into the ORM stay on the service's own lane")
|
|
291
|
+
self._warned_store = True
|
|
292
|
+
return self.opts.svc_id
|
|
293
|
+
label = "" if alias == "default" else alias
|
|
294
|
+
return d.lane("%s-%s" % (self.opts.service, store), "store", self.opts.context, label)
|
|
295
|
+
|
|
296
|
+
def peer_lane(self, d: Draft, pkg: str) -> Tuple[str, str, str]:
|
|
297
|
+
service = self.opts.peers.get(pkg, "")
|
|
298
|
+
if service:
|
|
299
|
+
context = service.split(".")[0]
|
|
300
|
+
return d.lane(service, "service", context), service, catalog.DECLARED
|
|
301
|
+
if pkg not in self._warned_peers:
|
|
302
|
+
self._warned_peers.add(pkg)
|
|
303
|
+
self.b.warn(
|
|
304
|
+
self.opts.svc_id,
|
|
305
|
+
"calls %s and the manifest names no peer for it; add it under `peers` to say which service answers, until then the calls are unresolved" % pkg,
|
|
306
|
+
)
|
|
307
|
+
return d.lane(pkg.replace(".", "-"), "unknown", None, pkg), pkg, catalog.UNRESOLVED
|
|
308
|
+
|
|
309
|
+
def consumes(self) -> List[Dict[str, object]]:
|
|
310
|
+
return [self.calls[key] for key in sorted(self.calls)]
|
|
311
|
+
|
|
312
|
+
# --- the two openings ----------------------------------------------------
|
|
313
|
+
|
|
314
|
+
def endpoint_flow(self, endpoint, serializer=None, model: Optional[ModelDef] = None) -> Optional[Dict[str, object]]:
|
|
315
|
+
d = Draft()
|
|
316
|
+
d.lane(LANE_CLIENT, "actor", None)
|
|
317
|
+
self.service_lane(d)
|
|
318
|
+
d.add(LANE_CLIENT, self.opts.svc_id, "rpc", endpoint.id, line=endpoint.module.where(endpoint.node))
|
|
319
|
+
ran: List[UseCase] = []
|
|
320
|
+
if isinstance(endpoint.node, ast.ClassDef):
|
|
321
|
+
self.inherited_endpoint(d, endpoint, serializer, model)
|
|
322
|
+
else:
|
|
323
|
+
owner = self.class_target(endpoint.module, endpoint.view) if endpoint.view else None
|
|
324
|
+
frame = Frame(endpoint.module, self.attributes(endpoint), owner=owner)
|
|
325
|
+
self.walk(d, frame, endpoint.node.body, 0, ran)
|
|
326
|
+
endpoint.use_cases = [u.key for u in ran]
|
|
327
|
+
name = slug(endpoint.id)
|
|
328
|
+
ident = "%s-%s" % (self.opts.service, name)
|
|
329
|
+
return catalog.flow(
|
|
330
|
+
"flow." + ident,
|
|
331
|
+
ident,
|
|
332
|
+
sentence(name),
|
|
333
|
+
ran[-1].doc if ran else endpoint.doc,
|
|
334
|
+
endpoint.module.rel,
|
|
335
|
+
self.opts.context,
|
|
336
|
+
d.lanes,
|
|
337
|
+
d.steps,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
def inherited_endpoint(self, d: Draft, endpoint, serializer, model: Optional[ModelDef]) -> None:
|
|
341
|
+
"""Materialise the behaviour DRF supplies for inherited CRUD actions.
|
|
342
|
+
|
|
343
|
+
The action itself is known from the concrete generic base. Persistence
|
|
344
|
+
is drawn only when ``queryset`` or serializer metadata proves a model;
|
|
345
|
+
the extractor never invents a table from the URL or view name.
|
|
346
|
+
"""
|
|
347
|
+
action = endpoint.action
|
|
348
|
+
serializer_name = getattr(serializer, "name", "")
|
|
349
|
+
framework = next(
|
|
350
|
+
(
|
|
351
|
+
dotted(base).split(".")[-1]
|
|
352
|
+
for base in endpoint.node.bases
|
|
353
|
+
if dotted(base).split(".")[-1].endswith(("APIView", "ViewSet"))
|
|
354
|
+
),
|
|
355
|
+
"generic view",
|
|
356
|
+
)
|
|
357
|
+
source = endpoint.module.where(endpoint.node)
|
|
358
|
+
model_note = "Model resolved from queryset or serializer metadata." if model is not None else "No model is declared by queryset or serializer metadata."
|
|
359
|
+
note = "Supplied by DRF %s. %s" % (framework, model_note)
|
|
360
|
+
|
|
361
|
+
if action in ("retrieve", "update", "partial_update", "destroy") and model is not None:
|
|
362
|
+
d.add(self.opts.svc_id, self.store_lane(d), "call", "%s.objects.get" % model.name, note=note, line=source)
|
|
363
|
+
elif action == "list" and model is not None:
|
|
364
|
+
d.add(self.opts.svc_id, self.store_lane(d), "call", "%s.objects.all" % model.name, note=note, line=source)
|
|
365
|
+
|
|
366
|
+
if action in ("create", "update", "partial_update") and serializer_name:
|
|
367
|
+
suffix = " (partial)" if action == "partial_update" else ""
|
|
368
|
+
d.add(self.opts.svc_id, self.opts.svc_id, "call", "Validate %s%s" % (serializer_name, suffix), note=note, line=source)
|
|
369
|
+
|
|
370
|
+
if model is not None:
|
|
371
|
+
if action == "create":
|
|
372
|
+
d.add(self.opts.svc_id, self.store_lane(d), "call", "%s.objects.create" % model.name, note=note, line=source)
|
|
373
|
+
elif action in ("update", "partial_update"):
|
|
374
|
+
d.add(self.opts.svc_id, self.store_lane(d), "call", "%s.save" % model.name, note=note, line=source)
|
|
375
|
+
elif action == "destroy":
|
|
376
|
+
d.add(self.opts.svc_id, self.store_lane(d), "call", "%s.delete" % model.name, note=note, line=source)
|
|
377
|
+
|
|
378
|
+
if action in ("list", "retrieve") and serializer_name:
|
|
379
|
+
suffix = " collection" if action == "list" else ""
|
|
380
|
+
d.add(self.opts.svc_id, self.opts.svc_id, "call", "Serialize %s%s" % (serializer_name, suffix), note=note, line=source)
|
|
381
|
+
|
|
382
|
+
def policy_flow(self, agg: Aggregate, module: Module, node: ast.AST, decorator: ast.Call) -> Optional[Dict[str, object]]:
|
|
383
|
+
signal = dotted(decorator.args[0]).split(".")[-1] if decorator.args else ""
|
|
384
|
+
if signal in ORM_SIGNALS:
|
|
385
|
+
# Not a policy on an event: a hook on the row. It says nothing
|
|
386
|
+
# about what happened, and it fires for every save - migrations,
|
|
387
|
+
# fixtures and the admin included - so it is reported rather than
|
|
388
|
+
# drawn as a flow nothing in the domain triggers.
|
|
389
|
+
self.b.warn(
|
|
390
|
+
module.where(node),
|
|
391
|
+
"%s runs on %s of %s: a policy hanging on a persistence hook rather than a domain event - nothing says what happened, and it fires for any save, migrations and fixtures included"
|
|
392
|
+
% (node.name, signal, sender_of(decorator)),
|
|
393
|
+
)
|
|
394
|
+
return None
|
|
395
|
+
trigger = self.trigger(module, decorator)
|
|
396
|
+
if trigger is None:
|
|
397
|
+
self.b.warn(
|
|
398
|
+
self.opts.svc_id,
|
|
399
|
+
"%s: %s receives a signal this repository does not declare and the manifest's `events` does not place; the flow is left out" % (module.rel, node.name),
|
|
400
|
+
)
|
|
401
|
+
return None
|
|
402
|
+
ref, label, status = trigger
|
|
403
|
+
d = Draft()
|
|
404
|
+
d.lane(LANE_BUS, "broker", None)
|
|
405
|
+
self.service_lane(d)
|
|
406
|
+
d.add(LANE_BUS, self.opts.svc_id, "event", label, status, ref if status == catalog.DECLARED else "", line=module.where(node))
|
|
407
|
+
if status == catalog.DECLARED:
|
|
408
|
+
self.referenced.add(ref)
|
|
409
|
+
self.walk(d, Frame(module, self.module_env(module)), node.body, 0, [])
|
|
410
|
+
ident = "%s-%s" % (self.opts.service, slug(node.name))
|
|
411
|
+
return catalog.flow(
|
|
412
|
+
"flow." + ident,
|
|
413
|
+
ident,
|
|
414
|
+
sentence(slug(node.name)),
|
|
415
|
+
doc(node),
|
|
416
|
+
module.rel,
|
|
417
|
+
self.opts.context,
|
|
418
|
+
d.lanes,
|
|
419
|
+
d.steps,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
def task_flows(self) -> List[Dict[str, object]]:
|
|
423
|
+
"""Source-backed continuations for Celery worker bodies.
|
|
424
|
+
|
|
425
|
+
The Celery extractor owns transport truth. These fragments start at a
|
|
426
|
+
shared source seam, so enrichment appends observable task work after
|
|
427
|
+
the worker receive without replaying the enqueue.
|
|
428
|
+
"""
|
|
429
|
+
out = []
|
|
430
|
+
for task in celery_tasks.read_tasks(self.project):
|
|
431
|
+
d = Draft()
|
|
432
|
+
self.service_lane(d)
|
|
433
|
+
self.walk(d, Frame(task.module, self.module_env(task.module)), task.node.body, 0, [])
|
|
434
|
+
if not d.steps:
|
|
435
|
+
continue
|
|
436
|
+
ident = "%s-celery-body-%s" % (self.opts.service, slug(task.name))
|
|
437
|
+
out.append(
|
|
438
|
+
catalog.flow(
|
|
439
|
+
"flow." + ident,
|
|
440
|
+
ident,
|
|
441
|
+
sentence(task.short) + " work",
|
|
442
|
+
(task.doc + " Observable work performed by the Celery task.").strip(),
|
|
443
|
+
task.module.rel,
|
|
444
|
+
self.opts.context,
|
|
445
|
+
d.lanes,
|
|
446
|
+
d.steps,
|
|
447
|
+
entrypoint=task.entrypoint,
|
|
448
|
+
)
|
|
449
|
+
)
|
|
450
|
+
return out
|
|
451
|
+
|
|
452
|
+
def trigger(self, module: Module, decorator: ast.Call) -> Optional[Tuple[str, str, str]]:
|
|
453
|
+
"""What a receiver reacts to: the signal it is given, resolved to an
|
|
454
|
+
event of this service, or placed by the manifest's `events`."""
|
|
455
|
+
if not decorator.args:
|
|
456
|
+
return None
|
|
457
|
+
name = dotted(decorator.args[0]).split(".")[-1]
|
|
458
|
+
local = self.events.get(name)
|
|
459
|
+
if local is not None:
|
|
460
|
+
return local.id, local.name, catalog.DECLARED
|
|
461
|
+
imported = module.imports.get(name)
|
|
462
|
+
if imported is not None:
|
|
463
|
+
for prefix, aggregate in sorted(self.opts.events.items()):
|
|
464
|
+
if imported.module == prefix or imported.module.startswith(prefix + "."):
|
|
465
|
+
return "%s.%s" % (aggregate, pascal(name)), pascal(name), catalog.DECLARED
|
|
466
|
+
return None
|
|
467
|
+
|
|
468
|
+
def attributes(self, endpoint) -> Dict[str, Tuple[str, object]]:
|
|
469
|
+
"""`self.pricing = PricingClient()` in the view, and the same at module
|
|
470
|
+
level: what a handler can reach without being handed it."""
|
|
471
|
+
out: Dict[str, Tuple[str, object]] = self.module_env(endpoint.module)
|
|
472
|
+
for node in endpoint.module.classes():
|
|
473
|
+
if node.name != endpoint.view:
|
|
474
|
+
continue
|
|
475
|
+
for name, value, _ in assigned(node):
|
|
476
|
+
binding = self.constructed(endpoint.module, value)
|
|
477
|
+
if binding is not None:
|
|
478
|
+
out["self." + name] = binding
|
|
479
|
+
init = None
|
|
480
|
+
for m in methods(node):
|
|
481
|
+
if m.name == "__init__":
|
|
482
|
+
init = m
|
|
483
|
+
if init is not None:
|
|
484
|
+
for stmt in ast.walk(init):
|
|
485
|
+
if isinstance(stmt, ast.Assign) and len(stmt.targets) == 1 and isinstance(stmt.targets[0], ast.Attribute):
|
|
486
|
+
binding = self.constructed(endpoint.module, stmt.value)
|
|
487
|
+
if binding is not None:
|
|
488
|
+
out["self." + stmt.targets[0].attr] = binding
|
|
489
|
+
return out
|
|
490
|
+
|
|
491
|
+
def constructed(self, module: Module, value: ast.AST, variables=None) -> Optional[Tuple[str, object]]:
|
|
492
|
+
if not isinstance(value, ast.Call):
|
|
493
|
+
return None
|
|
494
|
+
name = dotted(value.func).split(".")[-1]
|
|
495
|
+
if name in self.clients:
|
|
496
|
+
return ("client", self.clients[name])
|
|
497
|
+
external = self.external_name(module, dotted(value.func))
|
|
498
|
+
if external in ("requests.Session", "httpx.Client", "httpx.AsyncClient", "aiohttp.ClientSession"):
|
|
499
|
+
return ("http", external.split(".", 1)[0])
|
|
500
|
+
kafka_client = kafka.constructed(self.project, module, value, self.opts.settings, variables)
|
|
501
|
+
if kafka_client is not None:
|
|
502
|
+
return ("kafka", kafka_client)
|
|
503
|
+
target = self.resolve_class(module, dotted(value.func))
|
|
504
|
+
if target is not None:
|
|
505
|
+
return ("object", target)
|
|
506
|
+
return None
|
|
507
|
+
|
|
508
|
+
# --- the walk ------------------------------------------------------------
|
|
509
|
+
|
|
510
|
+
def walk(self, d: Draft, frame: Frame, body: List[ast.AST], depth: int, ran: List[UseCase]) -> None:
|
|
511
|
+
for stmt in body:
|
|
512
|
+
self.statement(d, frame, stmt, depth, ran)
|
|
513
|
+
|
|
514
|
+
def statement(self, d: Draft, frame: Frame, stmt: ast.AST, depth: int, ran: List[UseCase]) -> None:
|
|
515
|
+
if isinstance(stmt, ast.Assign):
|
|
516
|
+
binding = self.value(d, frame, stmt.value, depth, ran)
|
|
517
|
+
for target in stmt.targets:
|
|
518
|
+
self.bind(frame, target, binding)
|
|
519
|
+
elif isinstance(stmt, ast.AnnAssign) and stmt.value is not None:
|
|
520
|
+
self.bind(frame, stmt.target, self.value(d, frame, stmt.value, depth, ran))
|
|
521
|
+
elif isinstance(stmt, ast.Expr):
|
|
522
|
+
self.value(d, frame, stmt.value, depth, ran)
|
|
523
|
+
elif isinstance(stmt, ast.Return):
|
|
524
|
+
if stmt.value is not None:
|
|
525
|
+
frame.returned = self.value(d, frame, stmt.value, depth, ran)
|
|
526
|
+
elif isinstance(stmt, ast.If):
|
|
527
|
+
self.choice(d, frame, stmt, depth, ran)
|
|
528
|
+
elif isinstance(stmt, (ast.For, ast.AsyncFor)):
|
|
529
|
+
# Creating or advancing an iterable may itself perform the
|
|
530
|
+
# observable work: project wrappers commonly return generators
|
|
531
|
+
# around streaming HTTP responses.
|
|
532
|
+
self.value(d, frame, stmt.iter, depth, ran)
|
|
533
|
+
d.enter("for each %s" % condition(stmt.target))
|
|
534
|
+
self.walk(d, frame, stmt.body, depth, ran)
|
|
535
|
+
d.leave()
|
|
536
|
+
elif isinstance(stmt, ast.While):
|
|
537
|
+
d.enter("while %s" % condition(stmt.test))
|
|
538
|
+
self.walk(d, frame, stmt.body, depth, ran)
|
|
539
|
+
d.leave()
|
|
540
|
+
elif isinstance(stmt, (ast.With, ast.AsyncWith)):
|
|
541
|
+
atomic = any(dotted(item.context_expr).split(".")[-1] == "atomic" for item in stmt.items)
|
|
542
|
+
d.enter("in one transaction" if atomic else "")
|
|
543
|
+
self.walk(d, frame, stmt.body, depth, ran)
|
|
544
|
+
d.leave()
|
|
545
|
+
elif isinstance(stmt, ast.Try):
|
|
546
|
+
self.walk(d, frame, stmt.body, depth, ran)
|
|
547
|
+
for handler in stmt.handlers:
|
|
548
|
+
d.enter("on %s" % (condition(handler.type) if handler.type is not None else "failure"))
|
|
549
|
+
self.walk(d, frame, handler.body, depth, ran)
|
|
550
|
+
d.leave()
|
|
551
|
+
self.walk(d, frame, stmt.orelse, depth, ran)
|
|
552
|
+
self.walk(d, frame, stmt.finalbody, depth, ran)
|
|
553
|
+
elif isinstance(stmt, ast.Raise):
|
|
554
|
+
return
|
|
555
|
+
elif isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
556
|
+
frame.vars[stmt.name] = ("callable", CallableTarget(frame.module, stmt, frame.owner))
|
|
557
|
+
|
|
558
|
+
def choice(self, d: Draft, frame: Frame, stmt: ast.If, depth: int, ran: List[UseCase]) -> None:
|
|
559
|
+
branches = []
|
|
560
|
+
current: Optional[ast.AST] = stmt
|
|
561
|
+
while isinstance(current, ast.If):
|
|
562
|
+
d.push()
|
|
563
|
+
self.walk(d, frame, current.body, depth, ran)
|
|
564
|
+
steps = d.pop()
|
|
565
|
+
branches.append(catalog.branch(condition(current.test), steps, terminal(current.body)))
|
|
566
|
+
rest = current.orelse
|
|
567
|
+
if len(rest) == 1 and isinstance(rest[0], ast.If):
|
|
568
|
+
current = rest[0]
|
|
569
|
+
continue
|
|
570
|
+
if rest:
|
|
571
|
+
d.push()
|
|
572
|
+
self.walk(d, frame, rest, depth, ran)
|
|
573
|
+
branches.append(catalog.branch("otherwise", d.pop(), terminal(rest)))
|
|
574
|
+
current = None
|
|
575
|
+
if any(branch["steps"] for branch in branches):
|
|
576
|
+
if len(branches) == 1:
|
|
577
|
+
branches.append(catalog.branch("otherwise", []))
|
|
578
|
+
d.add_alt(branches)
|
|
579
|
+
|
|
580
|
+
def bind(self, frame: Frame, target: ast.AST, binding) -> None:
|
|
581
|
+
if binding is None:
|
|
582
|
+
return
|
|
583
|
+
if isinstance(target, ast.Name):
|
|
584
|
+
frame.vars[target.id] = binding
|
|
585
|
+
elif isinstance(target, ast.Attribute) and dotted(target):
|
|
586
|
+
frame.vars[dotted(target)] = binding
|
|
587
|
+
elif isinstance(target, (ast.Tuple, ast.List)) and isinstance(binding, tuple) and binding[0] == "many":
|
|
588
|
+
for element, one in zip(target.elts, binding[1]):
|
|
589
|
+
self.bind(frame, element, one)
|
|
590
|
+
|
|
591
|
+
def lookup(self, frame: Frame, node: ast.AST):
|
|
592
|
+
name = dotted(node)
|
|
593
|
+
if not name:
|
|
594
|
+
return None
|
|
595
|
+
if name in frame.vars:
|
|
596
|
+
return frame.vars[name]
|
|
597
|
+
base = name.split(".")[0]
|
|
598
|
+
found = frame.vars.get(base)
|
|
599
|
+
if found is not None:
|
|
600
|
+
return found
|
|
601
|
+
model = self.model_for(frame.module, name)
|
|
602
|
+
return ("model", model) if model is not None else None
|
|
603
|
+
|
|
604
|
+
def value(self, d: Draft, frame: Frame, node: ast.AST, depth: int, ran: List[UseCase]):
|
|
605
|
+
"""What an expression holds, and every hop it makes on the way."""
|
|
606
|
+
if isinstance(node, ast.Await):
|
|
607
|
+
return self.value(d, frame, node.value, depth, ran)
|
|
608
|
+
if isinstance(node, (ast.Tuple, ast.List)):
|
|
609
|
+
return ("many", [self.value(d, frame, element, depth, ran) for element in node.elts])
|
|
610
|
+
if isinstance(node, (ast.Name, ast.Attribute)):
|
|
611
|
+
binding = self.lookup(frame, node)
|
|
612
|
+
if binding is not None:
|
|
613
|
+
return binding
|
|
614
|
+
text = self.string_value(frame.module, node)
|
|
615
|
+
return ("string", text) if text else None
|
|
616
|
+
if not isinstance(node, ast.Call):
|
|
617
|
+
text = self.string_value(frame.module, node)
|
|
618
|
+
if text:
|
|
619
|
+
return ("string", text)
|
|
620
|
+
resolved = kafka.value(self.project, frame.module, node, self.opts.settings, frame.vars)
|
|
621
|
+
return ("value", resolved) if resolved is not None else None
|
|
622
|
+
|
|
623
|
+
# The receiver first, then the arguments: a chain is read the way it is
|
|
624
|
+
# written, so `Invoice.objects.filter(...).first()` makes its query
|
|
625
|
+
# where the queryset is built rather than nowhere at all.
|
|
626
|
+
holder = None
|
|
627
|
+
if isinstance(node.func, ast.Attribute):
|
|
628
|
+
inner = node.func.value
|
|
629
|
+
holder = self.value(d, frame, inner, depth, ran) if isinstance(inner, ast.Call) else self.lookup(frame, inner)
|
|
630
|
+
name = dotted(node.func)
|
|
631
|
+
last = name.split(".")[-1]
|
|
632
|
+
line = frame.module.where(node)
|
|
633
|
+
|
|
634
|
+
# A Kafka call is recognized from the constructed client type, not the
|
|
635
|
+
# method name alone. Handle it before walking serialization arguments:
|
|
636
|
+
# those expressions describe the payload and are not extra publishes.
|
|
637
|
+
if holder is not None and holder[0] == "kafka":
|
|
638
|
+
client: kafka.Client = holder[1]
|
|
639
|
+
if client.role == "producer" and last in kafka.PUBLISH_METHODS.get(client.library, set()):
|
|
640
|
+
topic_node = kafka.topic_node(node)
|
|
641
|
+
address = kafka.value(self.project, frame.module, topic_node, self.opts.settings, frame.vars)
|
|
642
|
+
if isinstance(address, str) and address:
|
|
643
|
+
payload = kafka.payload_node(node)
|
|
644
|
+
event_id = ""
|
|
645
|
+
if payload is not None:
|
|
646
|
+
for child in ast.walk(payload):
|
|
647
|
+
if not isinstance(child, (ast.Name, ast.Attribute)):
|
|
648
|
+
continue
|
|
649
|
+
binding = self.lookup(frame, child)
|
|
650
|
+
if binding is not None and binding[0] == "event":
|
|
651
|
+
event_id = binding[1]
|
|
652
|
+
break
|
|
653
|
+
message = event_id.rsplit(".", 1)[-1] if event_id else kafka.payload_name(payload, frame.vars)
|
|
654
|
+
lane = d.lane("kafka-" + slug(address), "broker", None, "Kafka · " + address)
|
|
655
|
+
detail = [kafka.config_note(client)]
|
|
656
|
+
if payload is not None:
|
|
657
|
+
detail.append("payload `%s`" % kafka.expression(payload))
|
|
658
|
+
key = keyword(node, "key")
|
|
659
|
+
headers = keyword(node, "headers")
|
|
660
|
+
if key is not None:
|
|
661
|
+
detail.append("key `%s`" % kafka.expression(key))
|
|
662
|
+
if headers is not None:
|
|
663
|
+
detail.append("headers `%s`" % kafka.expression(headers))
|
|
664
|
+
d.add(
|
|
665
|
+
self.opts.svc_id,
|
|
666
|
+
lane,
|
|
667
|
+
"event" if event_id else "call",
|
|
668
|
+
"publish " + message,
|
|
669
|
+
ref=event_id,
|
|
670
|
+
note="; ".join(detail),
|
|
671
|
+
line=line,
|
|
672
|
+
handoff={
|
|
673
|
+
"kind": "message",
|
|
674
|
+
"transport": "kafka",
|
|
675
|
+
"channel": address,
|
|
676
|
+
"message": message,
|
|
677
|
+
"direction": "send",
|
|
678
|
+
},
|
|
679
|
+
)
|
|
680
|
+
if event_id:
|
|
681
|
+
self.referenced.add(event_id)
|
|
682
|
+
self.produced.setdefault(event_id, []).append((address, line))
|
|
683
|
+
elif line not in self._warned_kafka_topics:
|
|
684
|
+
self._warned_kafka_topics.add(line)
|
|
685
|
+
self.b.warn(line, "Kafka topic is dynamic%s, so no flow step is invented" % ((": `%s`" % kafka.expression(topic_node)) if topic_node is not None else ""))
|
|
686
|
+
return None
|
|
687
|
+
|
|
688
|
+
positional = [self.value(d, frame, arg, depth, ran) for arg in node.args]
|
|
689
|
+
named = {kw.arg: self.value(d, frame, kw.value, depth, ran) for kw in node.keywords if kw.arg}
|
|
690
|
+
args = positional + list(named.values())
|
|
691
|
+
|
|
692
|
+
if self.external_name(frame.module, name) == "urllib.parse.urljoin" and len(node.args) >= 2:
|
|
693
|
+
left = binding_string(positional[0]) or self.string_value(frame.module, node.args[0])
|
|
694
|
+
right = binding_string(positional[1]) or self.string_value(frame.module, node.args[1])
|
|
695
|
+
text = left.rstrip("/") + "/" + right.lstrip("/") if left or right else ""
|
|
696
|
+
if text:
|
|
697
|
+
return ("string", text)
|
|
698
|
+
|
|
699
|
+
# A use case of this service: its steps are these steps. Looked for
|
|
700
|
+
# first, so a use case handed an event is followed rather than read as
|
|
701
|
+
# a publish of it.
|
|
702
|
+
use_case = self.use_case(frame.module, name)
|
|
703
|
+
if use_case is not None:
|
|
704
|
+
return self.inline(d, use_case, positional, named, depth, ran, line)
|
|
705
|
+
|
|
706
|
+
# A publish with an address, or a project's own helper that makes one
|
|
707
|
+
# when handed an event: the event leaves for the bus, and the step
|
|
708
|
+
# says where. Read before the plain event rule so the address is
|
|
709
|
+
# not lost to it.
|
|
710
|
+
event = next((a for a in args if isinstance(a, tuple) and a[0] == "event"), None)
|
|
711
|
+
if holder is None or holder[0] not in ("client", "model", "model-store"):
|
|
712
|
+
address = ""
|
|
713
|
+
if last in PRODUCE_METHODS and node.args:
|
|
714
|
+
address = self.address_of(frame.module, node.args[0])
|
|
715
|
+
if not address and event is not None:
|
|
716
|
+
address = self.helper_address(frame.module, name)
|
|
717
|
+
if address:
|
|
718
|
+
self.produce(d, address, event[1] if event is not None else "", line)
|
|
719
|
+
return None
|
|
720
|
+
|
|
721
|
+
# Work handed to Celery is a hop off the request. `on_commit` around
|
|
722
|
+
# it is the one fact about when the message leaves that the code
|
|
723
|
+
# states plainly, so the lambda or partial it holds is read inside a
|
|
724
|
+
# note rather than skipped.
|
|
725
|
+
if last == "on_commit":
|
|
726
|
+
d.enter("after the transaction commits")
|
|
727
|
+
for arg in node.args:
|
|
728
|
+
self.deferred(d, frame, arg, depth, ran)
|
|
729
|
+
d.leave()
|
|
730
|
+
return None
|
|
731
|
+
if last in ENQUEUE and isinstance(node.func, ast.Attribute):
|
|
732
|
+
task = self.task_of(frame.module, node.func.value)
|
|
733
|
+
if task is not None:
|
|
734
|
+
self.enqueue(d, task, keyword_str(node, "queue"), line)
|
|
735
|
+
return None
|
|
736
|
+
|
|
737
|
+
# A project helper is evidence stronger than its call shape. Follow it
|
|
738
|
+
# before the generic "an event was handed to something" fallback, so a
|
|
739
|
+
# formatter or validator receiving an event is not invented as a
|
|
740
|
+
# publish operation.
|
|
741
|
+
called = self.callable_target(frame, node.func, holder)
|
|
742
|
+
if called is not None and (holder is None or holder[0] == "object"):
|
|
743
|
+
wrapper = self.wrapper(called)
|
|
744
|
+
if wrapper is not None:
|
|
745
|
+
self.wrapper_step(d, called, wrapper, line)
|
|
746
|
+
return None
|
|
747
|
+
return self.inline_callable(d, frame, called, positional, named, depth, ran)
|
|
748
|
+
|
|
749
|
+
# An event handed to anything is the event leaving for the bus, which
|
|
750
|
+
# is the rule that catches a project's own `publish()` helper as well
|
|
751
|
+
# as a signal's `send`. A list it is being collected into is not one.
|
|
752
|
+
event = next((a for a in args if isinstance(a, tuple) and a[0] == "event"), None)
|
|
753
|
+
if event is not None and last not in ("append", "extend"):
|
|
754
|
+
self.publish(d, event[1], line)
|
|
755
|
+
return None
|
|
756
|
+
if last in SEND_METHODS and "." in name:
|
|
757
|
+
found = self.events.get(name.rsplit(".", 1)[0].split(".")[-1])
|
|
758
|
+
if found is not None:
|
|
759
|
+
self.publish(d, found.id, line)
|
|
760
|
+
return None
|
|
761
|
+
|
|
762
|
+
# An event, a model or a client, constructed.
|
|
763
|
+
if last in self.events and isinstance(node.func, (ast.Name, ast.Attribute)):
|
|
764
|
+
return ("event", self.events[last].id)
|
|
765
|
+
model = self.model_for(frame.module, name)
|
|
766
|
+
if model is not None and ".objects." not in name:
|
|
767
|
+
return ("model", model)
|
|
768
|
+
if last in self.clients:
|
|
769
|
+
return ("client", self.clients[last])
|
|
770
|
+
constructed = self.constructed(frame.module, node, frame.vars)
|
|
771
|
+
if constructed is not None:
|
|
772
|
+
return constructed
|
|
773
|
+
|
|
774
|
+
# The ORM: a manager on a model class, or a write on an instance.
|
|
775
|
+
parts = name.split(".")
|
|
776
|
+
manager_model = self.model_for(frame.module, ".".join(parts[:-2])) if len(parts) >= 3 and parts[-2] == "objects" else None
|
|
777
|
+
if manager_model is not None:
|
|
778
|
+
if last in ("using", "db_manager"):
|
|
779
|
+
alias = binding_string(positional[0]) if positional else ""
|
|
780
|
+
return ("model-store", (manager_model, alias or "default"))
|
|
781
|
+
d.add(self.opts.svc_id, self.store_lane(d), "call", "%s.objects.%s" % (parts[-3], last), line=line)
|
|
782
|
+
return ("model", manager_model)
|
|
783
|
+
if holder is not None and holder[0] == "model-store":
|
|
784
|
+
model, alias = holder[1]
|
|
785
|
+
d.add(self.opts.svc_id, self.store_lane(d, alias), "call", "%s.objects.%s" % (model.name, last), line=line)
|
|
786
|
+
return ("model", model)
|
|
787
|
+
if holder is not None and holder[0] == "model":
|
|
788
|
+
model: ModelDef = holder[1]
|
|
789
|
+
if last in STORE_METHODS or (len(parts) >= 2 and parts[-2] == "objects"):
|
|
790
|
+
alias = binding_string(named.get("using")) or "default"
|
|
791
|
+
d.add(self.opts.svc_id, self.store_lane(d, alias), "call", "%s.%s" % (model.name, last), line=line)
|
|
792
|
+
return holder
|
|
793
|
+
emitted = self.emits(model, last)
|
|
794
|
+
if emitted:
|
|
795
|
+
return ("event", emitted)
|
|
796
|
+
if holder is not None and holder[0] == "client":
|
|
797
|
+
return self.rpc(d, holder[1], last, line)
|
|
798
|
+
|
|
799
|
+
http = self.http_call(frame.module, node, holder, positional)
|
|
800
|
+
if http is not None:
|
|
801
|
+
method, target = http
|
|
802
|
+
self.http_step(d, method, target, line)
|
|
803
|
+
return None
|
|
804
|
+
|
|
805
|
+
called = self.callable_target(frame, node.func, holder)
|
|
806
|
+
if called is not None:
|
|
807
|
+
wrapper = self.wrapper(called)
|
|
808
|
+
if wrapper is not None:
|
|
809
|
+
self.wrapper_step(d, called, wrapper, line)
|
|
810
|
+
return None
|
|
811
|
+
return self.inline_callable(d, frame, called, positional, named, depth, ran)
|
|
812
|
+
|
|
813
|
+
if holder is not None and holder[0] == "model":
|
|
814
|
+
return holder
|
|
815
|
+
|
|
816
|
+
return None
|
|
817
|
+
|
|
818
|
+
def inline(self, d: Draft, use_case: UseCase, positional, named, depth: int, ran: List[UseCase], line: str):
|
|
819
|
+
"""A use case runs here, so its steps are drawn here - two deep at
|
|
820
|
+
most, past which the call itself is the step."""
|
|
821
|
+
if use_case not in ran:
|
|
822
|
+
ran.append(use_case)
|
|
823
|
+
if depth >= 2:
|
|
824
|
+
d.add(self.opts.svc_id, self.opts.svc_id, "call", use_case.id, note="Runs the use case, whose steps are not drawn again here.", line=line)
|
|
825
|
+
return None
|
|
826
|
+
inner = Frame(use_case.module, self.module_env(use_case.module))
|
|
827
|
+
self.bind_arguments(inner, use_case.node, positional, named, False)
|
|
828
|
+
self.walk(d, inner, use_case.node.body, depth + 1, ran)
|
|
829
|
+
return inner.returned
|
|
830
|
+
|
|
831
|
+
def inline_callable(self, d: Draft, caller: Frame, target: CallableTarget, positional, named, depth: int, ran: List[UseCase]):
|
|
832
|
+
"""Follow a project function or method until its observable effects."""
|
|
833
|
+
if depth >= MAX_CALL_DEPTH or target.key in caller.active:
|
|
834
|
+
return None
|
|
835
|
+
variables = self.module_env(target.module)
|
|
836
|
+
if target.module is caller.module:
|
|
837
|
+
variables.update(caller.vars)
|
|
838
|
+
if target.owner is not None:
|
|
839
|
+
variables.update(self.class_env(target.owner))
|
|
840
|
+
variables.update({name: value for name, value in caller.vars.items() if name.startswith("self.")})
|
|
841
|
+
inner = Frame(target.module, variables, owner=target.owner, active=caller.active + (target.key,))
|
|
842
|
+
self.bind_arguments(inner, target.node, positional, named, target.owner is not None)
|
|
843
|
+
self.walk(d, inner, getattr(target.node, "body", []), depth + 1, ran)
|
|
844
|
+
return inner.returned
|
|
845
|
+
|
|
846
|
+
def wrapper(self, target: CallableTarget) -> Optional[Dict[str, object]]:
|
|
847
|
+
configured = self.opts.flow_wrappers.get(target.qualified)
|
|
848
|
+
return configured if isinstance(configured, dict) else None
|
|
849
|
+
|
|
850
|
+
def wrapper_step(self, d: Draft, target: CallableTarget, wrapper: Dict[str, object], line: str) -> None:
|
|
851
|
+
configured_label = wrapper.get("label")
|
|
852
|
+
label = configured_label if isinstance(configured_label, str) and configured_label else sentence(getattr(target.node, "name", ""))
|
|
853
|
+
configured_target = wrapper.get("target")
|
|
854
|
+
target_id = configured_target if isinstance(configured_target, str) else ""
|
|
855
|
+
if target_id:
|
|
856
|
+
context = target_id.split(".", 1)[0] if "." in target_id else None
|
|
857
|
+
d.lane(target_id, "service", context)
|
|
858
|
+
d.add_wrapper(
|
|
859
|
+
target.qualified,
|
|
860
|
+
label,
|
|
861
|
+
target_id,
|
|
862
|
+
wrapper.get("technical") is True,
|
|
863
|
+
self.opts.svc_id,
|
|
864
|
+
line,
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
def bind_arguments(self, frame: Frame, node: ast.AST, positional, named, method: bool) -> None:
|
|
868
|
+
args = getattr(node, "args", None)
|
|
869
|
+
if args is None:
|
|
870
|
+
return
|
|
871
|
+
parameters = list(getattr(args, "posonlyargs", [])) + list(getattr(args, "args", []))
|
|
872
|
+
if method and parameters and parameters[0].arg in ("self", "cls"):
|
|
873
|
+
parameters = parameters[1:]
|
|
874
|
+
for parameter, binding in zip(parameters, positional):
|
|
875
|
+
if binding is not None:
|
|
876
|
+
frame.vars[parameter.arg] = binding
|
|
877
|
+
for parameter in parameters:
|
|
878
|
+
binding = named.get(parameter.arg)
|
|
879
|
+
if binding is not None:
|
|
880
|
+
frame.vars[parameter.arg] = binding
|
|
881
|
+
|
|
882
|
+
def deferred(self, d: Draft, frame: Frame, node: ast.AST, depth: int, ran: List[UseCase]) -> None:
|
|
883
|
+
"""What `on_commit` was handed: a lambda, whose body is read where the
|
|
884
|
+
lambda is; `partial(task.delay, ...)`, which is the enqueue it binds;
|
|
885
|
+
or anything else, read as a value."""
|
|
886
|
+
if isinstance(node, ast.Lambda):
|
|
887
|
+
self.value(d, frame, node.body, depth, ran)
|
|
888
|
+
return
|
|
889
|
+
if isinstance(node, ast.Call) and dotted(node.func).split(".")[-1] == "partial" and node.args:
|
|
890
|
+
bound = node.args[0]
|
|
891
|
+
if isinstance(bound, ast.Attribute) and bound.attr in ENQUEUE:
|
|
892
|
+
task = self.task_of(frame.module, bound.value)
|
|
893
|
+
if task is not None:
|
|
894
|
+
self.enqueue(d, task, "", frame.module.where(node))
|
|
895
|
+
return
|
|
896
|
+
self.value(d, frame, node, depth, ran)
|
|
897
|
+
|
|
898
|
+
def celery(self) -> Tuple[celery_conf.Config, Dict[Tuple[str, str], celery_tasks.Task]]:
|
|
899
|
+
"""The tasks of the tree and the configuration that places them, read
|
|
900
|
+
once, the first time an enqueue is met."""
|
|
901
|
+
if self._celery is None:
|
|
902
|
+
by_key, _ = celery_tasks.index(celery_tasks.read_tasks(self.project))
|
|
903
|
+
self._celery = (celery_conf.read_config(self.project, self.opts.settings), by_key)
|
|
904
|
+
return self._celery
|
|
905
|
+
|
|
906
|
+
def task_of(self, module: Module, receiver: ast.AST) -> Optional[celery_tasks.Task]:
|
|
907
|
+
"""The task `.delay` was called on, when the name resolves by import to
|
|
908
|
+
a function decorated as one; None for anything else."""
|
|
909
|
+
if isinstance(receiver, ast.Call) and isinstance(receiver.func, ast.Attribute) and receiver.func.attr in SIGNATURE:
|
|
910
|
+
receiver = receiver.func.value
|
|
911
|
+
name = dotted(receiver)
|
|
912
|
+
if not name:
|
|
913
|
+
return None
|
|
914
|
+
parts = name.split(".")
|
|
915
|
+
if len(parts) == 1:
|
|
916
|
+
hit = self.project.resolve(module, name)
|
|
917
|
+
if hit is None:
|
|
918
|
+
return None
|
|
919
|
+
target, local = hit
|
|
920
|
+
key = (celery_tasks.package_of(target), local)
|
|
921
|
+
else:
|
|
922
|
+
imported = module.imports.get(parts[0])
|
|
923
|
+
if imported is None:
|
|
924
|
+
return None
|
|
925
|
+
package = imported.module if imported.name == "*" else imported.module + "." + imported.name
|
|
926
|
+
key = (".".join([package] + parts[1:-1]), parts[-1])
|
|
927
|
+
_, by_key = self.celery()
|
|
928
|
+
return by_key.get(key)
|
|
929
|
+
|
|
930
|
+
def enqueue(self, d: Draft, task: celery_tasks.Task, at_call: str, line: str) -> None:
|
|
931
|
+
"""One lane per queue, named as `extract-celery` names it, so the two
|
|
932
|
+
flows meet on the same participant."""
|
|
933
|
+
cfg, _ = self.celery()
|
|
934
|
+
address, _ = celery_conf.queue_for(task.name, at_call, task.queue, cfg)
|
|
935
|
+
lane = d.lane("celery-" + slug(address), "broker", None, "Celery · " + address)
|
|
936
|
+
d.add(
|
|
937
|
+
self.opts.svc_id,
|
|
938
|
+
lane,
|
|
939
|
+
"call",
|
|
940
|
+
"enqueue " + task.short,
|
|
941
|
+
line=line,
|
|
942
|
+
handoff={
|
|
943
|
+
"kind": "job",
|
|
944
|
+
"transport": "celery",
|
|
945
|
+
"channel": address,
|
|
946
|
+
"message": task.name,
|
|
947
|
+
"direction": "send",
|
|
948
|
+
},
|
|
949
|
+
)
|
|
950
|
+
|
|
951
|
+
def address_of(self, module: Module, node: ast.AST) -> str:
|
|
952
|
+
"""The topic a producer was handed, as far as syntax carries it: the
|
|
953
|
+
literal, a module-level constant, or `settings.X` read out of the
|
|
954
|
+
settings module. A name in the settings module follows once more."""
|
|
955
|
+
text = celery_conf.str_value(node, module)
|
|
956
|
+
if text:
|
|
957
|
+
return text
|
|
958
|
+
name = dotted(node)
|
|
959
|
+
if name.startswith("settings.") and name.count(".") == 1:
|
|
960
|
+
if self._settings is None:
|
|
961
|
+
found = self.project.module(self.opts.settings or celery_conf.settings_module_name(self.project))
|
|
962
|
+
self._settings = found if found is not None else False # type: ignore[assignment]
|
|
963
|
+
if self._settings:
|
|
964
|
+
return celery_conf.str_value(ast.Name(id=name.split(".")[1]), self._settings)
|
|
965
|
+
return ""
|
|
966
|
+
|
|
967
|
+
def helper_address(self, module: Module, name: str) -> str:
|
|
968
|
+
"""`bus.publish(event)`: a function of this project, one hop away,
|
|
969
|
+
whose body puts what it was handed on an address. Followed one call
|
|
970
|
+
further - a sync wrapper around an async publish - and no more."""
|
|
971
|
+
parts = name.split(".")
|
|
972
|
+
target: Optional[Module] = None
|
|
973
|
+
local = parts[-1]
|
|
974
|
+
if len(parts) == 1:
|
|
975
|
+
hit = self.project.resolve(module, name)
|
|
976
|
+
if hit is not None:
|
|
977
|
+
target, local = hit
|
|
978
|
+
else:
|
|
979
|
+
imported = module.imports.get(parts[0])
|
|
980
|
+
if imported is not None:
|
|
981
|
+
package = imported.module if imported.name == "*" else imported.module + "." + imported.name
|
|
982
|
+
target = self.project.module(".".join([package] + parts[1:-1]))
|
|
983
|
+
if target is None or target.dotted.split(".")[-1] == "services":
|
|
984
|
+
return ""
|
|
985
|
+
return self.produced_in(target, local, 0)
|
|
986
|
+
|
|
987
|
+
def produced_in(self, module: Module, function: str, depth: int) -> str:
|
|
988
|
+
fn = next((f for f in module.functions() if f.name == function), None)
|
|
989
|
+
if fn is None or depth > 1:
|
|
990
|
+
return ""
|
|
991
|
+
for node in ast.walk(fn):
|
|
992
|
+
if isinstance(node, ast.Call) and dotted(node.func).split(".")[-1] in PRODUCE_METHODS and node.args:
|
|
993
|
+
address = self.address_of(module, node.args[0])
|
|
994
|
+
if address:
|
|
995
|
+
return address
|
|
996
|
+
for node in ast.walk(fn):
|
|
997
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
|
|
998
|
+
address = self.produced_in(module, node.func.id, depth + 1)
|
|
999
|
+
if address:
|
|
1000
|
+
return address
|
|
1001
|
+
return ""
|
|
1002
|
+
|
|
1003
|
+
def produce(self, d: Draft, address: str, event_id: str, line: str) -> None:
|
|
1004
|
+
lane = d.lane(LANE_BUS, "broker", None)
|
|
1005
|
+
if event_id:
|
|
1006
|
+
d.add(self.opts.svc_id, lane, "event", event_id.rsplit(".", 1)[-1], ref=event_id, note="on " + address, line=line)
|
|
1007
|
+
self.referenced.add(event_id)
|
|
1008
|
+
self.produced.setdefault(event_id, []).append((address, line))
|
|
1009
|
+
else:
|
|
1010
|
+
d.add(self.opts.svc_id, lane, "call", "publish on " + address, line=line)
|
|
1011
|
+
|
|
1012
|
+
def publish(self, d: Draft, event_id: str, line: str) -> None:
|
|
1013
|
+
d.add(self.opts.svc_id, d.lane(LANE_BUS, "broker", None), "event", event_id.rsplit(".", 1)[-1], ref=event_id, line=line)
|
|
1014
|
+
self.referenced.add(event_id)
|
|
1015
|
+
|
|
1016
|
+
def emits(self, model: ModelDef, method: str) -> str:
|
|
1017
|
+
"""A method of the root hands back the event of the move it made."""
|
|
1018
|
+
for node in methods(model.node):
|
|
1019
|
+
if node.name != method:
|
|
1020
|
+
continue
|
|
1021
|
+
annotation = getattr(node, "returns", None)
|
|
1022
|
+
if annotation is None:
|
|
1023
|
+
return ""
|
|
1024
|
+
for child in ast.walk(annotation):
|
|
1025
|
+
if isinstance(child, ast.Name) and child.id in self.events:
|
|
1026
|
+
return self.events[child.id].id
|
|
1027
|
+
if isinstance(child, ast.Constant) and isinstance(child.value, str) and child.value in self.events:
|
|
1028
|
+
return self.events[child.value].id
|
|
1029
|
+
return ""
|
|
1030
|
+
|
|
1031
|
+
def rpc(self, d: Draft, client: Client, method: str, line: str):
|
|
1032
|
+
call = client.calls.get(method)
|
|
1033
|
+
if call is None:
|
|
1034
|
+
return None
|
|
1035
|
+
lane, peer, status = self.peer_lane(d, call.pkg)
|
|
1036
|
+
d.add(self.opts.svc_id, lane, "rpc", call.label, status, call.id if status == catalog.DECLARED else "", line=line)
|
|
1037
|
+
if status == catalog.DECLARED:
|
|
1038
|
+
self.calls[call.id] = catalog.rpc_call(call.id, peer, catalog.DECLARED, call.source)
|
|
1039
|
+
else:
|
|
1040
|
+
self.calls[call.id] = catalog.rpc_call(call.id, peer, catalog.UNRESOLVED, call.source)
|
|
1041
|
+
return None
|
|
1042
|
+
|
|
1043
|
+
# --- project calls and standard HTTP clients ----------------------------
|
|
1044
|
+
|
|
1045
|
+
def class_target(self, module: Module, name: str) -> Optional[ClassTarget]:
|
|
1046
|
+
return self.classes.get((module.dotted, name))
|
|
1047
|
+
|
|
1048
|
+
def resolve_class(self, module: Module, name: str) -> Optional[ClassTarget]:
|
|
1049
|
+
if not name:
|
|
1050
|
+
return None
|
|
1051
|
+
if "." not in name:
|
|
1052
|
+
hit = self.project.resolve(module, name)
|
|
1053
|
+
if hit is not None:
|
|
1054
|
+
return self.classes.get((hit[0].dotted, hit[1]))
|
|
1055
|
+
return self.classes.get((module.dotted, name))
|
|
1056
|
+
parts = name.split(".")
|
|
1057
|
+
imported = module.imports.get(parts[0])
|
|
1058
|
+
if imported is None:
|
|
1059
|
+
return None
|
|
1060
|
+
if imported.name == "*":
|
|
1061
|
+
target_module = self.project.module(".".join([imported.module] + parts[1:-1]))
|
|
1062
|
+
return self.classes.get((target_module.dotted, parts[-1])) if target_module is not None else None
|
|
1063
|
+
direct = self.classes.get((imported.module, imported.name))
|
|
1064
|
+
if direct is not None and len(parts) == 1:
|
|
1065
|
+
return direct
|
|
1066
|
+
module_name = imported.module + "." + imported.name
|
|
1067
|
+
target_module = self.project.module(".".join([module_name] + parts[1:-1]))
|
|
1068
|
+
return self.classes.get((target_module.dotted, parts[-1])) if target_module is not None else None
|
|
1069
|
+
|
|
1070
|
+
def callable_target(self, frame: Frame, func: ast.AST, holder) -> Optional[CallableTarget]:
|
|
1071
|
+
name = dotted(func)
|
|
1072
|
+
if not name:
|
|
1073
|
+
return None
|
|
1074
|
+
local = frame.vars.get(name)
|
|
1075
|
+
if local is not None and local[0] == "callable":
|
|
1076
|
+
return local[1]
|
|
1077
|
+
if isinstance(func, ast.Attribute):
|
|
1078
|
+
if isinstance(func.value, ast.Call) and dotted(func.value.func) == "super":
|
|
1079
|
+
return self.super_method(frame.owner, func.attr)
|
|
1080
|
+
receiver = dotted(func.value)
|
|
1081
|
+
if receiver in ("self", "cls") and frame.owner is not None:
|
|
1082
|
+
return self.method_target(frame.owner, func.attr)
|
|
1083
|
+
if holder is not None and holder[0] == "object":
|
|
1084
|
+
return self.method_target(holder[1], func.attr)
|
|
1085
|
+
owner = self.resolve_class(frame.module, receiver)
|
|
1086
|
+
if owner is not None:
|
|
1087
|
+
return self.method_target(owner, func.attr)
|
|
1088
|
+
return self.resolve_function(frame.module, name)
|
|
1089
|
+
|
|
1090
|
+
def resolve_function(self, module: Module, name: str) -> Optional[CallableTarget]:
|
|
1091
|
+
parts = name.split(".")
|
|
1092
|
+
if len(parts) == 1:
|
|
1093
|
+
hit = self.project.resolve(module, name)
|
|
1094
|
+
if hit is None:
|
|
1095
|
+
return None
|
|
1096
|
+
return self.functions.get((hit[0].dotted, hit[1]))
|
|
1097
|
+
imported = module.imports.get(parts[0])
|
|
1098
|
+
if imported is None:
|
|
1099
|
+
return None
|
|
1100
|
+
if imported.name == "*":
|
|
1101
|
+
target = self.project.module(".".join([imported.module] + parts[1:-1]))
|
|
1102
|
+
else:
|
|
1103
|
+
module_name = imported.module + "." + imported.name
|
|
1104
|
+
target = self.project.module(".".join([module_name] + parts[1:-1]))
|
|
1105
|
+
if target is None and len(parts) == 2:
|
|
1106
|
+
target = self.project.module(imported.module)
|
|
1107
|
+
return self.functions.get((target.dotted, parts[-1])) if target is not None else None
|
|
1108
|
+
|
|
1109
|
+
def method_target(self, owner: ClassTarget, name: str) -> Optional[CallableTarget]:
|
|
1110
|
+
node = next((item for item in methods(owner.node) if item.name == name), None)
|
|
1111
|
+
if node is not None:
|
|
1112
|
+
return CallableTarget(owner.module, node, owner)
|
|
1113
|
+
for base in owner.node.bases:
|
|
1114
|
+
parent = self.resolve_class(owner.module, dotted(base))
|
|
1115
|
+
if parent is not None:
|
|
1116
|
+
found = self.method_target(parent, name)
|
|
1117
|
+
if found is not None:
|
|
1118
|
+
return found
|
|
1119
|
+
return None
|
|
1120
|
+
|
|
1121
|
+
def super_method(self, owner: Optional[ClassTarget], name: str) -> Optional[CallableTarget]:
|
|
1122
|
+
if owner is None:
|
|
1123
|
+
return None
|
|
1124
|
+
for base in owner.node.bases:
|
|
1125
|
+
parent = self.resolve_class(owner.module, dotted(base))
|
|
1126
|
+
if parent is not None:
|
|
1127
|
+
found = self.method_target(parent, name)
|
|
1128
|
+
if found is not None:
|
|
1129
|
+
return found
|
|
1130
|
+
return None
|
|
1131
|
+
|
|
1132
|
+
def class_env(self, target: ClassTarget) -> Dict[str, Tuple[str, object]]:
|
|
1133
|
+
out: Dict[str, Tuple[str, object]] = {}
|
|
1134
|
+
for name, value, _ in assigned(target.node):
|
|
1135
|
+
binding = self.constructed(target.module, value)
|
|
1136
|
+
if binding is not None:
|
|
1137
|
+
out["self." + name] = binding
|
|
1138
|
+
init = next((node for node in methods(target.node) if node.name == "__init__"), None)
|
|
1139
|
+
if init is not None:
|
|
1140
|
+
for stmt in ast.walk(init):
|
|
1141
|
+
if isinstance(stmt, ast.Assign) and len(stmt.targets) == 1 and isinstance(stmt.targets[0], ast.Attribute):
|
|
1142
|
+
binding = self.constructed(target.module, stmt.value)
|
|
1143
|
+
if binding is not None:
|
|
1144
|
+
out["self." + stmt.targets[0].attr] = binding
|
|
1145
|
+
return out
|
|
1146
|
+
|
|
1147
|
+
def model_for(self, module: Module, name: str) -> Optional[ModelDef]:
|
|
1148
|
+
short = name.split(".")[-1]
|
|
1149
|
+
candidates = self.models_by_name.get(short, [])
|
|
1150
|
+
if not candidates:
|
|
1151
|
+
return None
|
|
1152
|
+
hit = self.project.resolve(module, short) if "." not in name else None
|
|
1153
|
+
if hit is not None:
|
|
1154
|
+
exact = [model for model in candidates if model.module.dotted == hit[0].dotted and model.name == hit[1]]
|
|
1155
|
+
if len(exact) == 1:
|
|
1156
|
+
return exact[0]
|
|
1157
|
+
same_package = [model for model in candidates if module.dotted == model.app.dotted or module.dotted.startswith(model.app.dotted + ".")]
|
|
1158
|
+
if len(same_package) == 1:
|
|
1159
|
+
return same_package[0]
|
|
1160
|
+
return candidates[0] if len(candidates) == 1 else None
|
|
1161
|
+
|
|
1162
|
+
def external_name(self, module: Module, name: str) -> str:
|
|
1163
|
+
if not name:
|
|
1164
|
+
return ""
|
|
1165
|
+
parts = name.split(".")
|
|
1166
|
+
imported = module.imports.get(parts[0])
|
|
1167
|
+
if imported is None:
|
|
1168
|
+
return name
|
|
1169
|
+
base = imported.module if imported.name == "*" else imported.module + "." + imported.name
|
|
1170
|
+
return ".".join([base] + parts[1:])
|
|
1171
|
+
|
|
1172
|
+
def http_call(self, module: Module, node: ast.Call, holder, positional) -> Optional[Tuple[str, str]]:
|
|
1173
|
+
name = dotted(node.func)
|
|
1174
|
+
last = name.split(".")[-1].lower()
|
|
1175
|
+
if holder is not None and holder[0] == "http" and last in HTTP_METHODS:
|
|
1176
|
+
url = binding_string(positional[0]) if positional else ""
|
|
1177
|
+
url = url or (self.string_value(module, node.args[0]) if node.args else "")
|
|
1178
|
+
return last.upper(), url or expression(node.args[0] if node.args else None)
|
|
1179
|
+
external = self.external_name(module, name)
|
|
1180
|
+
library = external.split(".", 1)[0]
|
|
1181
|
+
if library not in HTTP_LIBRARIES:
|
|
1182
|
+
return None
|
|
1183
|
+
if last == "request" and len(node.args) >= 2:
|
|
1184
|
+
method = binding_string(positional[0]).upper() if positional else ""
|
|
1185
|
+
target = binding_string(positional[1]) if len(positional) > 1 else ""
|
|
1186
|
+
return method or "HTTP", target or self.string_value(module, node.args[1]) or expression(node.args[1])
|
|
1187
|
+
if last not in HTTP_METHODS:
|
|
1188
|
+
return None
|
|
1189
|
+
url = binding_string(positional[0]) if positional else ""
|
|
1190
|
+
url = url or (self.string_value(module, node.args[0]) if node.args else "")
|
|
1191
|
+
return last.upper(), url or expression(node.args[0] if node.args else None)
|
|
1192
|
+
|
|
1193
|
+
def string_value(self, module: Module, node: Optional[ast.AST], depth: int = 0) -> str:
|
|
1194
|
+
if node is None or depth > 5:
|
|
1195
|
+
return ""
|
|
1196
|
+
text = celery_conf.str_value(node, module)
|
|
1197
|
+
if text:
|
|
1198
|
+
return text
|
|
1199
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
|
|
1200
|
+
left = self.string_value(module, node.left, depth + 1)
|
|
1201
|
+
right = self.string_value(module, node.right, depth + 1)
|
|
1202
|
+
return left + right if left or right else ""
|
|
1203
|
+
if isinstance(node, ast.JoinedStr):
|
|
1204
|
+
chunks = []
|
|
1205
|
+
for item in node.values:
|
|
1206
|
+
if isinstance(item, ast.Constant) and isinstance(item.value, str):
|
|
1207
|
+
chunks.append(item.value)
|
|
1208
|
+
elif isinstance(item, ast.FormattedValue):
|
|
1209
|
+
chunks.append("{%s}" % expression(item.value))
|
|
1210
|
+
return "".join(chunks)
|
|
1211
|
+
if isinstance(node, ast.Call) and self.external_name(module, dotted(node.func)) == "urllib.parse.urljoin" and len(node.args) >= 2:
|
|
1212
|
+
left = self.string_value(module, node.args[0], depth + 1)
|
|
1213
|
+
right = self.string_value(module, node.args[1], depth + 1)
|
|
1214
|
+
return left.rstrip("/") + "/" + right.lstrip("/") if left or right else ""
|
|
1215
|
+
name = dotted(node)
|
|
1216
|
+
if not name:
|
|
1217
|
+
return ""
|
|
1218
|
+
if name.startswith("settings."):
|
|
1219
|
+
return self.address_of(module, node)
|
|
1220
|
+
if "." not in name:
|
|
1221
|
+
hit = self.project.resolve(module, name)
|
|
1222
|
+
if hit is not None and hit[0] is not module:
|
|
1223
|
+
return self.string_value(hit[0], ast.Name(id=hit[1]), depth + 1)
|
|
1224
|
+
return ""
|
|
1225
|
+
|
|
1226
|
+
def http_step(self, d: Draft, method: str, target: str, line: str) -> None:
|
|
1227
|
+
parsed = urlparse(target)
|
|
1228
|
+
peer = parsed.netloc or parsed.path.split("/", 1)[0] or "external-http"
|
|
1229
|
+
path = parsed.path if parsed.netloc else target
|
|
1230
|
+
label = "%s %s" % (method, path or "/")
|
|
1231
|
+
lane = d.lane("http-" + slug(peer), "unknown", None, peer)
|
|
1232
|
+
d.add(self.opts.svc_id, lane, "rpc", label, catalog.UNRESOLVED, line=line)
|
|
1233
|
+
|
|
1234
|
+
def use_case(self, module: Module, name: str) -> Optional[UseCase]:
|
|
1235
|
+
"""`issue_invoice(...)` imported from the services module,
|
|
1236
|
+
`services.issue_invoice(...)` through the module itself, or a function
|
|
1237
|
+
of the module being read."""
|
|
1238
|
+
parts = name.split(".")
|
|
1239
|
+
keys = []
|
|
1240
|
+
imported = module.imports.get(parts[0])
|
|
1241
|
+
if imported is not None:
|
|
1242
|
+
if imported.name != "*":
|
|
1243
|
+
keys.append(imported.module + "." + imported.name) # from .services import x
|
|
1244
|
+
keys.append(imported.module + "." + imported.name + "." + parts[-1]) # from . import services
|
|
1245
|
+
keys.append(imported.module + "." + parts[-1]) # import package.services
|
|
1246
|
+
keys.append(module.dotted + "." + parts[-1])
|
|
1247
|
+
for key in keys:
|
|
1248
|
+
hit = self.use_cases.get(key)
|
|
1249
|
+
if hit is not None:
|
|
1250
|
+
return hit
|
|
1251
|
+
return None
|
|
1252
|
+
|
|
1253
|
+
def module_env(self, module: Module) -> Dict[str, Tuple[str, object]]:
|
|
1254
|
+
"""What a module-level name holds before a body starts: the clients a
|
|
1255
|
+
module keeps as singletons, which is how a Django project holds one."""
|
|
1256
|
+
out: Dict[str, Tuple[str, object]] = {}
|
|
1257
|
+
for name, value, _ in assigned(module.tree):
|
|
1258
|
+
binding = self.constructed(module, value)
|
|
1259
|
+
if binding is not None:
|
|
1260
|
+
out[name] = binding
|
|
1261
|
+
return out
|
|
1262
|
+
|
|
1263
|
+
|
|
1264
|
+
def terminal(body: List[ast.AST]) -> bool:
|
|
1265
|
+
return bool(body) and isinstance(body[-1], (ast.Return, ast.Raise))
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
def expression(node: Optional[ast.AST]) -> str:
|
|
1269
|
+
if node is None:
|
|
1270
|
+
return ""
|
|
1271
|
+
try:
|
|
1272
|
+
return " ".join(ast.unparse(node).split()) # type: ignore[attr-defined]
|
|
1273
|
+
except Exception:
|
|
1274
|
+
return dotted(node)
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
def binding_string(binding) -> str:
|
|
1278
|
+
return binding[1] if isinstance(binding, tuple) and len(binding) > 1 and binding[0] == "string" else ""
|