pyfect-dev 0.1.0__py3-none-any.whl
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.
- pyfect/__init__.py +729 -0
- pyfect/_all.py +242 -0
- pyfect/_background.py +28 -0
- pyfect/_binary_codecs.py +124 -0
- pyfect/_cached.py +165 -0
- pyfect/_catch.py +128 -0
- pyfect/_catch_reason.py +574 -0
- pyfect/_concurrency.py +291 -0
- pyfect/_context_effects.py +163 -0
- pyfect/_context_managers.py +76 -0
- pyfect/_core.py +368 -0
- pyfect/_digits.py +71 -0
- pyfect/_effect_args.py +46 -0
- pyfect/_effect_base.py +71 -0
- pyfect/_effect_concurrency.py +133 -0
- pyfect/_effect_eager.py +103 -0
- pyfect/_effect_errors.py +344 -0
- pyfect/_effect_folding.py +110 -0
- pyfect/_effect_hash.py +140 -0
- pyfect/_effect_logging.py +57 -0
- pyfect/_effect_metrics.py +102 -0
- pyfect/_effect_resources.py +179 -0
- pyfect/_effect_services.py +25 -0
- pyfect/_effect_timing.py +199 -0
- pyfect/_effect_tracing.py +99 -0
- pyfect/_equal.py +195 -0
- pyfect/_extras.py +43 -0
- pyfect/_fiber.py +969 -0
- pyfect/_fiber_all.py +134 -0
- pyfect/_fiber_collections.py +106 -0
- pyfect/_fn.py +706 -0
- pyfect/_hamt.py +174 -0
- pyfect/_interrupt_reason.py +33 -0
- pyfect/_isaac.py +149 -0
- pyfect/_iterables.py +231 -0
- pyfect/_js.py +263 -0
- pyfect/_js_date.py +87 -0
- pyfect/_json_text.py +286 -0
- pyfect/_json_value.py +9 -0
- pyfect/_layer_core.py +403 -0
- pyfect/_layer_for_life.py +33 -0
- pyfect/_layer_tracing.py +70 -0
- pyfect/_locked_ref.py +147 -0
- pyfect/_loop_thread.py +152 -0
- pyfect/_metric_ref.py +29 -0
- pyfect/_one_or_many.py +14 -0
- pyfect/_os_wait.py +118 -0
- pyfect/_path_posix.py +129 -0
- pyfect/_primitives.py +434 -0
- pyfect/_public.py +36 -0
- pyfect/_reasons.py +64 -0
- pyfect/_released.py +67 -0
- pyfect/_root.py +87 -0
- pyfect/_round_robin.py +73 -0
- pyfect/_some.py +17 -0
- pyfect/_stack.py +52 -0
- pyfect/_stack_frame.py +114 -0
- pyfect/_string_hash.py +35 -0
- pyfect/_subscriber_queue.py +47 -0
- pyfect/_tags.py +14 -0
- pyfect/_tracer_ref.py +21 -0
- pyfect/_tx_ops.py +77 -0
- pyfect/_unsubscripted.py +23 -0
- pyfect/_url.py +54 -0
- pyfect/_waiters.py +127 -0
- pyfect/_wake_tracking.py +98 -0
- pyfect/_wire_codec.py +123 -0
- pyfect/ai/__init__.py +73 -0
- pyfect/ai/_approvals.py +249 -0
- pyfect/ai/_decision_answers.py +185 -0
- pyfect/ai/_generated_client.py +298 -0
- pyfect/ai/_generated_support.py +84 -0
- pyfect/ai/_mcp/__init__.py +1 -0
- pyfect/ai/_mcp/batch.py +68 -0
- pyfect/ai/_mcp/client_transport.py +412 -0
- pyfect/ai/_mcp/client_wire.py +284 -0
- pyfect/ai/_mcp/core.py +490 -0
- pyfect/ai/_mcp/dispatch.py +1048 -0
- pyfect/ai/_mcp/http.py +462 -0
- pyfect/ai/_mcp/http_admission.py +269 -0
- pyfect/ai/_mcp/http_status.py +27 -0
- pyfect/ai/_mcp/input_wire.py +226 -0
- pyfect/ai/_mcp/log_levels.py +55 -0
- pyfect/ai/_mcp/param_headers.py +63 -0
- pyfect/ai/_mcp/projection.py +248 -0
- pyfect/ai/_mcp/register.py +683 -0
- pyfect/ai/_mcp/reverse_policy.py +155 -0
- pyfect/ai/_mcp/routing_header.py +24 -0
- pyfect/ai/_mcp/sessions.py +114 -0
- pyfect/ai/_mcp/stdio.py +110 -0
- pyfect/ai/_mcp/subscriptions.py +134 -0
- pyfect/ai/_mcp/tool_schema.py +35 -0
- pyfect/ai/_noul_answers.py +64 -0
- pyfect/ai/_openai_all_of.py +137 -0
- pyfect/ai/_openai_errors.py +233 -0
- pyfect/ai/_provider.py +439 -0
- pyfect/ai/_provider_tools.py +129 -0
- pyfect/ai/_stream_resolution.py +231 -0
- pyfect/ai/_structured_output_checks.py +138 -0
- pyfect/ai/_structured_output_codec.py +229 -0
- pyfect/ai/_structured_output_json.py +109 -0
- pyfect/ai/_tool_call_ids.py +80 -0
- pyfect/ai/ai_error.py +785 -0
- pyfect/ai/anthropic/__init__.py +28 -0
- pyfect/ai/anthropic/_errors.py +164 -0
- pyfect/ai/anthropic/_prompt.py +375 -0
- pyfect/ai/anthropic/_response.py +443 -0
- pyfect/ai/anthropic/_schema.py +206 -0
- pyfect/ai/anthropic/_stream.py +377 -0
- pyfect/ai/anthropic/_tools.py +248 -0
- pyfect/ai/anthropic/client.py +289 -0
- pyfect/ai/anthropic/config.py +32 -0
- pyfect/ai/anthropic/error.py +27 -0
- pyfect/ai/anthropic/language_model.py +366 -0
- pyfect/ai/anthropic/telemetry.py +51 -0
- pyfect/ai/anthropic/tool.py +570 -0
- pyfect/ai/anthropic_structured_output.py +138 -0
- pyfect/ai/chat.py +548 -0
- pyfect/ai/cloudflare/__init__.py +15 -0
- pyfect/ai/cloudflare/_errors.py +108 -0
- pyfect/ai/cloudflare/client.py +174 -0
- pyfect/ai/cloudflare/decision_model.py +117 -0
- pyfect/ai/cloudflare/schema.py +144 -0
- pyfect/ai/decision.py +290 -0
- pyfect/ai/decision_model.py +283 -0
- pyfect/ai/embedding_model.py +177 -0
- pyfect/ai/id_generator.py +79 -0
- pyfect/ai/language_model.py +1077 -0
- pyfect/ai/mcp_client.py +682 -0
- pyfect/ai/mcp_client_error.py +223 -0
- pyfect/ai/mcp_protocol.py +205 -0
- pyfect/ai/mcp_schema.py +1300 -0
- pyfect/ai/mcp_server.py +404 -0
- pyfect/ai/model.py +56 -0
- pyfect/ai/openai/__init__.py +49 -0
- pyfect/ai/openai/_errors.py +89 -0
- pyfect/ai/openai/_prompt.py +527 -0
- pyfect/ai/openai/_requests.py +34 -0
- pyfect/ai/openai/_response.py +569 -0
- pyfect/ai/openai/_schema.py +769 -0
- pyfect/ai/openai/_stream.py +771 -0
- pyfect/ai/openai/_tools.py +128 -0
- pyfect/ai/openai/_web_socket.py +295 -0
- pyfect/ai/openai/client.py +296 -0
- pyfect/ai/openai/client_generated.py +123 -0
- pyfect/ai/openai/config.py +34 -0
- pyfect/ai/openai/decision_model.py +177 -0
- pyfect/ai/openai/embedding_model.py +116 -0
- pyfect/ai/openai/error.py +8 -0
- pyfect/ai/openai/generated/__init__.py +7213 -0
- pyfect/ai/openai/generated/_recursive.py +18 -0
- pyfect/ai/openai/generated/_schemas_0.py +1015 -0
- pyfect/ai/openai/generated/_schemas_1.py +1016 -0
- pyfect/ai/openai/generated/_schemas_2.py +1017 -0
- pyfect/ai/openai/generated/_schemas_3.py +615 -0
- pyfect/ai/openai/generated/client.py +5298 -0
- pyfect/ai/openai/generated/types.py +8123 -0
- pyfect/ai/openai/language_model.py +304 -0
- pyfect/ai/openai/telemetry.py +19 -0
- pyfect/ai/openai/tool.py +391 -0
- pyfect/ai/openai_compat/__init__.py +25 -0
- pyfect/ai/openai_compat/_prompt.py +191 -0
- pyfect/ai/openai_compat/_request.py +153 -0
- pyfect/ai/openai_compat/_response.py +407 -0
- pyfect/ai/openai_compat/_schema.py +97 -0
- pyfect/ai/openai_compat/client.py +234 -0
- pyfect/ai/openai_compat/config.py +32 -0
- pyfect/ai/openai_compat/embedding_model.py +139 -0
- pyfect/ai/openai_compat/error.py +7 -0
- pyfect/ai/openai_compat/language_model.py +245 -0
- pyfect/ai/openai_compat/telemetry.py +51 -0
- pyfect/ai/openai_structured_output.py +118 -0
- pyfect/ai/openrouter/__init__.py +36 -0
- pyfect/ai/openrouter/_errors.py +186 -0
- pyfect/ai/openrouter/_prompt.py +272 -0
- pyfect/ai/openrouter/_response.py +226 -0
- pyfect/ai/openrouter/_stream.py +320 -0
- pyfect/ai/openrouter/client.py +283 -0
- pyfect/ai/openrouter/config.py +36 -0
- pyfect/ai/openrouter/decision_model.py +161 -0
- pyfect/ai/openrouter/error.py +23 -0
- pyfect/ai/openrouter/generated/__init__.py +3029 -0
- pyfect/ai/openrouter/generated/_recursive.py +15 -0
- pyfect/ai/openrouter/generated/_schemas_0.py +1015 -0
- pyfect/ai/openrouter/generated/_schemas_1.py +522 -0
- pyfect/ai/openrouter/generated/client.py +1445 -0
- pyfect/ai/openrouter/generated/types.py +4769 -0
- pyfect/ai/openrouter/language_model.py +345 -0
- pyfect/ai/openrouter/schema.py +165 -0
- pyfect/ai/prompt.py +664 -0
- pyfect/ai/response.py +768 -0
- pyfect/ai/response_id_tracker.py +82 -0
- pyfect/ai/telemetry.py +126 -0
- pyfect/ai/tokenizer.py +61 -0
- pyfect/ai/tool.py +536 -0
- pyfect/ai/toolkit.py +490 -0
- pyfect/ai/typesafe/__init__.py +17 -0
- pyfect/ai/typesafe/_errors.py +120 -0
- pyfect/ai/typesafe/client.py +146 -0
- pyfect/ai/typesafe/config.py +34 -0
- pyfect/ai/typesafe/decision_model.py +90 -0
- pyfect/ai/typesafe/schema.py +140 -0
- pyfect/batching/__init__.py +58 -0
- pyfect/batching/_api.py +71 -0
- pyfect/batching/_engine.py +229 -0
- pyfect/batching/_from_effect.py +76 -0
- pyfect/batching/_persisted.py +98 -0
- pyfect/batching/_request.py +133 -0
- pyfect/batching/_resolver.py +477 -0
- pyfect/batching/_with_cache.py +90 -0
- pyfect/big_decimal.py +500 -0
- pyfect/brand.py +114 -0
- pyfect/byte_size.py +354 -0
- pyfect/cache/__init__.py +29 -0
- pyfect/cache/_cache.py +426 -0
- pyfect/cache/_expiry.py +69 -0
- pyfect/cache/_scoped_cache.py +546 -0
- pyfect/case.py +174 -0
- pyfect/cause.py +462 -0
- pyfect/channel/__init__.py +155 -0
- pyfect/channel/_base.py +92 -0
- pyfect/channel/_catch.py +158 -0
- pyfect/channel/_catch_reason.py +572 -0
- pyfect/channel/_channel.py +158 -0
- pyfect/channel/_combinators.py +220 -0
- pyfect/channel/_concurrency_methods.py +217 -0
- pyfect/channel/_concurrent_map.py +118 -0
- pyfect/channel/_constructors.py +349 -0
- pyfect/channel/_context.py +232 -0
- pyfect/channel/_error_methods.py +336 -0
- pyfect/channel/_errors.py +180 -0
- pyfect/channel/_filters.py +225 -0
- pyfect/channel/_lifecycle.py +71 -0
- pyfect/channel/_lifecycle_methods.py +206 -0
- pyfect/channel/_loop.py +100 -0
- pyfect/channel/_merge.py +246 -0
- pyfect/channel/_repeat.py +154 -0
- pyfect/channel/_run.py +149 -0
- pyfect/channel/_run_methods.py +341 -0
- pyfect/channel/_sinks.py +88 -0
- pyfect/channel/_sources.py +238 -0
- pyfect/channel/_text.py +176 -0
- pyfect/channel/_transform_methods.py +415 -0
- pyfect/channel/_upstream.py +67 -0
- pyfect/channel_schema.py +131 -0
- pyfect/cli/__init__.py +79 -0
- pyfect/cli/_ansi.py +82 -0
- pyfect/cli/_command.py +469 -0
- pyfect/cli/_config.py +164 -0
- pyfect/cli/_environment.py +10 -0
- pyfect/cli/_help.py +195 -0
- pyfect/cli/_lexer.py +81 -0
- pyfect/cli/_parser.py +465 -0
- pyfect/cli/_settings.py +85 -0
- pyfect/cli/_suggest.py +33 -0
- pyfect/cli/_width.py +134 -0
- pyfect/cli/_wizard.py +317 -0
- pyfect/cli/argument.py +212 -0
- pyfect/cli/cli_config.py +40 -0
- pyfect/cli/cli_error.py +242 -0
- pyfect/cli/cli_output.py +305 -0
- pyfect/cli/command.py +440 -0
- pyfect/cli/completions/__init__.py +50 -0
- pyfect/cli/completions/_completer.ps1 +190 -0
- pyfect/cli/completions/_descriptor.py +182 -0
- pyfect/cli/completions/bash.py +300 -0
- pyfect/cli/completions/fish.py +164 -0
- pyfect/cli/completions/powershell.py +156 -0
- pyfect/cli/completions/zsh.py +189 -0
- pyfect/cli/flag.py +246 -0
- pyfect/cli/global_flag.py +99 -0
- pyfect/cli/help_doc.py +63 -0
- pyfect/cli/param.py +789 -0
- pyfect/cli/primitive.py +308 -0
- pyfect/cli/prompt/__init__.py +75 -0
- pyfect/cli/prompt/_auto_complete.py +205 -0
- pyfect/cli/prompt/_choices.py +104 -0
- pyfect/cli/prompt/_confirm.py +195 -0
- pyfect/cli/prompt/_core.py +512 -0
- pyfect/cli/prompt/_date.py +173 -0
- pyfect/cli/prompt/_date_parts.py +387 -0
- pyfect/cli/prompt/_file.py +372 -0
- pyfect/cli/prompt/_multi_select.py +204 -0
- pyfect/cli/prompt/_number.py +257 -0
- pyfect/cli/prompt/_select.py +117 -0
- pyfect/cli/prompt/_text.py +239 -0
- pyfect/cli/terminal_capabilities.py +118 -0
- pyfect/clock.py +150 -0
- pyfect/cluster/__init__.py +155 -0
- pyfect/cluster/_cluster_abandon.py +36 -0
- pyfect/cluster/_cluster_layer.py +122 -0
- pyfect/cluster/_entity_activation.py +16 -0
- pyfect/cluster/_entity_manager/__init__.py +22 -0
- pyfect/cluster/_entity_manager/_activation.py +540 -0
- pyfect/cluster/_entity_manager/_core.py +186 -0
- pyfect/cluster/_entity_manager/_decode.py +60 -0
- pyfect/cluster/_entity_manager/_manager.py +423 -0
- pyfect/cluster/_entity_manager/_state.py +113 -0
- pyfect/cluster/_entity_reaper.py +94 -0
- pyfect/cluster/_hash.py +13 -0
- pyfect/cluster/_interruptors.py +88 -0
- pyfect/cluster/_resource_map.py +179 -0
- pyfect/cluster/_resource_ref.py +266 -0
- pyfect/cluster/_shard_lock.py +14 -0
- pyfect/cluster/cluster_cron.py +230 -0
- pyfect/cluster/cluster_error.py +81 -0
- pyfect/cluster/cluster_http.py +210 -0
- pyfect/cluster/cluster_metrics.py +54 -0
- pyfect/cluster/cluster_schema.py +137 -0
- pyfect/cluster/cluster_socket.py +197 -0
- pyfect/cluster/cluster_workflow_engine/__init__.py +24 -0
- pyfect/cluster/cluster_workflow_engine/_engine.py +1099 -0
- pyfect/cluster/cluster_workflow_engine/_entities.py +298 -0
- pyfect/cluster/deliver_at.py +31 -0
- pyfect/cluster/entity/__init__.py +39 -0
- pyfect/cluster/entity/_entity.py +332 -0
- pyfect/cluster/entity/_handlers.py +168 -0
- pyfect/cluster/entity/_keep_alive.py +74 -0
- pyfect/cluster/entity/_queue.py +103 -0
- pyfect/cluster/entity/_request.py +34 -0
- pyfect/cluster/entity/_services.py +23 -0
- pyfect/cluster/entity/_test_client.py +237 -0
- pyfect/cluster/entity_address.py +25 -0
- pyfect/cluster/entity_id.py +20 -0
- pyfect/cluster/entity_proxy.py +161 -0
- pyfect/cluster/entity_proxy_server.py +103 -0
- pyfect/cluster/entity_resource.py +122 -0
- pyfect/cluster/entity_type.py +20 -0
- pyfect/cluster/envelope.py +307 -0
- pyfect/cluster/http_runner.py +254 -0
- pyfect/cluster/k8s_http_client.py +281 -0
- pyfect/cluster/k8s_types.py +1394 -0
- pyfect/cluster/machine_id.py +21 -0
- pyfect/cluster/message.py +219 -0
- pyfect/cluster/message_storage/__init__.py +67 -0
- pyfect/cluster/message_storage/_encoded.py +393 -0
- pyfect/cluster/message_storage/_memory.py +386 -0
- pyfect/cluster/message_storage/_noop.py +109 -0
- pyfect/cluster/message_storage/_service.py +388 -0
- pyfect/cluster/reply.py +236 -0
- pyfect/cluster/runner.py +32 -0
- pyfect/cluster/runner_address.py +23 -0
- pyfect/cluster/runner_health.py +93 -0
- pyfect/cluster/runner_server.py +286 -0
- pyfect/cluster/runner_storage.py +212 -0
- pyfect/cluster/runners/__init__.py +55 -0
- pyfect/cluster/runners/_local.py +460 -0
- pyfect/cluster/runners/_noop.py +57 -0
- pyfect/cluster/runners/_rpc.py +290 -0
- pyfect/cluster/runners/_service.py +113 -0
- pyfect/cluster/shard_id.py +39 -0
- pyfect/cluster/sharding/__init__.py +12 -0
- pyfect/cluster/sharding/_acquisition.py +490 -0
- pyfect/cluster/sharding/_clients.py +271 -0
- pyfect/cluster/sharding/_make.py +388 -0
- pyfect/cluster/sharding/_routing.py +296 -0
- pyfect/cluster/sharding/_runners_sync.py +198 -0
- pyfect/cluster/sharding/_service.py +91 -0
- pyfect/cluster/sharding/_singletons.py +183 -0
- pyfect/cluster/sharding/_state.py +296 -0
- pyfect/cluster/sharding/_storage_loop.py +421 -0
- pyfect/cluster/sharding_config.py +230 -0
- pyfect/cluster/sharding_registration_event.py +28 -0
- pyfect/cluster/single_runner.py +49 -0
- pyfect/cluster/singleton.py +36 -0
- pyfect/cluster/singleton_address.py +20 -0
- pyfect/cluster/snowflake.py +133 -0
- pyfect/cluster/socket_runner.py +67 -0
- pyfect/cluster/sql_message_storage.py +1001 -0
- pyfect/cluster/sql_runner_storage.py +841 -0
- pyfect/cluster/test_runner.py +23 -0
- pyfect/config/__init__.py +93 -0
- pyfect/config/_all.py +96 -0
- pyfect/config/_collections.py +291 -0
- pyfect/config/_config.py +261 -0
- pyfect/config/_constructors.py +106 -0
- pyfect/config/_error.py +27 -0
- pyfect/config/_leaf.py +92 -0
- pyfect/config/_scalars.py +193 -0
- pyfect/config/_schema.py +340 -0
- pyfect/config_provider/__init__.py +60 -0
- pyfect/config_provider/_dir.py +84 -0
- pyfect/config_provider/_dotenv.py +127 -0
- pyfect/config_provider/_env.py +79 -0
- pyfect/config_provider/_layers.py +80 -0
- pyfect/config_provider/_node.py +90 -0
- pyfect/config_provider/_provider.py +118 -0
- pyfect/config_provider/_unknown.py +62 -0
- pyfect/console/__init__.py +63 -0
- pyfect/console/_effects.py +134 -0
- pyfect/console/_live.py +156 -0
- pyfect/console/_service.py +49 -0
- pyfect/console/_table.py +90 -0
- pyfect/context.py +414 -0
- pyfect/cron/__init__.py +30 -0
- pyfect/cron/_model.py +183 -0
- pyfect/cron/_search.py +268 -0
- pyfect/cron/_text.py +202 -0
- pyfect/cron/_wall.py +42 -0
- pyfect/crypto.py +257 -0
- pyfect/date_time/__init__.py +77 -0
- pyfect/date_time/_calendar.py +209 -0
- pyfect/date_time/_current.py +118 -0
- pyfect/date_time/_epoch.py +22 -0
- pyfect/date_time/_iso.py +80 -0
- pyfect/date_time/_zones.py +135 -0
- pyfect/date_time/intl.py +144 -0
- pyfect/deferred.py +145 -0
- pyfect/devtools/__init__.py +24 -0
- pyfect/devtools/client.py +323 -0
- pyfect/devtools/layers.py +32 -0
- pyfect/devtools/schema.py +301 -0
- pyfect/devtools/server.py +72 -0
- pyfect/duration.py +295 -0
- pyfect/effect.py +565 -0
- pyfect/effectable.py +37 -0
- pyfect/encoding/__init__.py +62 -0
- pyfect/encoding/_archive.py +197 -0
- pyfect/encoding/_json_array_scanner.py +477 -0
- pyfect/encoding/base64.py +23 -0
- pyfect/encoding/base64_url.py +19 -0
- pyfect/encoding/encoding_error.py +17 -0
- pyfect/encoding/hex.py +28 -0
- pyfect/encoding/ini.py +188 -0
- pyfect/encoding/json_array.py +246 -0
- pyfect/encoding/ndjson.py +461 -0
- pyfect/encoding/schema_binary/__init__.py +257 -0
- pyfect/encoding/schema_binary/_channels.py +173 -0
- pyfect/encoding/schema_binary/_compile.py +584 -0
- pyfect/encoding/schema_binary/_decode.py +848 -0
- pyfect/encoding/schema_binary/_dictionary.py +90 -0
- pyfect/encoding/schema_binary/_encode.py +838 -0
- pyfect/encoding/schema_binary/_fingerprint.py +168 -0
- pyfect/encoding/schema_binary/_layout.py +350 -0
- pyfect/encoding/schema_binary/_literals.py +47 -0
- pyfect/encoding/schema_binary/_parser.py +163 -0
- pyfect/encoding/schema_binary/_reader.py +148 -0
- pyfect/encoding/schema_binary/_target.py +310 -0
- pyfect/encoding/schema_binary/_values.py +133 -0
- pyfect/encoding/schema_binary/_wire.py +253 -0
- pyfect/encoding/sse.py +476 -0
- pyfect/encoding/tar/__init__.py +396 -0
- pyfect/encoding/tar/_decoder.py +220 -0
- pyfect/encoding/tar/_encoder.py +83 -0
- pyfect/encoding/tar/_header.py +494 -0
- pyfect/encoding/tar/_model.py +82 -0
- pyfect/encoding/toml.py +375 -0
- pyfect/encoding/xml/__init__.py +222 -0
- pyfect/encoding/xml/_chars.py +119 -0
- pyfect/encoding/xml/_collector.py +106 -0
- pyfect/encoding/xml/_inputs.py +126 -0
- pyfect/encoding/xml/_model.py +114 -0
- pyfect/encoding/xml/_parser.py +694 -0
- pyfect/encoding/xml/_scanner.py +180 -0
- pyfect/encoding/xml/_writer.py +238 -0
- pyfect/encoding/yaml.py +739 -0
- pyfect/encoding/zip/__init__.py +392 -0
- pyfect/encoding/zip/_decoder.py +378 -0
- pyfect/encoding/zip/_format.py +122 -0
- pyfect/encoding/zip/_inflate.py +65 -0
- pyfect/encoding/zip/_model.py +86 -0
- pyfect/encoding/zip/_writer.py +311 -0
- pyfect/error_reporter.py +240 -0
- pyfect/errors.py +321 -0
- pyfect/eventlog/__init__.py +77 -0
- pyfect/eventlog/_conflicts.py +94 -0
- pyfect/eventlog/_crypto.py +64 -0
- pyfect/eventlog/_identity.py +26 -0
- pyfect/eventlog/_identity_derivation.py +79 -0
- pyfect/eventlog/_transferable_bytes.py +55 -0
- pyfect/eventlog/_uuid.py +33 -0
- pyfect/eventlog/event.py +115 -0
- pyfect/eventlog/event_group.py +55 -0
- pyfect/eventlog/event_journal.py +630 -0
- pyfect/eventlog/event_log.py +1039 -0
- pyfect/eventlog/event_log_encryption.py +219 -0
- pyfect/eventlog/event_log_message.py +357 -0
- pyfect/eventlog/event_log_remote.py +465 -0
- pyfect/eventlog/event_log_server.py +442 -0
- pyfect/eventlog/event_log_server_encrypted.py +322 -0
- pyfect/eventlog/event_log_server_unencrypted.py +814 -0
- pyfect/eventlog/event_log_session_auth.py +250 -0
- pyfect/eventlog/event_row.py +423 -0
- pyfect/eventlog/sql_event_journal.py +692 -0
- pyfect/eventlog/sql_event_log_server_encrypted.py +440 -0
- pyfect/eventlog/sql_event_log_server_unencrypted.py +568 -0
- pyfect/execution_plan/__init__.py +42 -0
- pyfect/execution_plan/_events.py +128 -0
- pyfect/execution_plan/_model.py +108 -0
- pyfect/execution_plan/_run.py +176 -0
- pyfect/exit.py +205 -0
- pyfect/fiber_handle.py +305 -0
- pyfect/fiber_map.py +350 -0
- pyfect/fiber_set.py +232 -0
- pyfect/file_system/__init__.py +69 -0
- pyfect/file_system/_copy.py +116 -0
- pyfect/file_system/_errors.py +32 -0
- pyfect/file_system/_file.py +177 -0
- pyfect/file_system/_live.py +351 -0
- pyfect/file_system/_model.py +72 -0
- pyfect/file_system/_noop.py +189 -0
- pyfect/file_system/_service.py +336 -0
- pyfect/file_system/_stat.py +69 -0
- pyfect/file_system/_watch.py +107 -0
- pyfect/filters.py +245 -0
- pyfect/graph/__init__.py +101 -0
- pyfect/graph/_all_pairs.py +150 -0
- pyfect/graph/_combine.py +259 -0
- pyfect/graph/_compact.py +143 -0
- pyfect/graph/_components.py +200 -0
- pyfect/graph/_cycles.py +144 -0
- pyfect/graph/_enumerate.py +168 -0
- pyfect/graph/_errors.py +51 -0
- pyfect/graph/_flow.py +164 -0
- pyfect/graph/_forest.py +109 -0
- pyfect/graph/_graph.py +269 -0
- pyfect/graph/_lowlink.py +98 -0
- pyfect/graph/_matching.py +101 -0
- pyfect/graph/_mutate.py +211 -0
- pyfect/graph/_query.py +138 -0
- pyfect/graph/_records.py +119 -0
- pyfect/graph/_render.py +109 -0
- pyfect/graph/_shortest.py +274 -0
- pyfect/graph/_snapshot.py +50 -0
- pyfect/graph/_store.py +136 -0
- pyfect/graph/_traverse.py +289 -0
- pyfect/graph/_view.py +384 -0
- pyfect/graph/_walker.py +70 -0
- pyfect/graphql/__init__.py +87 -0
- pyfect/graphql/_codec.py +117 -0
- pyfect/graphql/_http.py +309 -0
- pyfect/graphql/_schemas.py +39 -0
- pyfect/graphql/_slices.py +110 -0
- pyfect/graphql/_template.py +111 -0
- pyfect/graphql/_types.py +115 -0
- pyfect/graphql/_ws.py +395 -0
- pyfect/graphql/_ws_messages.py +120 -0
- pyfect/graphql/client.py +428 -0
- pyfect/graphql/client_error.py +285 -0
- pyfect/graphql/group.py +142 -0
- pyfect/graphql/middleware.py +98 -0
- pyfect/graphql/operation.py +409 -0
- pyfect/graphql/protocol.py +119 -0
- pyfect/graphql/resolver.py +574 -0
- pyfect/graphql/template.py +61 -0
- pyfect/hash_map.py +317 -0
- pyfect/hash_ring.py +206 -0
- pyfect/hash_set.py +163 -0
- pyfect/http/__init__.py +125 -0
- pyfect/http/_asgi_listener.py +272 -0
- pyfect/http/_authority.py +63 -0
- pyfect/http/_brotli.py +27 -0
- pyfect/http/_compression.py +52 -0
- pyfect/http/_field.py +18 -0
- pyfect/http/_foreign_path.py +27 -0
- pyfect/http/_form_data.py +34 -0
- pyfect/http/_httpx_options.py +120 -0
- pyfect/http/_in_memory.py +76 -0
- pyfect/http/_request_state.py +59 -0
- pyfect/http/_vary.py +14 -0
- pyfect/http/abort.py +65 -0
- pyfect/http/asgi/__init__.py +59 -0
- pyfect/http/asgi/_app.py +155 -0
- pyfect/http/asgi/_http.py +201 -0
- pyfect/http/asgi/_mount.py +263 -0
- pyfect/http/asgi/_scope.py +86 -0
- pyfect/http/asgi/_serve.py +72 -0
- pyfect/http/asgi/_types.py +13 -0
- pyfect/http/asgi/_web_socket.py +325 -0
- pyfect/http/body.py +437 -0
- pyfect/http/client/__init__.py +111 -0
- pyfect/http/client/_circuit_breaker.py +113 -0
- pyfect/http/client/_cookie_jar.py +120 -0
- pyfect/http/client/_make.py +252 -0
- pyfect/http/client/_rate_limit.py +423 -0
- pyfect/http/client/_redirects.py +93 -0
- pyfect/http/client/_retry.py +115 -0
- pyfect/http/client/_service.py +497 -0
- pyfect/http/cookies.py +444 -0
- pyfect/http/error.py +188 -0
- pyfect/http/etag.py +96 -0
- pyfect/http/file_like.py +49 -0
- pyfect/http/find_my_way/__init__.py +10 -0
- pyfect/http/find_my_way/_nodes.py +186 -0
- pyfect/http/find_my_way/_paths.py +43 -0
- pyfect/http/find_my_way/_query_string.py +67 -0
- pyfect/http/find_my_way/_router.py +401 -0
- pyfect/http/headers.py +343 -0
- pyfect/http/http_effect.py +198 -0
- pyfect/http/httpx_client.py +291 -0
- pyfect/http/hypercorn_server.py +122 -0
- pyfect/http/incoming_message.py +140 -0
- pyfect/http/live_server/__init__.py +191 -0
- pyfect/http/live_server/_body.py +164 -0
- pyfect/http/live_server/_exchange.py +321 -0
- pyfect/http/live_server/_head.py +194 -0
- pyfect/http/live_server/_inbound.py +156 -0
- pyfect/http/live_server/_listener.py +211 -0
- pyfect/http/live_server/_serve.py +225 -0
- pyfect/http/live_server/_timers.py +20 -0
- pyfect/http/live_server/_upgrade.py +108 -0
- pyfect/http/live_server/_websocket.py +286 -0
- pyfect/http/live_server/_ws_frames.py +242 -0
- pyfect/http/method.py +30 -0
- pyfect/http/middleware/__init__.py +61 -0
- pyfect/http/middleware/_compression.py +156 -0
- pyfect/http/middleware/_cors.py +129 -0
- pyfect/http/middleware/_decompression.py +274 -0
- pyfect/http/middleware/_forwarded.py +113 -0
- pyfect/http/middleware/_logger.py +73 -0
- pyfect/http/middleware/_search_params.py +34 -0
- pyfect/http/middleware/_tracer.py +153 -0
- pyfect/http/middleware/_types.py +16 -0
- pyfect/http/mime/__init__.py +69 -0
- pyfect/http/mime/_types.py +361 -0
- pyfect/http/multipart.py +613 -0
- pyfect/http/multipart_parser/__init__.py +348 -0
- pyfect/http/multipart_parser/_content_type.py +109 -0
- pyfect/http/multipart_parser/_headers.py +206 -0
- pyfect/http/multipart_parser/_search.py +85 -0
- pyfect/http/origin.py +146 -0
- pyfect/http/platform.py +593 -0
- pyfect/http/prometheus.py +34 -0
- pyfect/http/request.py +384 -0
- pyfect/http/response.py +474 -0
- pyfect/http/router/__init__.py +82 -0
- pyfect/http/router/_api.py +462 -0
- pyfect/http/router/_in_memory.py +76 -0
- pyfect/http/router/_markers.py +47 -0
- pyfect/http/router/_middleware.py +181 -0
- pyfect/http/router/_middleware_stack.py +51 -0
- pyfect/http/router/_provided.py +31 -0
- pyfect/http/router/_route.py +114 -0
- pyfect/http/router/_router.py +308 -0
- pyfect/http/schema.py +276 -0
- pyfect/http/server.py +199 -0
- pyfect/http/server_error.py +261 -0
- pyfect/http/server_request.py +729 -0
- pyfect/http/server_respondable.py +77 -0
- pyfect/http/server_response.py +569 -0
- pyfect/http/static_server/__init__.py +257 -0
- pyfect/http/static_server/_conditional.py +72 -0
- pyfect/http/static_server/_mime.py +55 -0
- pyfect/http/static_server/_range.py +53 -0
- pyfect/http/status.py +51 -0
- pyfect/http/template.py +161 -0
- pyfect/http/trace_context.py +91 -0
- pyfect/http/url.py +442 -0
- pyfect/http/url_params.py +304 -0
- pyfect/http/uvicorn_server.py +119 -0
- pyfect/http/web.py +171 -0
- pyfect/http/web_handler.py +463 -0
- pyfect/http/wire.py +209 -0
- pyfect/http_api/__init__.py +83 -0
- pyfect/http_api/_assets/scalar.js +8 -0
- pyfect/http_api/_assets/swagger-ui.css +3 -0
- pyfect/http_api/_assets/swagger-ui.js +4 -0
- pyfect/http_api/_client_method.py +220 -0
- pyfect/http_api/_client_payload.py +144 -0
- pyfect/http_api/_client_response.py +377 -0
- pyfect/http_api/_handlers.py +282 -0
- pyfect/http_api/_html.py +38 -0
- pyfect/http_api/_media_type.py +6 -0
- pyfect/http_api/_openapi_responses.py +164 -0
- pyfect/http_api/_openapi_schemas.py +163 -0
- pyfect/http_api/_path.py +102 -0
- pyfect/http_api/_payload.py +260 -0
- pyfect/http_api/_response.py +363 -0
- pyfect/http_api/_security.py +174 -0
- pyfect/http_api/api.py +265 -0
- pyfect/http_api/builder.py +447 -0
- pyfect/http_api/client.py +474 -0
- pyfect/http_api/endpoint.py +969 -0
- pyfect/http_api/error.py +280 -0
- pyfect/http_api/group.py +215 -0
- pyfect/http_api/middleware.py +232 -0
- pyfect/http_api/openapi.py +396 -0
- pyfect/http_api/scalar.py +143 -0
- pyfect/http_api/schema.py +713 -0
- pyfect/http_api/security.py +91 -0
- pyfect/http_api/swagger.py +69 -0
- pyfect/http_api/test.py +105 -0
- pyfect/json_patch.py +255 -0
- pyfect/json_pointer.py +100 -0
- pyfect/json_schema/__init__.py +66 -0
- pyfect/json_schema/_api.py +96 -0
- pyfect/json_schema/_converter.py +258 -0
- pyfect/json_schema/_draft04.py +198 -0
- pyfect/json_schema/_draft07.py +203 -0
- pyfect/json_schema/_keywords.py +202 -0
- pyfect/json_schema/_model.py +201 -0
- pyfect/json_schema/_openapi.py +208 -0
- pyfect/latch.py +127 -0
- pyfect/layer.py +479 -0
- pyfect/layer_map.py +279 -0
- pyfect/layer_ref.py +208 -0
- pyfect/logger/__init__.py +91 -0
- pyfect/logger/_batched.py +49 -0
- pyfect/logger/_console.py +116 -0
- pyfect/logger/_file.py +31 -0
- pyfect/logger/_formats.py +104 -0
- pyfect/logger/_layer.py +50 -0
- pyfect/logger/_level.py +53 -0
- pyfect/logger/_log.py +142 -0
- pyfect/logger/_model.py +163 -0
- pyfect/logger/_refs.py +78 -0
- pyfect/logger/_stdlib.py +54 -0
- pyfect/logger/_text.py +263 -0
- pyfect/logger/_tracer.py +35 -0
- pyfect/managed_runtime.py +393 -0
- pyfect/metric/__init__.py +119 -0
- pyfect/metric/_boundaries.py +27 -0
- pyfect/metric/_kinds.py +417 -0
- pyfect/metric/_metric.py +302 -0
- pyfect/metric/_prometheus.py +150 -0
- pyfect/metric/_refs.py +78 -0
- pyfect/metric/_runtime.py +75 -0
- pyfect/metric/_snapshot.py +97 -0
- pyfect/metric/_state.py +74 -0
- pyfect/msgspec.py +98 -0
- pyfect/mutable_hash_map.py +140 -0
- pyfect/mutable_hash_set.py +95 -0
- pyfect/net/__init__.py +18 -0
- pyfect/net/_errors.py +29 -0
- pyfect/net/address_resolver.py +179 -0
- pyfect/net/dns/__init__.py +92 -0
- pyfect/net/dns/_error.py +43 -0
- pyfect/net/dns/_records.py +335 -0
- pyfect/net/dns/_service.py +287 -0
- pyfect/net/dns/_system.py +89 -0
- pyfect/net/host.py +311 -0
- pyfect/net/ip_interface.py +202 -0
- pyfect/net/ip_network.py +250 -0
- pyfect/net/net_address/__init__.py +245 -0
- pyfect/net/net_address/_inet.py +412 -0
- pyfect/net/net_address/_ip.py +348 -0
- pyfect/net/net_address/_mac.py +119 -0
- pyfect/optic/__init__.py +50 -0
- pyfect/optic/_builtins.py +90 -0
- pyfect/optic/_data.py +97 -0
- pyfect/optic/_optic.py +422 -0
- pyfect/optic/_steps.py +151 -0
- pyfect/option.py +362 -0
- pyfect/otel/__init__.py +22 -0
- pyfect/otel/_attributes.py +67 -0
- pyfect/otel/_producer.py +322 -0
- pyfect/otel/_shutdown.py +26 -0
- pyfect/otel/_span.py +292 -0
- pyfect/otel/_wrapper.py +92 -0
- pyfect/otel/logger.py +154 -0
- pyfect/otel/metrics.py +126 -0
- pyfect/otel/resource.py +111 -0
- pyfect/otel/sdk.py +195 -0
- pyfect/otel/tracer.py +157 -0
- pyfect/partitioned_semaphore.py +211 -0
- pyfect/path.py +304 -0
- pyfect/persistence/__init__.py +77 -0
- pyfect/persistence/_columns.py +32 -0
- pyfect/persistence/_error.py +14 -0
- pyfect/persistence/_seconds.py +68 -0
- pyfect/persistence/_sql_keys.py +47 -0
- pyfect/persistence/circuit_breaker/__init__.py +120 -0
- pyfect/persistence/circuit_breaker/_breaker.py +317 -0
- pyfect/persistence/circuit_breaker/_errors.py +67 -0
- pyfect/persistence/circuit_breaker/_memory.py +243 -0
- pyfect/persistence/circuit_breaker/_redis.py +509 -0
- pyfect/persistence/circuit_breaker/_store.py +60 -0
- pyfect/persistence/circuit_breaker/_types.py +191 -0
- pyfect/persistence/key_value_store/__init__.py +51 -0
- pyfect/persistence/key_value_store/_layers.py +216 -0
- pyfect/persistence/key_value_store/_schema_store.py +91 -0
- pyfect/persistence/key_value_store/_sql.py +197 -0
- pyfect/persistence/key_value_store/_store.py +212 -0
- pyfect/persistence/persistable.py +164 -0
- pyfect/persistence/persisted_cache.py +159 -0
- pyfect/persistence/persisted_queue/__init__.py +75 -0
- pyfect/persistence/persisted_queue/_error.py +42 -0
- pyfect/persistence/persisted_queue/_mailbox.py +101 -0
- pyfect/persistence/persisted_queue/_memory.py +182 -0
- pyfect/persistence/persisted_queue/_queue.py +272 -0
- pyfect/persistence/persisted_queue/_redis.py +455 -0
- pyfect/persistence/persisted_queue/_redis_scripts.py +395 -0
- pyfect/persistence/persisted_queue/_sql.py +334 -0
- pyfect/persistence/persisted_queue/_sql_migrations.py +319 -0
- pyfect/persistence/persisted_queue/_sql_statements.py +441 -0
- pyfect/persistence/persisted_queue/_store.py +147 -0
- pyfect/persistence/persistence/__init__.py +84 -0
- pyfect/persistence/persistence/_kvs.py +135 -0
- pyfect/persistence/persistence/_memory.py +62 -0
- pyfect/persistence/persistence/_redis.py +162 -0
- pyfect/persistence/persistence/_service.py +276 -0
- pyfect/persistence/persistence/_sql.py +499 -0
- pyfect/persistence/rate_limiter/__init__.py +82 -0
- pyfect/persistence/rate_limiter/_errors.py +59 -0
- pyfect/persistence/rate_limiter/_limiter.py +325 -0
- pyfect/persistence/rate_limiter/_memory.py +217 -0
- pyfect/persistence/rate_limiter/_redis.py +485 -0
- pyfect/persistence/rate_limiter/_store.py +89 -0
- pyfect/persistence/redis/__init__.py +52 -0
- pyfect/persistence/redis/_error.py +12 -0
- pyfect/persistence/redis/_make.py +234 -0
- pyfect/persistence/redis/_script.py +78 -0
- pyfect/persistence/redis/_service.py +104 -0
- pyfect/persistence/redis_py.py +303 -0
- pyfect/persistence/user_directories.py +195 -0
- pyfect/platform.py +26 -0
- pyfect/platform_error.py +256 -0
- pyfect/pool/__init__.py +36 -0
- pyfect/pool/_pool.py +707 -0
- pyfect/pool/_state.py +161 -0
- pyfect/pool/_ttl.py +105 -0
- pyfect/pool/_waiters.py +116 -0
- pyfect/primary_key.py +35 -0
- pyfect/process/__init__.py +82 -0
- pyfect/process/_child.py +213 -0
- pyfect/process/_command.py +358 -0
- pyfect/process/_handle.py +77 -0
- pyfect/process/_live.py +455 -0
- pyfect/process/_os.py +78 -0
- pyfect/process/_pipes.py +291 -0
- pyfect/process/_spawner.py +75 -0
- pyfect/pubsub/__init__.py +33 -0
- pyfect/pubsub/_atomic.py +181 -0
- pyfect/pubsub/_hub.py +214 -0
- pyfect/pubsub/_pubsub.py +214 -0
- pyfect/pubsub/_replay.py +107 -0
- pyfect/pubsub/_subscription.py +226 -0
- pyfect/pull.py +132 -0
- pyfect/py.typed +0 -0
- pyfect/pydantic.py +188 -0
- pyfect/queue/__init__.py +33 -0
- pyfect/queue/_queue.py +525 -0
- pyfect/queue/_state.py +111 -0
- pyfect/queue/_views.py +55 -0
- pyfect/random.py +146 -0
- pyfect/rc_map.py +363 -0
- pyfect/rc_ref.py +271 -0
- pyfect/reactivity/__init__.py +53 -0
- pyfect/reactivity/_async_result_schema.py +227 -0
- pyfect/reactivity/_hydration.py +18 -0
- pyfect/reactivity/_live.py +21 -0
- pyfect/reactivity/_reactivity.py +256 -0
- pyfect/reactivity/_result_effect.py +17 -0
- pyfect/reactivity/_time.py +11 -0
- pyfect/reactivity/async_result.py +679 -0
- pyfect/reactivity/atom/__init__.py +143 -0
- pyfect/reactivity/atom/_combinators.py +223 -0
- pyfect/reactivity/atom/_constructors.py +80 -0
- pyfect/reactivity/atom/_conversions.py +79 -0
- pyfect/reactivity/atom/_effect.py +252 -0
- pyfect/reactivity/atom/_fn.py +341 -0
- pyfect/reactivity/atom/_kvs.py +101 -0
- pyfect/reactivity/atom/_optimistic.py +162 -0
- pyfect/reactivity/atom/_runtime.py +323 -0
- pyfect/reactivity/atom/_serializable.py +65 -0
- pyfect/reactivity/atom/_subscription_ref.py +102 -0
- pyfect/reactivity/atom/_swr.py +69 -0
- pyfect/reactivity/atom/_types.py +435 -0
- pyfect/reactivity/atom_http_api.py +667 -0
- pyfect/reactivity/atom_ref.py +260 -0
- pyfect/reactivity/atom_registry/__init__.py +219 -0
- pyfect/reactivity/atom_registry/_batch.py +96 -0
- pyfect/reactivity/atom_registry/_dispatcher.py +60 -0
- pyfect/reactivity/atom_registry/_lifetime.py +202 -0
- pyfect/reactivity/atom_registry/_node.py +423 -0
- pyfect/reactivity/atom_registry/_registry.py +380 -0
- pyfect/reactivity/atom_rpc.py +283 -0
- pyfect/reactivity/hydration.py +200 -0
- pyfect/redacted.py +83 -0
- pyfect/ref.py +129 -0
- pyfect/resource.py +98 -0
- pyfect/result.py +306 -0
- pyfect/rpc/__init__.py +64 -0
- pyfect/rpc/_encoded_schema.py +86 -0
- pyfect/rpc/_json_rpc.py +246 -0
- pyfect/rpc/_methods.py +177 -0
- pyfect/rpc/_rpc.py +498 -0
- pyfect/rpc/client/__init__.py +100 -0
- pyfect/rpc/client/_core.py +449 -0
- pyfect/rpc/client/_http.py +257 -0
- pyfect/rpc/client/_protocol.py +171 -0
- pyfect/rpc/client/_serialized.py +321 -0
- pyfect/rpc/client/_transports.py +328 -0
- pyfect/rpc/client/_worker.py +283 -0
- pyfect/rpc/client_error.py +35 -0
- pyfect/rpc/group.py +296 -0
- pyfect/rpc/message.py +299 -0
- pyfect/rpc/middleware.py +182 -0
- pyfect/rpc/schema.py +89 -0
- pyfect/rpc/serialization.py +366 -0
- pyfect/rpc/server/__init__.py +181 -0
- pyfect/rpc/server/_core.py +549 -0
- pyfect/rpc/server/_protocol.py +123 -0
- pyfect/rpc/server/_serialized.py +363 -0
- pyfect/rpc/server/_stdio.py +101 -0
- pyfect/rpc/server/_transports.py +427 -0
- pyfect/rpc/server/_worker.py +108 -0
- pyfect/rpc/test.py +74 -0
- pyfect/rpc/worker.py +92 -0
- pyfect/run.py +393 -0
- pyfect/schedule/__init__.py +91 -0
- pyfect/schedule/_combinators.py +168 -0
- pyfect/schedule/_constructors.py +223 -0
- pyfect/schedule/_cron.py +49 -0
- pyfect/schedule/_drive.py +191 -0
- pyfect/schedule/_model.py +405 -0
- pyfect/scheduler.py +575 -0
- pyfect/schema/__init__.py +866 -0
- pyfect/schema/_adapters.py +39 -0
- pyfect/schema/_arbitrary_constraint.py +242 -0
- pyfect/schema/_arbitrary_graph.py +325 -0
- pyfect/schema/_arbitrary_links.py +179 -0
- pyfect/schema/_arbitrary_number.py +112 -0
- pyfect/schema/_arrays.py +287 -0
- pyfect/schema/_assignable.py +118 -0
- pyfect/schema/_boolean_literals.py +31 -0
- pyfect/schema/_checks_number.py +216 -0
- pyfect/schema/_checks_order.py +140 -0
- pyfect/schema/_checks_size.py +277 -0
- pyfect/schema/_checks_string.py +241 -0
- pyfect/schema/_class.py +569 -0
- pyfect/schema/_codec_derivation.py +227 -0
- pyfect/schema/_codec_links.py +221 -0
- pyfect/schema/_codecs.py +316 -0
- pyfect/schema/_collections.py +172 -0
- pyfect/schema/_constructor_default.py +89 -0
- pyfect/schema/_data.py +440 -0
- pyfect/schema/_data_codecs.py +145 -0
- pyfect/schema/_decimal_regexp.py +243 -0
- pyfect/schema/_declare.py +68 -0
- pyfect/schema/_derive.py +292 -0
- pyfect/schema/_duration_codecs.py +27 -0
- pyfect/schema/_ecma_pattern.py +416 -0
- pyfect/schema/_error.py +26 -0
- pyfect/schema/_error_instance.py +222 -0
- pyfect/schema/_graph.py +271 -0
- pyfect/schema/_instances.py +240 -0
- pyfect/schema/_jsonschema/__init__.py +90 -0
- pyfect/schema/_jsonschema/_annotations.py +114 -0
- pyfect/schema/_jsonschema/_checks.py +253 -0
- pyfect/schema/_jsonschema/_compile.py +346 -0
- pyfect/schema/_jsonschema/_exact.py +19 -0
- pyfect/schema/_jsonschema/_merge.py +211 -0
- pyfect/schema/_jsonschema/_objects.py +231 -0
- pyfect/schema/_jsonschema/_references.py +221 -0
- pyfect/schema/_jsonschema/_template.py +69 -0
- pyfect/schema/_literals.py +147 -0
- pyfect/schema/_middleware.py +209 -0
- pyfect/schema/_net.py +522 -0
- pyfect/schema/_options.py +49 -0
- pyfect/schema/_parse.py +351 -0
- pyfect/schema/_parsed_text.py +37 -0
- pyfect/schema/_primitives.py +48 -0
- pyfect/schema/_record.py +41 -0
- pyfect/schema/_refine.py +81 -0
- pyfect/schema/_schema.py +158 -0
- pyfect/schema/_semver.py +144 -0
- pyfect/schema/_strategy/__init__.py +258 -0
- pyfect/schema/_strategy/_big_decimal.py +203 -0
- pyfect/schema/_strategy/_composites.py +119 -0
- pyfect/schema/_strategy/_leaves.py +94 -0
- pyfect/schema/_strategy/_net.py +133 -0
- pyfect/schema/_struct.py +87 -0
- pyfect/schema/_struct_keys.py +128 -0
- pyfect/schema/_tagged.py +254 -0
- pyfect/schema/_template_literal.py +172 -0
- pyfect/schema/_to_codec_array_from_single.py +115 -0
- pyfect/schema/_to_codec_go_json.py +174 -0
- pyfect/schema/_to_codec_json.py +73 -0
- pyfect/schema/_to_codec_string_tree.py +115 -0
- pyfect/schema/_to_differ_json_patch.py +44 -0
- pyfect/schema/_to_encoder_xml.py +119 -0
- pyfect/schema/_to_equivalence.py +186 -0
- pyfect/schema/_to_formatter.py +180 -0
- pyfect/schema/_to_iso.py +73 -0
- pyfect/schema/_to_strategy.py +31 -0
- pyfect/schema/_transform.py +210 -0
- pyfect/schema/_tree.py +27 -0
- pyfect/schema/_type_walk.py +53 -0
- pyfect/schema/_undefined.py +58 -0
- pyfect/schema/_value_codecs.py +84 -0
- pyfect/schema/_values.py +123 -0
- pyfect/schema/_void.py +86 -0
- pyfect/schema/ast/__init__.py +227 -0
- pyfect/schema/ast/_annotations.py +43 -0
- pyfect/schema/ast/_arrays.py +210 -0
- pyfect/schema/ast/_base.py +295 -0
- pyfect/schema/ast/_candidates.py +309 -0
- pyfect/schema/ast/_check.py +181 -0
- pyfect/schema/ast/_class.py +159 -0
- pyfect/schema/ast/_constructors.py +65 -0
- pyfect/schema/ast/_declaration.py +97 -0
- pyfect/schema/ast/_encoding.py +111 -0
- pyfect/schema/ast/_json.py +129 -0
- pyfect/schema/ast/_keywords.py +125 -0
- pyfect/schema/ast/_literal.py +157 -0
- pyfect/schema/ast/_objects.py +356 -0
- pyfect/schema/ast/_record.py +36 -0
- pyfect/schema/ast/_result.py +116 -0
- pyfect/schema/ast/_steps.py +209 -0
- pyfect/schema/ast/_suspend.py +65 -0
- pyfect/schema/ast/_template_literal.py +256 -0
- pyfect/schema/ast/_union.py +172 -0
- pyfect/schema/getter/__init__.py +136 -0
- pyfect/schema/getter/_binary.py +104 -0
- pyfect/schema/getter/_coerce.py +196 -0
- pyfect/schema/getter/_getter.py +243 -0
- pyfect/schema/getter/_make.py +151 -0
- pyfect/schema/getter/_text.py +141 -0
- pyfect/schema/getter/_tree_record.py +170 -0
- pyfect/schema/issue.py +420 -0
- pyfect/schema/model/__init__.py +120 -0
- pyfect/schema/model/_class.py +245 -0
- pyfect/schema/model/_dates.py +103 -0
- pyfect/schema/model/_fields.py +77 -0
- pyfect/schema/model/_toolkit.py +114 -0
- pyfect/schema/model/_uuid.py +69 -0
- pyfect/schema/representation/__init__.py +424 -0
- pyfect/schema/representation/_builtin_revivers.py +331 -0
- pyfect/schema/representation/_code.py +106 -0
- pyfect/schema/representation/_code_builtins.py +281 -0
- pyfect/schema/representation/_code_document.py +563 -0
- pyfect/schema/representation/_code_sort.py +111 -0
- pyfect/schema/representation/_from.py +278 -0
- pyfect/schema/representation/_import/__init__.py +78 -0
- pyfect/schema/representation/_import/_finalize.py +121 -0
- pyfect/schema/representation/_import/_intersect.py +467 -0
- pyfect/schema/representation/_import/_js_order.py +66 -0
- pyfect/schema/representation/_import/_keywords.py +224 -0
- pyfect/schema/representation/_import/_nodes.py +199 -0
- pyfect/schema/representation/_import/_translate.py +455 -0
- pyfect/schema/representation/_json.py +520 -0
- pyfect/schema/representation/_json_schema.py +178 -0
- pyfect/schema/representation/_model.py +284 -0
- pyfect/schema/representation/_revivers.py +89 -0
- pyfect/schema/representation/_to.py +249 -0
- pyfect/schema/transformation/__init__.py +99 -0
- pyfect/schema/transformation/_builtins.py +314 -0
- pyfect/schema/transformation/_durations.py +150 -0
- pyfect/schema/transformation/_transformation.py +177 -0
- pyfect/schema/variant_schema/__init__.py +69 -0
- pyfect/schema/variant_schema/_class.py +188 -0
- pyfect/schema/variant_schema/_defaults.py +30 -0
- pyfect/schema/variant_schema/_field.py +32 -0
- pyfect/schema/variant_schema/_struct.py +130 -0
- pyfect/schema/variant_schema/_toolkit.py +160 -0
- pyfect/scope.py +374 -0
- pyfect/scoped_ref.py +124 -0
- pyfect/semaphore.py +276 -0
- pyfect/semver.py +403 -0
- pyfect/semver_range.py +420 -0
- pyfect/sink/__init__.py +131 -0
- pyfect/sink/_combinators.py +151 -0
- pyfect/sink/_constructors.py +187 -0
- pyfect/sink/_context.py +87 -0
- pyfect/sink/_folds.py +336 -0
- pyfect/sink/_for_each.py +87 -0
- pyfect/sink/_sink.py +265 -0
- pyfect/sink/_steps.py +34 -0
- pyfect/sink/_taking.py +183 -0
- pyfect/socket/__init__.py +146 -0
- pyfect/socket/_accepted.py +65 -0
- pyfect/socket/_connection.py +384 -0
- pyfect/socket/_errors.py +135 -0
- pyfect/socket/_loop.py +66 -0
- pyfect/socket/_net.py +281 -0
- pyfect/socket/_socket.py +245 -0
- pyfect/socket/_tls.py +88 -0
- pyfect/socket/_tls_session.py +117 -0
- pyfect/socket/_web_socket.py +555 -0
- pyfect/socket/datagram/__init__.py +110 -0
- pyfect/socket/datagram/_core.py +460 -0
- pyfect/socket/datagram/_errors.py +102 -0
- pyfect/socket/datagram/_live.py +675 -0
- pyfect/socket/datagram/_model.py +217 -0
- pyfect/socket/server.py +258 -0
- pyfect/socket/ws.py +484 -0
- pyfect/sql/__init__.py +60 -0
- pyfect/sql/_loop.py +134 -0
- pyfect/sql/clickhouse/__init__.py +74 -0
- pyfect/sql/clickhouse/_connection.py +237 -0
- pyfect/sql/clickhouse/_rows.py +87 -0
- pyfect/sql/clickhouse/client.py +322 -0
- pyfect/sql/clickhouse/config.py +241 -0
- pyfect/sql/clickhouse/driver.py +434 -0
- pyfect/sql/clickhouse/error.py +106 -0
- pyfect/sql/clickhouse/migrator.py +47 -0
- pyfect/sql/clickhouse/params.py +168 -0
- pyfect/sql/client/__init__.py +35 -0
- pyfect/sql/client/_client.py +309 -0
- pyfect/sql/client/_sqlite_acquirers.py +130 -0
- pyfect/sql/client/_transaction.py +410 -0
- pyfect/sql/connection.py +87 -0
- pyfect/sql/d1/__init__.py +48 -0
- pyfect/sql/d1/api.py +270 -0
- pyfect/sql/d1/client.py +344 -0
- pyfect/sql/d1/config.py +55 -0
- pyfect/sql/error.py +311 -0
- pyfect/sql/libsql/__init__.py +58 -0
- pyfect/sql/libsql/_connection.py +110 -0
- pyfect/sql/libsql/_hrana.py +258 -0
- pyfect/sql/libsql/_local.py +251 -0
- pyfect/sql/libsql/_targets.py +28 -0
- pyfect/sql/libsql/_values.py +46 -0
- pyfect/sql/libsql/client.py +225 -0
- pyfect/sql/libsql/config.py +170 -0
- pyfect/sql/libsql/migrator.py +43 -0
- pyfect/sql/libsql/sdk.py +123 -0
- pyfect/sql/migrator.py +434 -0
- pyfect/sql/model.py +324 -0
- pyfect/sql/mssql/__init__.py +79 -0
- pyfect/sql/mssql/_connection.py +169 -0
- pyfect/sql/mssql/_driver.py +103 -0
- pyfect/sql/mssql/_names.py +9 -0
- pyfect/sql/mssql/_pool.py +125 -0
- pyfect/sql/mssql/_tds.py +447 -0
- pyfect/sql/mssql/_tds_params.py +155 -0
- pyfect/sql/mssql/_worker.py +155 -0
- pyfect/sql/mssql/client.py +344 -0
- pyfect/sql/mssql/config.py +118 -0
- pyfect/sql/mssql/error.py +111 -0
- pyfect/sql/mssql/migrator.py +43 -0
- pyfect/sql/mssql/parameter.py +44 -0
- pyfect/sql/mssql/procedure.py +119 -0
- pyfect/sql/mssql/types.py +137 -0
- pyfect/sql/mysql/__init__.py +60 -0
- pyfect/sql/mysql/_asyncmy.py +168 -0
- pyfect/sql/mysql/_bind.py +76 -0
- pyfect/sql/mysql/_connection.py +156 -0
- pyfect/sql/mysql/_pool.py +63 -0
- pyfect/sql/mysql/_session.py +262 -0
- pyfect/sql/mysql/client.py +244 -0
- pyfect/sql/mysql/config.py +108 -0
- pyfect/sql/mysql/error.py +104 -0
- pyfect/sql/mysql/migrator.py +44 -0
- pyfect/sql/pg/__init__.py +82 -0
- pyfect/sql/pg/_resolve.py +284 -0
- pyfect/sql/pg/_session.py +380 -0
- pyfect/sql/pg/client.py +398 -0
- pyfect/sql/pg/config.py +142 -0
- pyfect/sql/pg/connection.py +585 -0
- pyfect/sql/pg/error.py +131 -0
- pyfect/sql/pg/migrator.py +139 -0
- pyfect/sql/pg/pool.py +265 -0
- pyfect/sql/resolver.py +294 -0
- pyfect/sql/result.py +24 -0
- pyfect/sql/schema.py +116 -0
- pyfect/sql/sqlite/__init__.py +26 -0
- pyfect/sql/sqlite/_connection.py +230 -0
- pyfect/sql/sqlite/_worker.py +71 -0
- pyfect/sql/sqlite/client.py +248 -0
- pyfect/sql/sqlite/migrator.py +43 -0
- pyfect/sql/statement/__init__.py +144 -0
- pyfect/sql/statement/_compiler.py +384 -0
- pyfect/sql/statement/_segments.py +487 -0
- pyfect/sql/statement/_statement.py +526 -0
- pyfect/sql/statement/_transforms.py +93 -0
- pyfect/sql/stream.py +88 -0
- pyfect/stdio.py +206 -0
- pyfect/stream/__init__.py +151 -0
- pyfect/stream/_async_iterable.py +129 -0
- pyfect/stream/_base.py +69 -0
- pyfect/stream/_bridge_methods.py +139 -0
- pyfect/stream/_catch.py +135 -0
- pyfect/stream/_catch_reason.py +558 -0
- pyfect/stream/_channel_methods.py +61 -0
- pyfect/stream/_chunk_methods.py +169 -0
- pyfect/stream/_chunking.py +310 -0
- pyfect/stream/_concat.py +65 -0
- pyfect/stream/_constructors.py +341 -0
- pyfect/stream/_context.py +147 -0
- pyfect/stream/_error_methods.py +254 -0
- pyfect/stream/_execution_plan.py +199 -0
- pyfect/stream/_fanout.py +154 -0
- pyfect/stream/_fanout_methods.py +183 -0
- pyfect/stream/_filter_methods.py +203 -0
- pyfect/stream/_grouping.py +168 -0
- pyfect/stream/_many.py +65 -0
- pyfect/stream/_map_methods.py +233 -0
- pyfect/stream/_mapping.py +126 -0
- pyfect/stream/_run.py +114 -0
- pyfect/stream/_run_methods.py +114 -0
- pyfect/stream/_service_methods.py +36 -0
- pyfect/stream/_sink_methods.py +151 -0
- pyfect/stream/_sinking.py +277 -0
- pyfect/stream/_sources.py +124 -0
- pyfect/stream/_stream.py +162 -0
- pyfect/stream/_taking.py +330 -0
- pyfect/stream/_text.py +43 -0
- pyfect/stream/_timing.py +294 -0
- pyfect/stream/_timing_methods.py +252 -0
- pyfect/stream/_zip_methods.py +279 -0
- pyfect/stream/_zipping.py +213 -0
- pyfect/subscription_ref.py +66 -0
- pyfect/synchronized_ref.py +38 -0
- pyfect/take.py +36 -0
- pyfect/terminal/__init__.py +41 -0
- pyfect/terminal/_keys.py +113 -0
- pyfect/terminal/_live.py +223 -0
- pyfect/terminal/_service.py +73 -0
- pyfect/testing/__init__.py +122 -0
- pyfect/testing/_assertions.py +92 -0
- pyfect/testing/_clock.py +309 -0
- pyfect/testing/_console.py +164 -0
- pyfect/testing/_crypto.py +44 -0
- pyfect/testing/_decorators.py +211 -0
- pyfect/testing/_flaky.py +34 -0
- pyfect/testing/_layers.py +153 -0
- pyfect/testing/_prop.py +255 -0
- pyfect/testing/_runners.py +84 -0
- pyfect/testing/_runtime.py +174 -0
- pyfect/testing/_schema.py +238 -0
- pyfect/testing/plugin.py +59 -0
- pyfect/tracer/__init__.py +107 -0
- pyfect/tracer/_model.py +176 -0
- pyfect/tracer/_native.py +172 -0
- pyfect/tracer/_refs.py +87 -0
- pyfect/tracer/_spans.py +269 -0
- pyfect/transaction.py +340 -0
- pyfect/trie.py +317 -0
- pyfect/tx_chunk.py +127 -0
- pyfect/tx_deferred.py +75 -0
- pyfect/tx_hash_map.py +265 -0
- pyfect/tx_hash_set.py +151 -0
- pyfect/tx_priority_queue.py +224 -0
- pyfect/tx_pubsub.py +194 -0
- pyfect/tx_queue/__init__.py +27 -0
- pyfect/tx_queue/_queue.py +359 -0
- pyfect/tx_queue/_views.py +60 -0
- pyfect/tx_reentrant_lock.py +201 -0
- pyfect/tx_ref.py +79 -0
- pyfect/tx_semaphore.py +136 -0
- pyfect/tx_subscription_ref.py +118 -0
- pyfect/workers/__init__.py +62 -0
- pyfect/workers/_frames.py +82 -0
- pyfect/workers/_mailbox.py +51 -0
- pyfect/workers/_port.py +106 -0
- pyfect/workers/_process.py +155 -0
- pyfect/workers/_thread.py +100 -0
- pyfect/workers/python_worker.py +133 -0
- pyfect/workers/python_worker_runner.py +112 -0
- pyfect/workers/transferable.py +103 -0
- pyfect/workers/worker.py +273 -0
- pyfect/workers/worker_error.py +86 -0
- pyfect/workers/worker_runner.py +85 -0
- pyfect/workflow/__init__.py +78 -0
- pyfect/workflow/_crypto.py +23 -0
- pyfect/workflow/_encoded.py +381 -0
- pyfect/workflow/_memory.py +424 -0
- pyfect/workflow/_result.py +43 -0
- pyfect/workflow/_services.py +241 -0
- pyfect/workflow/_suspension.py +153 -0
- pyfect/workflow/activity.py +427 -0
- pyfect/workflow/durable_clock.py +72 -0
- pyfect/workflow/durable_deferred.py +403 -0
- pyfect/workflow/durable_queue.py +259 -0
- pyfect/workflow/workflow.py +509 -0
- pyfect/workflow/workflow_engine.py +42 -0
- pyfect/workflow/workflow_proxy.py +161 -0
- pyfect/workflow/workflow_proxy_server.py +146 -0
- pyfect_dev-0.1.0.dist-info/METADATA +296 -0
- pyfect_dev-0.1.0.dist-info/RECORD +1265 -0
- pyfect_dev-0.1.0.dist-info/WHEEL +4 -0
- pyfect_dev-0.1.0.dist-info/entry_points.txt +3 -0
- pyfect_dev-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1001 @@
|
|
|
1
|
+
"""Message storage in a SQL database (Effect's ``SqlMessageStorage``).
|
|
2
|
+
|
|
3
|
+
Envelopes are rows of ``<prefix>_messages`` and replies rows of
|
|
4
|
+
``<prefix>_replies``, both keyed by their snowflakes. A request with a
|
|
5
|
+
primary key stores it in ``message_id`` (a SHA-256 digest when longer than
|
|
6
|
+
the 255-character column), which detects duplicates. ``unprocessed_messages``
|
|
7
|
+
claims the rows it returns by setting ``last_read``; a claim lapses after
|
|
8
|
+
ten minutes.
|
|
9
|
+
|
|
10
|
+
MySQL and SQL Server statements are ported but not run by the tests.
|
|
11
|
+
Python's integers are exact, so ids need no ``SafeIntegers``; SHA-256 comes
|
|
12
|
+
from ``hashlib`` (Effect reads a ``Crypto`` service).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import hashlib
|
|
18
|
+
from collections.abc import Generator, Mapping, Sequence
|
|
19
|
+
from typing import Any, Never
|
|
20
|
+
|
|
21
|
+
from pyfect import _json_text
|
|
22
|
+
from pyfect import layer as layer_
|
|
23
|
+
from pyfect._core import Effect
|
|
24
|
+
from pyfect._effect_args import require_effect
|
|
25
|
+
from pyfect._js import js_length, text_encode
|
|
26
|
+
from pyfect.cause import Cause, Fail
|
|
27
|
+
from pyfect.cluster import snowflake as snowflake_
|
|
28
|
+
from pyfect.cluster.cluster_error import PersistenceError
|
|
29
|
+
from pyfect.cluster.entity_address import EntityAddress
|
|
30
|
+
from pyfect.cluster.envelope import Encoded as EnvelopeEncoded
|
|
31
|
+
from pyfect.cluster.envelope import ShardIdEncoded
|
|
32
|
+
from pyfect.cluster.message_storage import (
|
|
33
|
+
SAVE_SUCCESS,
|
|
34
|
+
DuplicateEncoded,
|
|
35
|
+
EncodedMessage,
|
|
36
|
+
EncodedStorage,
|
|
37
|
+
MessageStorage,
|
|
38
|
+
MessageStorageService,
|
|
39
|
+
SaveResultEncoded,
|
|
40
|
+
make_encoded,
|
|
41
|
+
)
|
|
42
|
+
from pyfect.cluster.reply import ChunkEncoded, Encoded, WithExitEncoded
|
|
43
|
+
from pyfect.cluster.sharding_config import ShardingConfig
|
|
44
|
+
from pyfect.cluster.snowflake import Snowflake
|
|
45
|
+
from pyfect.effect import die, fail_cause, gen, succeed, suspend, void
|
|
46
|
+
from pyfect.sql import migrator
|
|
47
|
+
from pyfect.sql.client import SqlClient, SqlClientService
|
|
48
|
+
from pyfect.sql.connection import Row
|
|
49
|
+
from pyfect.sql.error import SqlError, is_sql_error
|
|
50
|
+
from pyfect.sql.result import affected_rows
|
|
51
|
+
from pyfect.sql.statement import Fragment, Statement
|
|
52
|
+
|
|
53
|
+
_REQUEST, _ACK_CHUNK, _INTERRUPT = 0, 1, 2
|
|
54
|
+
_WITH_EXIT = 0
|
|
55
|
+
"""A reply's ``kind``: ``0`` for an exit, ``NULL`` for a chunk."""
|
|
56
|
+
|
|
57
|
+
_MESSAGE_ID_WIDTH = 255
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def message_id_for_primary_key(primary_key: str) -> str:
|
|
61
|
+
"""The stored key: the primary key when it fits the column (in UTF-16
|
|
62
|
+
code units, as Effect measures it), else its
|
|
63
|
+
SHA-256 hex digest (a digest has no ``/``, a composed key always
|
|
64
|
+
does, so the two never collide)."""
|
|
65
|
+
if js_length(primary_key) <= _MESSAGE_ID_WIDTH:
|
|
66
|
+
return primary_key
|
|
67
|
+
return hashlib.sha256(text_encode(primary_key)).hexdigest()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _shard_id_encoded(text: str) -> ShardIdEncoded:
|
|
71
|
+
group, _, id = text.rpartition(":")
|
|
72
|
+
return {"group": group, "id": int(id)}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _address(row: Row) -> Any:
|
|
76
|
+
return {
|
|
77
|
+
"shardId": _shard_id_encoded(str(row["shard_id"])),
|
|
78
|
+
"entityType": row["entity_type"],
|
|
79
|
+
"entityId": row["entity_id"],
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _str(value: object) -> str:
|
|
84
|
+
return str(value)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _reply_from_row(row: Row) -> Encoded:
|
|
88
|
+
if row["kind"] is not None and int(str(row["kind"])) == _WITH_EXIT:
|
|
89
|
+
exit_reply: WithExitEncoded = {
|
|
90
|
+
"_tag": "WithExit",
|
|
91
|
+
"id": _str(row["id"]),
|
|
92
|
+
"requestId": _str(row["request_id"]),
|
|
93
|
+
"exit": _parse(str(row["payload"])),
|
|
94
|
+
}
|
|
95
|
+
return exit_reply
|
|
96
|
+
chunk: ChunkEncoded = {
|
|
97
|
+
"_tag": "Chunk",
|
|
98
|
+
"id": _str(row["id"]),
|
|
99
|
+
"requestId": _str(row["request_id"]),
|
|
100
|
+
"values": _parse(str(row["payload"])),
|
|
101
|
+
"sequence": int(str(row["sequence"])),
|
|
102
|
+
}
|
|
103
|
+
return chunk
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _message_from_row(row: Row) -> EncodedMessage:
|
|
107
|
+
kind = int(str(row["kind"]))
|
|
108
|
+
if kind == _REQUEST:
|
|
109
|
+
envelope: Any = {
|
|
110
|
+
"_tag": "Request",
|
|
111
|
+
"requestId": _str(row["id"]),
|
|
112
|
+
"address": _address(row),
|
|
113
|
+
"tag": row["tag"],
|
|
114
|
+
"payload": _parse(str(row["payload"])),
|
|
115
|
+
"headers": _parse(str(row["headers"])),
|
|
116
|
+
}
|
|
117
|
+
if row["trace_id"]:
|
|
118
|
+
envelope["traceId"] = row["trace_id"]
|
|
119
|
+
envelope["spanId"] = row["span_id"]
|
|
120
|
+
envelope["sampled"] = bool(row["sampled"])
|
|
121
|
+
last: Encoded | None = None
|
|
122
|
+
if row["reply_reply_id"]:
|
|
123
|
+
last = {
|
|
124
|
+
"_tag": "Chunk",
|
|
125
|
+
"id": _str(row["reply_reply_id"]),
|
|
126
|
+
"requestId": _str(row["request_id"]),
|
|
127
|
+
"sequence": int(str(row["reply_sequence"])),
|
|
128
|
+
"values": _parse(str(row["reply_payload"])),
|
|
129
|
+
}
|
|
130
|
+
return EncodedMessage(envelope, last)
|
|
131
|
+
if kind == _ACK_CHUNK:
|
|
132
|
+
ack: Any = {
|
|
133
|
+
"_tag": "AckChunk",
|
|
134
|
+
"id": _str(row["id"]),
|
|
135
|
+
"requestId": _str(row["request_id"]),
|
|
136
|
+
"replyId": _str(row["reply_id"]),
|
|
137
|
+
"address": _address(row),
|
|
138
|
+
}
|
|
139
|
+
return EncodedMessage(ack, None)
|
|
140
|
+
interrupt: Any = {
|
|
141
|
+
"_tag": "Interrupt",
|
|
142
|
+
"id": _str(row["id"]),
|
|
143
|
+
"requestId": _str(row["request_id"]),
|
|
144
|
+
"address": _address(row),
|
|
145
|
+
}
|
|
146
|
+
return EncodedMessage(interrupt, None)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _group_addresses(
|
|
150
|
+
addresses: Sequence[EntityAddress],
|
|
151
|
+
) -> list[tuple[str, str, list[str]]]:
|
|
152
|
+
"""``(shard, entity type, entity ids)`` groups, in first-seen order."""
|
|
153
|
+
groups: dict[tuple[str, str], list[str]] = {}
|
|
154
|
+
for address in addresses:
|
|
155
|
+
groups.setdefault((str(address.shard_id), address.entity_type), []).append(
|
|
156
|
+
address.entity_id
|
|
157
|
+
)
|
|
158
|
+
return [(shard, entity_type, ids) for (shard, entity_type), ids in groups.items()]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class _Storage:
|
|
162
|
+
"""The ``EncodedStorage`` of one client."""
|
|
163
|
+
|
|
164
|
+
def __init__(self, sql: SqlClientService, prefix: str) -> None:
|
|
165
|
+
self.sql = sql
|
|
166
|
+
self.messages = sql.identifier(f"{prefix}_messages")
|
|
167
|
+
self.replies = sql.identifier(f"{prefix}_replies")
|
|
168
|
+
self.supports_booleans: bool = sql.on_dialect_or_else(
|
|
169
|
+
mssql=lambda: False, sqlite=lambda: False, or_else=lambda: True
|
|
170
|
+
)
|
|
171
|
+
self.message_id_enforces_width: bool = sql.on_dialect_or_else(
|
|
172
|
+
mssql=lambda: True, mysql=lambda: True, pg=lambda: True, or_else=lambda: False
|
|
173
|
+
)
|
|
174
|
+
# SQLite ids read as text: drivers that return numbers would round
|
|
175
|
+
self.ids_as_text: bool = sql.dialect == "sqlite"
|
|
176
|
+
self.false = sql.literal("FALSE") if self.supports_booleans else sql.literal("0")
|
|
177
|
+
self.true = sql.literal("TRUE") if self.supports_booleans else sql.literal("1")
|
|
178
|
+
self.kind_ack_chunk = sql.literal("1")
|
|
179
|
+
self.kind_interrupt = sql.literal("2")
|
|
180
|
+
self.kind_with_exit = sql.literal("0")
|
|
181
|
+
self.ten_minutes_ago: Fragment = sql.on_dialect_or_else(
|
|
182
|
+
mssql=lambda: sql.literal("DATEADD(MINUTE, -10, GETDATE())"),
|
|
183
|
+
mysql=lambda: sql.literal("NOW() - INTERVAL 10 MINUTE"),
|
|
184
|
+
pg=lambda: sql.literal("NOW() - INTERVAL '10 minutes'"),
|
|
185
|
+
or_else=lambda: sql.literal("DATETIME('now', '-10 minute')"),
|
|
186
|
+
)
|
|
187
|
+
self.now: Fragment = sql.on_dialect_or_else(
|
|
188
|
+
pg=lambda: sql.literal("NOW()"),
|
|
189
|
+
mysql=lambda: sql.literal("NOW()"),
|
|
190
|
+
mssql=lambda: sql.literal("GETDATE()"),
|
|
191
|
+
or_else=lambda: sql.literal("CURRENT_TIMESTAMP"),
|
|
192
|
+
)
|
|
193
|
+
self.for_update: Fragment = sql.on_dialect_or_else(
|
|
194
|
+
mssql=lambda: sql.literal(""),
|
|
195
|
+
sqlite=lambda: sql.literal(""),
|
|
196
|
+
or_else=lambda: sql.literal("FOR UPDATE"),
|
|
197
|
+
)
|
|
198
|
+
self.update_lock: Fragment = sql.on_dialect_or_else(
|
|
199
|
+
mssql=lambda: sql.literal("WITH (UPDLOCK)"), or_else=lambda: sql.literal("")
|
|
200
|
+
)
|
|
201
|
+
reply_join = (
|
|
202
|
+
"r.kind as reply_kind, r.payload as reply_payload, r.sequence as reply_sequence"
|
|
203
|
+
)
|
|
204
|
+
if self.ids_as_text:
|
|
205
|
+
message_columns = (
|
|
206
|
+
f"{self._id_as('m.id', 'id')}, m.message_id, m.shard_id, m.entity_type, "
|
|
207
|
+
"m.entity_id, m.kind, m.tag, m.payload, m.headers, m.trace_id, m.span_id, "
|
|
208
|
+
f"m.sampled, m.processed, {self._id_as('m.request_id', 'request_id')}, "
|
|
209
|
+
f"{self._id_as('m.reply_id', 'reply_id')}, "
|
|
210
|
+
f"{self._id_as('m.last_reply_id', 'last_reply_id')}, m.last_read, m.deliver_at"
|
|
211
|
+
)
|
|
212
|
+
else:
|
|
213
|
+
message_columns = "m.*"
|
|
214
|
+
self.id_column = sql.unsafe(self._id_as("id", "id"))
|
|
215
|
+
self.reply_columns = sql.unsafe(
|
|
216
|
+
f"{self._id_as('id', 'id')}, kind, {self._id_as('request_id', 'request_id')}, "
|
|
217
|
+
"payload, sequence"
|
|
218
|
+
)
|
|
219
|
+
self.duplicate_columns = sql.unsafe(
|
|
220
|
+
f"{self._id_as('m.id', 'id')}, {self._id_as('r.id', 'reply_id')}, {reply_join}"
|
|
221
|
+
)
|
|
222
|
+
self.message_join_columns = sql.unsafe(
|
|
223
|
+
f"{message_columns}, {self._id_as('r.id', 'reply_reply_id')}, {reply_join}"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def _id_as(self, column: str, alias: str) -> str:
|
|
227
|
+
return f"CAST({column} AS TEXT) AS {alias}" if self.ids_as_text else f"{column} AS {alias}"
|
|
228
|
+
|
|
229
|
+
def wrap_string(self, text: str) -> str:
|
|
230
|
+
return f"N'{text}'" if self.sql.dialect == "mssql" else f"'{text}'"
|
|
231
|
+
|
|
232
|
+
def shard_list(self, shard_ids: Sequence[str]) -> Statement[Any]:
|
|
233
|
+
return self.sql.unsafe(",".join(self.wrap_string(s) for s in shard_ids))
|
|
234
|
+
|
|
235
|
+
def may_have_legacy_row(self, primary_key: str) -> bool:
|
|
236
|
+
"""SQLite's ``message_id`` stored over-long keys as text before
|
|
237
|
+
digests; those rows are read by the plain key too."""
|
|
238
|
+
return not self.message_id_enforces_width and js_length(primary_key) > _MESSAGE_ID_WIDTH
|
|
239
|
+
|
|
240
|
+
def sampled(self, sampled: bool | None) -> object:
|
|
241
|
+
if sampled is None:
|
|
242
|
+
return None
|
|
243
|
+
return sampled if self.supports_booleans else int(sampled)
|
|
244
|
+
|
|
245
|
+
def envelope_row(
|
|
246
|
+
self, envelope: EnvelopeEncoded, message_id: str | None, deliver_at: int | None
|
|
247
|
+
) -> dict[str, object]:
|
|
248
|
+
address = envelope["address"]
|
|
249
|
+
shard = f"{address['shardId']['group']}:{address['shardId']['id']}"
|
|
250
|
+
common: dict[str, object] = {
|
|
251
|
+
"message_id": message_id,
|
|
252
|
+
"shard_id": shard,
|
|
253
|
+
"entity_type": address["entityType"],
|
|
254
|
+
"entity_id": address["entityId"],
|
|
255
|
+
"deliver_at": deliver_at,
|
|
256
|
+
}
|
|
257
|
+
match envelope["_tag"]:
|
|
258
|
+
case "Request":
|
|
259
|
+
return {
|
|
260
|
+
"id": envelope["requestId"],
|
|
261
|
+
**common,
|
|
262
|
+
"kind": _REQUEST,
|
|
263
|
+
"tag": envelope["tag"],
|
|
264
|
+
"payload": _json_text.stringify(envelope["payload"]),
|
|
265
|
+
"headers": _json_text.stringify(dict(envelope["headers"])),
|
|
266
|
+
"trace_id": envelope.get("traceId"),
|
|
267
|
+
"span_id": envelope.get("spanId"),
|
|
268
|
+
"sampled": self.sampled(envelope.get("sampled")),
|
|
269
|
+
"request_id": envelope["requestId"],
|
|
270
|
+
"reply_id": None,
|
|
271
|
+
}
|
|
272
|
+
case "AckChunk":
|
|
273
|
+
return {
|
|
274
|
+
"id": envelope["id"],
|
|
275
|
+
**common,
|
|
276
|
+
"kind": _ACK_CHUNK,
|
|
277
|
+
"tag": None,
|
|
278
|
+
"payload": None,
|
|
279
|
+
"headers": None,
|
|
280
|
+
"trace_id": None,
|
|
281
|
+
"span_id": None,
|
|
282
|
+
"sampled": None,
|
|
283
|
+
"request_id": envelope["requestId"],
|
|
284
|
+
"reply_id": envelope["replyId"],
|
|
285
|
+
}
|
|
286
|
+
case "Interrupt":
|
|
287
|
+
return {
|
|
288
|
+
"id": envelope["id"],
|
|
289
|
+
**common,
|
|
290
|
+
"kind": _INTERRUPT,
|
|
291
|
+
"tag": None,
|
|
292
|
+
"payload": None,
|
|
293
|
+
"headers": None,
|
|
294
|
+
"trace_id": None,
|
|
295
|
+
"span_id": None,
|
|
296
|
+
"sampled": None,
|
|
297
|
+
"request_id": envelope["requestId"],
|
|
298
|
+
"reply_id": None,
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
def select_by_message_id(self, message_id: str) -> Effect[Sequence[Row], SqlError]:
|
|
302
|
+
return self.sql(
|
|
303
|
+
"SELECT {} FROM {} m LEFT JOIN {} r ON r.id = m.last_reply_id WHERE m.message_id = {}",
|
|
304
|
+
self.duplicate_columns,
|
|
305
|
+
self.messages,
|
|
306
|
+
self.replies,
|
|
307
|
+
message_id,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
def insert_envelope(
|
|
311
|
+
self, row: Mapping[str, object], message_id: str
|
|
312
|
+
) -> Effect[Sequence[Row], SqlError]:
|
|
313
|
+
sql = self.sql
|
|
314
|
+
|
|
315
|
+
def unless_inserted(inserted: Sequence[object]) -> Effect[Sequence[Row], SqlError]:
|
|
316
|
+
return succeed([]) if inserted else self.select_by_message_id(message_id)
|
|
317
|
+
|
|
318
|
+
if sql.dialect == "pg":
|
|
319
|
+
return sql(
|
|
320
|
+
"INSERT INTO {} {} ON CONFLICT (message_id) DO NOTHING RETURNING id",
|
|
321
|
+
self.messages,
|
|
322
|
+
sql.insert(row),
|
|
323
|
+
).flat_map(unless_inserted)
|
|
324
|
+
if sql.dialect == "mysql":
|
|
325
|
+
|
|
326
|
+
def affected(result: Any) -> Effect[Sequence[Row], SqlError]:
|
|
327
|
+
if affected_rows(result) > 0:
|
|
328
|
+
return succeed([])
|
|
329
|
+
return self.select_by_message_id(message_id)
|
|
330
|
+
|
|
331
|
+
return sql("INSERT IGNORE INTO {} {}", self.messages, sql.insert(row)).raw.flat_map(
|
|
332
|
+
affected
|
|
333
|
+
)
|
|
334
|
+
if sql.dialect == "mssql":
|
|
335
|
+
return sql(
|
|
336
|
+
"MERGE {} WITH (HOLDLOCK) AS target USING (SELECT {} as message_id) AS source "
|
|
337
|
+
"ON target.message_id = source.message_id WHEN NOT MATCHED THEN INSERT {} "
|
|
338
|
+
"OUTPUT inserted.id;",
|
|
339
|
+
self.messages,
|
|
340
|
+
message_id,
|
|
341
|
+
sql.insert(row),
|
|
342
|
+
).flat_map(unless_inserted)
|
|
343
|
+
insert = sql("INSERT OR IGNORE INTO {} {}", self.messages, sql.insert(row))
|
|
344
|
+
return sql.with_transaction(
|
|
345
|
+
self.select_by_message_id(message_id).tap(lambda _: insert)
|
|
346
|
+
).retry(times=3)
|
|
347
|
+
|
|
348
|
+
def address_filter(self, addresses: Sequence[EntityAddress], prefix: str) -> Fragment:
|
|
349
|
+
sql = self.sql
|
|
350
|
+
return sql.or_(
|
|
351
|
+
[
|
|
352
|
+
sql.and_(
|
|
353
|
+
[
|
|
354
|
+
sql(f"{prefix}shard_id = {{}}", shard), # pyright: ignore[reportArgumentType]
|
|
355
|
+
sql(f"{prefix}entity_type = {{}}", entity_type), # pyright: ignore[reportArgumentType]
|
|
356
|
+
sql.in_(f"{prefix}entity_id", ids),
|
|
357
|
+
]
|
|
358
|
+
)
|
|
359
|
+
for shard, entity_type, ids in _group_addresses(addresses)
|
|
360
|
+
]
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
def unprocessed_rows(
|
|
364
|
+
self,
|
|
365
|
+
shard_ids: Sequence[str],
|
|
366
|
+
now: int,
|
|
367
|
+
limit: int | None,
|
|
368
|
+
addresses: Sequence[EntityAddress] | None,
|
|
369
|
+
) -> Effect[Sequence[Row], SqlError]:
|
|
370
|
+
sql = self.sql
|
|
371
|
+
address_filter = (
|
|
372
|
+
sql.literal("")
|
|
373
|
+
if addresses is None
|
|
374
|
+
else sql("AND ({})", self.address_filter(addresses, "m."))
|
|
375
|
+
)
|
|
376
|
+
if limit is None:
|
|
377
|
+
limit_fragment: Fragment = sql.literal("")
|
|
378
|
+
elif sql.dialect == "mssql":
|
|
379
|
+
limit_fragment = sql.unsafe(f"OFFSET 0 ROWS FETCH NEXT {int(limit)} ROWS ONLY")
|
|
380
|
+
else:
|
|
381
|
+
limit_fragment = sql.unsafe(f"LIMIT {int(limit)}")
|
|
382
|
+
deliverable = sql.unsafe(str(now))
|
|
383
|
+
if sql.dialect == "pg":
|
|
384
|
+
return sql(
|
|
385
|
+
"WITH messages AS ("
|
|
386
|
+
"UPDATE {} m SET last_read = {} FROM ("
|
|
387
|
+
"SELECT m.* FROM {} m WHERE m.shard_id IN ({}) {} "
|
|
388
|
+
"AND NOT EXISTS (SELECT 1 FROM {} WHERE request_id = m.request_id "
|
|
389
|
+
"AND (kind = {} OR acked = {})) "
|
|
390
|
+
"AND m.processed = {} "
|
|
391
|
+
"AND (m.last_read IS NULL OR m.last_read < {}) "
|
|
392
|
+
"AND (m.deliver_at IS NULL OR m.deliver_at <= {}) "
|
|
393
|
+
"ORDER BY m.rowid ASC {} FOR UPDATE"
|
|
394
|
+
") AS ids LEFT JOIN {} r ON r.id = ids.last_reply_id "
|
|
395
|
+
"WHERE m.id = ids.id "
|
|
396
|
+
"RETURNING ids.*, r.id as reply_reply_id, r.kind as reply_kind, "
|
|
397
|
+
"r.payload as reply_payload, r.sequence as reply_sequence"
|
|
398
|
+
") SELECT * FROM messages ORDER BY rowid ASC",
|
|
399
|
+
self.messages,
|
|
400
|
+
self.now,
|
|
401
|
+
self.messages,
|
|
402
|
+
self.shard_list(shard_ids),
|
|
403
|
+
address_filter,
|
|
404
|
+
self.replies,
|
|
405
|
+
self.kind_with_exit,
|
|
406
|
+
self.false,
|
|
407
|
+
self.false,
|
|
408
|
+
self.ten_minutes_ago,
|
|
409
|
+
deliverable,
|
|
410
|
+
limit_fragment,
|
|
411
|
+
self.replies,
|
|
412
|
+
)
|
|
413
|
+
select = sql(
|
|
414
|
+
"SELECT {} FROM {} m {} LEFT JOIN {} r ON r.id = m.last_reply_id "
|
|
415
|
+
"WHERE m.shard_id IN ({}) {} "
|
|
416
|
+
"AND NOT EXISTS (SELECT 1 FROM {} WHERE request_id = m.request_id "
|
|
417
|
+
"AND (kind = {} OR acked = {})) "
|
|
418
|
+
"AND processed = {} "
|
|
419
|
+
"AND (m.last_read IS NULL OR m.last_read < {}) "
|
|
420
|
+
"AND (m.deliver_at IS NULL OR m.deliver_at <= {}) "
|
|
421
|
+
"ORDER BY m.rowid ASC {} {}",
|
|
422
|
+
self.message_join_columns,
|
|
423
|
+
self.messages,
|
|
424
|
+
self.update_lock,
|
|
425
|
+
self.replies,
|
|
426
|
+
self.shard_list(shard_ids),
|
|
427
|
+
address_filter,
|
|
428
|
+
self.replies,
|
|
429
|
+
self.kind_with_exit,
|
|
430
|
+
self.false,
|
|
431
|
+
self.false,
|
|
432
|
+
self.ten_minutes_ago,
|
|
433
|
+
deliverable,
|
|
434
|
+
limit_fragment,
|
|
435
|
+
self.for_update,
|
|
436
|
+
).unprepared
|
|
437
|
+
|
|
438
|
+
def claim(rows: Sequence[Row]) -> Effect[object, SqlError]:
|
|
439
|
+
if not rows:
|
|
440
|
+
return void
|
|
441
|
+
ids = sql.unsafe(",".join(str(row["id"]) for row in rows))
|
|
442
|
+
return sql(
|
|
443
|
+
"UPDATE {} SET last_read = {} WHERE id IN ({})", self.messages, self.now, ids
|
|
444
|
+
).unprepared
|
|
445
|
+
|
|
446
|
+
return sql.with_transaction(select.tap(claim))
|
|
447
|
+
|
|
448
|
+
def reset_unprocessed(self, filter: Fragment) -> Effect[object, SqlError]:
|
|
449
|
+
sql = self.sql
|
|
450
|
+
if sql.dialect == "pg":
|
|
451
|
+
# the claim query's lock order, so resets cannot deadlock with it
|
|
452
|
+
return sql(
|
|
453
|
+
"WITH messages AS (SELECT id FROM {} WHERE processed = {} AND {} "
|
|
454
|
+
"ORDER BY rowid ASC FOR UPDATE) "
|
|
455
|
+
"UPDATE {} SET last_read = NULL WHERE id IN (SELECT id FROM messages)",
|
|
456
|
+
self.messages,
|
|
457
|
+
self.false,
|
|
458
|
+
filter,
|
|
459
|
+
self.messages,
|
|
460
|
+
)
|
|
461
|
+
return sql(
|
|
462
|
+
"UPDATE {} SET last_read = NULL WHERE processed = {} AND {}",
|
|
463
|
+
self.messages,
|
|
464
|
+
self.false,
|
|
465
|
+
filter,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
# --- EncodedStorage ---------------------------------------------------------------
|
|
469
|
+
|
|
470
|
+
def save_envelope(
|
|
471
|
+
self, envelope: EnvelopeEncoded, primary_key: str | None, deliver_at: int | None, /
|
|
472
|
+
) -> Effect[SaveResultEncoded, PersistenceError]:
|
|
473
|
+
sql = self.sql
|
|
474
|
+
|
|
475
|
+
def insert() -> Effect[Sequence[Row], SqlError]:
|
|
476
|
+
if primary_key is not None:
|
|
477
|
+
message_id = message_id_for_primary_key(primary_key)
|
|
478
|
+
row = self.envelope_row(envelope, message_id, deliver_at)
|
|
479
|
+
if not self.may_have_legacy_row(primary_key):
|
|
480
|
+
return self.insert_envelope(row, message_id)
|
|
481
|
+
return self.select_by_message_id(primary_key).flat_map(
|
|
482
|
+
lambda rows: succeed(rows) if rows else self.insert_envelope(row, message_id)
|
|
483
|
+
)
|
|
484
|
+
row = self.envelope_row(envelope, None, deliver_at)
|
|
485
|
+
plain = sql("INSERT INTO {} {}", self.messages, sql.insert(row)).unprepared.as_(
|
|
486
|
+
list[Row]()
|
|
487
|
+
)
|
|
488
|
+
if envelope["_tag"] != "AckChunk":
|
|
489
|
+
return plain
|
|
490
|
+
return sql.with_transaction(
|
|
491
|
+
sql(
|
|
492
|
+
"UPDATE {} SET acked = {} WHERE id = {}",
|
|
493
|
+
self.replies,
|
|
494
|
+
self.true,
|
|
495
|
+
envelope["replyId"],
|
|
496
|
+
)
|
|
497
|
+
.flat_map(
|
|
498
|
+
lambda _: sql(
|
|
499
|
+
"UPDATE {} SET processed = {} WHERE processed = {} AND request_id = {} "
|
|
500
|
+
"AND kind = {}",
|
|
501
|
+
self.messages,
|
|
502
|
+
self.true,
|
|
503
|
+
self.false,
|
|
504
|
+
envelope["requestId"],
|
|
505
|
+
self.kind_ack_chunk,
|
|
506
|
+
)
|
|
507
|
+
)
|
|
508
|
+
.flat_map(lambda _: plain)
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
def result(rows: Sequence[Row]) -> SaveResultEncoded:
|
|
512
|
+
if not rows:
|
|
513
|
+
return SAVE_SUCCESS
|
|
514
|
+
row = rows[0]
|
|
515
|
+
last: Encoded | None = None
|
|
516
|
+
if row["reply_id"]:
|
|
517
|
+
kind = row["reply_kind"]
|
|
518
|
+
if kind is not None and int(str(kind)) == _WITH_EXIT:
|
|
519
|
+
last = {
|
|
520
|
+
"_tag": "WithExit",
|
|
521
|
+
"id": _str(row["reply_id"]),
|
|
522
|
+
"requestId": _str(row["id"]),
|
|
523
|
+
"exit": _parse(str(row["reply_payload"])),
|
|
524
|
+
}
|
|
525
|
+
else:
|
|
526
|
+
last = {
|
|
527
|
+
"_tag": "Chunk",
|
|
528
|
+
"id": _str(row["reply_id"]),
|
|
529
|
+
"requestId": _str(row["id"]),
|
|
530
|
+
"sequence": int(str(row["reply_sequence"])),
|
|
531
|
+
"values": _parse(str(row["reply_payload"])),
|
|
532
|
+
}
|
|
533
|
+
return DuplicateEncoded(
|
|
534
|
+
original_id=Snowflake(int(str(row["id"]))), last_received_reply=last
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
return _refail(suspend(insert).map(result))
|
|
538
|
+
|
|
539
|
+
def save_reply(self, reply: Encoded, /) -> Effect[None, PersistenceError]:
|
|
540
|
+
sql = self.sql
|
|
541
|
+
|
|
542
|
+
def run() -> Effect[object, SqlError]:
|
|
543
|
+
if reply["_tag"] == "WithExit":
|
|
544
|
+
kind: object = _WITH_EXIT
|
|
545
|
+
payload = _json_text.stringify(reply["exit"])
|
|
546
|
+
sequence: object = None
|
|
547
|
+
update = sql(
|
|
548
|
+
"UPDATE {} SET processed = {}, last_reply_id = {} WHERE request_id = {}",
|
|
549
|
+
self.messages,
|
|
550
|
+
self.true,
|
|
551
|
+
reply["id"],
|
|
552
|
+
reply["requestId"],
|
|
553
|
+
)
|
|
554
|
+
else:
|
|
555
|
+
kind = None
|
|
556
|
+
payload = _json_text.stringify(list(reply["values"]))
|
|
557
|
+
sequence = reply["sequence"]
|
|
558
|
+
update = sql(
|
|
559
|
+
"UPDATE {} SET last_reply_id = {} WHERE id = {}",
|
|
560
|
+
self.messages,
|
|
561
|
+
reply["id"],
|
|
562
|
+
reply["requestId"],
|
|
563
|
+
)
|
|
564
|
+
row = {
|
|
565
|
+
"id": reply["id"],
|
|
566
|
+
"kind": kind,
|
|
567
|
+
"request_id": reply["requestId"],
|
|
568
|
+
"payload": payload,
|
|
569
|
+
"sequence": sequence,
|
|
570
|
+
}
|
|
571
|
+
insert = sql("INSERT INTO {} {}", self.replies, sql.insert(row))
|
|
572
|
+
return sql.with_transaction(update.unprepared.flat_map(lambda _: insert))
|
|
573
|
+
|
|
574
|
+
return _refail(suspend(run).as_void())
|
|
575
|
+
|
|
576
|
+
def clear_replies(
|
|
577
|
+
self, request_id: Snowflake, /, *, expected_reply_id: Snowflake | None = None
|
|
578
|
+
) -> Effect[None, PersistenceError]:
|
|
579
|
+
sql = self.sql
|
|
580
|
+
id = str(request_id)
|
|
581
|
+
|
|
582
|
+
def body() -> Generator[Effect[Any, Any, Any], Any]:
|
|
583
|
+
if expected_reply_id is None:
|
|
584
|
+
yield from sql(
|
|
585
|
+
"DELETE FROM {} WHERE request_id = {} AND kind = 0", self.replies, id
|
|
586
|
+
)
|
|
587
|
+
yield from sql(
|
|
588
|
+
"DELETE FROM {} WHERE request_id = {} AND kind = {}",
|
|
589
|
+
self.messages,
|
|
590
|
+
id,
|
|
591
|
+
self.kind_interrupt,
|
|
592
|
+
)
|
|
593
|
+
yield from sql(
|
|
594
|
+
"UPDATE {} SET processed = {}, last_reply_id = NULL, last_read = NULL "
|
|
595
|
+
"WHERE request_id = {}",
|
|
596
|
+
self.messages,
|
|
597
|
+
self.false,
|
|
598
|
+
id,
|
|
599
|
+
)
|
|
600
|
+
return
|
|
601
|
+
expected = str(expected_reply_id)
|
|
602
|
+
# checked here, at the storage boundary, not at an earlier read
|
|
603
|
+
yield from sql(
|
|
604
|
+
"DELETE FROM {} WHERE request_id = {} AND kind = 0 "
|
|
605
|
+
"AND EXISTS (SELECT 1 FROM {} WHERE id = {} AND last_reply_id = {})",
|
|
606
|
+
self.replies,
|
|
607
|
+
id,
|
|
608
|
+
self.messages,
|
|
609
|
+
id,
|
|
610
|
+
expected,
|
|
611
|
+
)
|
|
612
|
+
yield from sql(
|
|
613
|
+
"UPDATE {} SET processed = {}, last_reply_id = NULL, last_read = NULL "
|
|
614
|
+
"WHERE request_id = {} AND last_reply_id = {}",
|
|
615
|
+
self.messages,
|
|
616
|
+
self.false,
|
|
617
|
+
id,
|
|
618
|
+
expected,
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
return _refail(sql.with_transaction(gen(body)))
|
|
622
|
+
|
|
623
|
+
def request_id_for_primary_key(
|
|
624
|
+
self, primary_key: str, /
|
|
625
|
+
) -> Effect[Snowflake | None, PersistenceError]:
|
|
626
|
+
sql = self.sql
|
|
627
|
+
|
|
628
|
+
def select(key: str) -> Effect[Sequence[Row], SqlError]:
|
|
629
|
+
return sql(
|
|
630
|
+
"SELECT {} FROM {} WHERE message_id = {}", self.id_column, self.messages, key
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
def legacy(rows: Sequence[Row]) -> Effect[Sequence[Row], SqlError]:
|
|
634
|
+
if not rows and self.may_have_legacy_row(primary_key):
|
|
635
|
+
return select(primary_key)
|
|
636
|
+
return succeed(rows)
|
|
637
|
+
|
|
638
|
+
return _refail(
|
|
639
|
+
select(message_id_for_primary_key(primary_key))
|
|
640
|
+
.flat_map(legacy)
|
|
641
|
+
.map(lambda rows: Snowflake(int(str(rows[0]["id"]))) if rows else None)
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
def replies_for(self, request_ids: Sequence[str], /) -> Effect[list[Encoded], PersistenceError]:
|
|
645
|
+
# exits, and chunks not acknowledged yet
|
|
646
|
+
sql = self.sql
|
|
647
|
+
return _refail(
|
|
648
|
+
sql(
|
|
649
|
+
"SELECT {} FROM {} WHERE request_id IN ({}) "
|
|
650
|
+
"AND (kind = {} OR (kind IS NULL AND acked = {})) ORDER BY rowid ASC",
|
|
651
|
+
self.reply_columns,
|
|
652
|
+
self.replies,
|
|
653
|
+
sql.unsafe(",".join(str(int(i)) for i in request_ids)),
|
|
654
|
+
self.kind_with_exit,
|
|
655
|
+
self.false,
|
|
656
|
+
).unprepared.map(lambda rows: [_reply_from_row(r) for r in rows])
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
def replies_for_unfiltered(
|
|
660
|
+
self, request_ids: Sequence[str], /
|
|
661
|
+
) -> Effect[list[Encoded], PersistenceError]:
|
|
662
|
+
sql = self.sql
|
|
663
|
+
return _refail(
|
|
664
|
+
sql(
|
|
665
|
+
"SELECT {} FROM {} WHERE request_id IN ({}) ORDER BY rowid ASC",
|
|
666
|
+
self.reply_columns,
|
|
667
|
+
self.replies,
|
|
668
|
+
sql.unsafe(",".join(str(int(i)) for i in request_ids)),
|
|
669
|
+
).unprepared.map(lambda rows: [_reply_from_row(r) for r in rows])
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
def unprocessed_messages(
|
|
673
|
+
self,
|
|
674
|
+
shard_ids: Sequence[str],
|
|
675
|
+
now: int,
|
|
676
|
+
/,
|
|
677
|
+
*,
|
|
678
|
+
limit: int | None = None,
|
|
679
|
+
addresses: Sequence[EntityAddress] | None = None,
|
|
680
|
+
) -> Effect[list[EncodedMessage], PersistenceError]:
|
|
681
|
+
if addresses is not None and len(addresses) == 0:
|
|
682
|
+
return succeed([])
|
|
683
|
+
return _refail(
|
|
684
|
+
self.unprocessed_rows(shard_ids, now, limit, addresses).map(
|
|
685
|
+
lambda rows: [_message_from_row(r) for r in rows]
|
|
686
|
+
)
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
def unprocessed_messages_by_id(
|
|
690
|
+
self, message_ids: Sequence[Snowflake], now: int, /
|
|
691
|
+
) -> Effect[list[EncodedMessage], PersistenceError]:
|
|
692
|
+
sql = self.sql
|
|
693
|
+
return _refail(
|
|
694
|
+
sql(
|
|
695
|
+
"SELECT {} FROM {} m LEFT JOIN {} r ON r.id = m.last_reply_id "
|
|
696
|
+
"WHERE m.id IN ({}) "
|
|
697
|
+
"AND NOT EXISTS (SELECT 1 FROM {} WHERE request_id = m.request_id "
|
|
698
|
+
"AND (kind = {} OR acked = {})) "
|
|
699
|
+
"AND m.processed = {} "
|
|
700
|
+
"AND (m.deliver_at IS NULL OR m.deliver_at <= {}) "
|
|
701
|
+
"ORDER BY m.rowid ASC",
|
|
702
|
+
self.message_join_columns,
|
|
703
|
+
self.messages,
|
|
704
|
+
self.replies,
|
|
705
|
+
sql.unsafe(",".join(str(int(i)) for i in message_ids)),
|
|
706
|
+
self.replies,
|
|
707
|
+
self.kind_with_exit,
|
|
708
|
+
self.false,
|
|
709
|
+
self.false,
|
|
710
|
+
sql.unsafe(str(now)),
|
|
711
|
+
).unprepared.map(lambda rows: [_message_from_row(r) for r in rows])
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
def reset_requests(self, request_ids: Sequence[Snowflake], /) -> Effect[None, PersistenceError]:
|
|
715
|
+
if not request_ids:
|
|
716
|
+
return void
|
|
717
|
+
sql = self.sql
|
|
718
|
+
return _refail(
|
|
719
|
+
sql(
|
|
720
|
+
"UPDATE {} SET last_read = NULL WHERE processed = {} AND {}",
|
|
721
|
+
self.messages,
|
|
722
|
+
self.false,
|
|
723
|
+
sql.in_("id", [str(i) for i in request_ids]),
|
|
724
|
+
).as_void()
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
def reset_addresses(
|
|
728
|
+
self, addresses: Sequence[EntityAddress], /
|
|
729
|
+
) -> Effect[None, PersistenceError]:
|
|
730
|
+
if not addresses:
|
|
731
|
+
return void
|
|
732
|
+
filter = self.sql("({})", self.address_filter(addresses, ""))
|
|
733
|
+
return _refail(self.reset_unprocessed(filter).as_void())
|
|
734
|
+
|
|
735
|
+
def clear_address(self, address: EntityAddress, /) -> Effect[None, PersistenceError]:
|
|
736
|
+
sql = self.sql
|
|
737
|
+
replies = sql(
|
|
738
|
+
"DELETE FROM {} WHERE request_id IN (SELECT id FROM {} WHERE entity_type = {} "
|
|
739
|
+
"AND entity_id = {})",
|
|
740
|
+
self.replies,
|
|
741
|
+
self.messages,
|
|
742
|
+
address.entity_type,
|
|
743
|
+
address.entity_id,
|
|
744
|
+
)
|
|
745
|
+
messages = sql(
|
|
746
|
+
"DELETE FROM {} WHERE entity_type = {} AND entity_id = {}",
|
|
747
|
+
self.messages,
|
|
748
|
+
address.entity_type,
|
|
749
|
+
address.entity_id,
|
|
750
|
+
)
|
|
751
|
+
return _refail(sql.with_transaction(replies.flat_map(lambda _: messages)).as_void())
|
|
752
|
+
|
|
753
|
+
def reset_shards(self, shard_ids: Sequence[str], /) -> Effect[None, PersistenceError]:
|
|
754
|
+
filter = self.sql("shard_id IN ({})", self.shard_list(shard_ids))
|
|
755
|
+
return _refail(self.reset_unprocessed(filter).as_void())
|
|
756
|
+
|
|
757
|
+
def with_transaction[A, E, R](self, effect: Effect[A, E, R], /) -> Effect[A, E, R]:
|
|
758
|
+
require_effect(effect, "effect")
|
|
759
|
+
|
|
760
|
+
def defect(cause: Cause[Any]) -> Effect[A, E, R]:
|
|
761
|
+
found = cause.find_recoverable_error()
|
|
762
|
+
if isinstance(found, Fail) and is_sql_error(found.error):
|
|
763
|
+
return die(found.error)
|
|
764
|
+
return fail_cause(found if not isinstance(found, Fail) else cause)
|
|
765
|
+
|
|
766
|
+
return self.sql.with_transaction(effect).catch_cause(defect)
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def _refail[A](effect: Effect[A, Any]) -> Effect[A, PersistenceError]:
|
|
770
|
+
return PersistenceError.refail(effect).with_tracer_enabled(False)
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def make_encoded_storage(*, prefix: str = "cluster") -> Effect[EncodedStorage, Never, SqlClient]:
|
|
774
|
+
"""The driver on the tables of ``prefix`` (already migrated)."""
|
|
775
|
+
return SqlClient().map(lambda sql: _Storage(sql.without_transforms(), prefix))
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def make(
|
|
779
|
+
*, prefix: str = "cluster"
|
|
780
|
+
) -> Effect[MessageStorageService, Never, SqlClient | snowflake_.Generator]:
|
|
781
|
+
"""Run the migrations, then a message storage on the tables."""
|
|
782
|
+
return (
|
|
783
|
+
run_migrations(prefix=prefix)
|
|
784
|
+
.flat_map(lambda _: make_encoded_storage(prefix=prefix))
|
|
785
|
+
.flat_map(make_encoded)
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def _create_messages(sql: SqlClientService, table: str) -> Effect[object, SqlError]:
|
|
790
|
+
messages = sql.identifier(table)
|
|
791
|
+
return sql.on_dialect_or_else(
|
|
792
|
+
mssql=lambda: sql(
|
|
793
|
+
"IF OBJECT_ID(N'{}', N'U') IS NULL CREATE TABLE {} ("
|
|
794
|
+
"id BIGINT PRIMARY KEY, rowid BIGINT IDENTITY(1,1), message_id VARCHAR(255), "
|
|
795
|
+
"shard_id VARCHAR(50) NOT NULL, entity_type VARCHAR(150) NOT NULL, "
|
|
796
|
+
"entity_id VARCHAR(255) NOT NULL, kind INT NOT NULL, tag VARCHAR(50), "
|
|
797
|
+
"payload NVARCHAR(MAX), headers NVARCHAR(MAX), trace_id VARCHAR(32), "
|
|
798
|
+
"span_id VARCHAR(16), sampled BIT, processed BIT NOT NULL DEFAULT 0, "
|
|
799
|
+
"request_id BIGINT NOT NULL, reply_id BIGINT, last_reply_id BIGINT, "
|
|
800
|
+
"last_read DATETIME, deliver_at BIGINT, "
|
|
801
|
+
"INDEX {} UNIQUE (message_id) WHERE message_id IS NOT NULL)",
|
|
802
|
+
messages,
|
|
803
|
+
messages,
|
|
804
|
+
sql.identifier(f"{table}_message_id_idx"),
|
|
805
|
+
),
|
|
806
|
+
mysql=lambda: sql(
|
|
807
|
+
"CREATE TABLE IF NOT EXISTS {} ("
|
|
808
|
+
"id BIGINT NOT NULL, rowid BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, "
|
|
809
|
+
"message_id VARCHAR(255), shard_id VARCHAR(50) NOT NULL, "
|
|
810
|
+
"entity_type VARCHAR(150) NOT NULL, entity_id VARCHAR(255) NOT NULL, "
|
|
811
|
+
"kind INT NOT NULL, tag VARCHAR(50), payload TEXT, headers TEXT, "
|
|
812
|
+
"trace_id VARCHAR(32), span_id VARCHAR(16), sampled BOOLEAN, "
|
|
813
|
+
"processed BOOLEAN NOT NULL DEFAULT FALSE, request_id BIGINT NOT NULL, "
|
|
814
|
+
"reply_id BIGINT, last_reply_id BIGINT, last_read DATETIME, deliver_at BIGINT, "
|
|
815
|
+
"UNIQUE (id), UNIQUE (message_id))",
|
|
816
|
+
messages,
|
|
817
|
+
),
|
|
818
|
+
pg=lambda: sql(
|
|
819
|
+
"CREATE TABLE IF NOT EXISTS {} ("
|
|
820
|
+
"id BIGINT PRIMARY KEY, rowid BIGSERIAL, message_id VARCHAR(255), "
|
|
821
|
+
"shard_id VARCHAR(50) NOT NULL, entity_type VARCHAR(150) NOT NULL, "
|
|
822
|
+
"entity_id VARCHAR(255) NOT NULL, kind INT NOT NULL, tag VARCHAR(50), "
|
|
823
|
+
"payload TEXT, headers TEXT, trace_id VARCHAR(32), span_id VARCHAR(16), "
|
|
824
|
+
"sampled BOOLEAN, processed BOOLEAN NOT NULL DEFAULT FALSE, "
|
|
825
|
+
"request_id BIGINT NOT NULL, reply_id BIGINT, last_reply_id BIGINT, "
|
|
826
|
+
"last_read TIMESTAMP, deliver_at BIGINT, UNIQUE (message_id))",
|
|
827
|
+
messages,
|
|
828
|
+
),
|
|
829
|
+
or_else=lambda: sql(
|
|
830
|
+
"CREATE TABLE IF NOT EXISTS {} ("
|
|
831
|
+
"id INTEGER PRIMARY KEY, message_id TEXT, shard_id TEXT NOT NULL, "
|
|
832
|
+
"entity_type TEXT NOT NULL, entity_id TEXT NOT NULL, kind INTEGER NOT NULL, "
|
|
833
|
+
"tag TEXT, payload TEXT, headers TEXT, trace_id TEXT, span_id TEXT, "
|
|
834
|
+
"sampled BOOLEAN, processed BOOLEAN NOT NULL DEFAULT FALSE, "
|
|
835
|
+
"request_id INTEGER NOT NULL, reply_id INTEGER, last_reply_id INTEGER, "
|
|
836
|
+
"last_read TEXT, deliver_at INTEGER, UNIQUE (message_id))",
|
|
837
|
+
messages,
|
|
838
|
+
),
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def _create_replies(sql: SqlClientService, table: str) -> Effect[object, SqlError]:
|
|
843
|
+
replies = sql.identifier(table)
|
|
844
|
+
return sql.on_dialect_or_else(
|
|
845
|
+
mssql=lambda: sql(
|
|
846
|
+
"IF OBJECT_ID(N'{}', N'U') IS NULL CREATE TABLE {} ("
|
|
847
|
+
"id BIGINT PRIMARY KEY, rowid BIGINT IDENTITY(1,1), kind INT, "
|
|
848
|
+
"request_id BIGINT NOT NULL, payload NVARCHAR(MAX) NOT NULL, sequence INT, "
|
|
849
|
+
"acked BIT NOT NULL DEFAULT 0, "
|
|
850
|
+
"INDEX {} UNIQUE (request_id, kind) WHERE kind IS NOT NULL, "
|
|
851
|
+
"INDEX {} UNIQUE (request_id, sequence) WHERE sequence IS NOT NULL)",
|
|
852
|
+
replies,
|
|
853
|
+
replies,
|
|
854
|
+
sql.identifier(f"{table}_one_exit"),
|
|
855
|
+
sql.identifier(f"{table}_sequence"),
|
|
856
|
+
),
|
|
857
|
+
mysql=lambda: sql(
|
|
858
|
+
"CREATE TABLE IF NOT EXISTS {} ("
|
|
859
|
+
"id BIGINT NOT NULL, rowid BIGINT AUTO_INCREMENT PRIMARY KEY, kind INT, "
|
|
860
|
+
"request_id BIGINT NOT NULL, payload TEXT NOT NULL, sequence INT, "
|
|
861
|
+
"acked BOOLEAN NOT NULL DEFAULT FALSE, UNIQUE (id), UNIQUE (request_id, kind), "
|
|
862
|
+
"UNIQUE (request_id, sequence))",
|
|
863
|
+
replies,
|
|
864
|
+
),
|
|
865
|
+
pg=lambda: sql(
|
|
866
|
+
"CREATE TABLE IF NOT EXISTS {} ("
|
|
867
|
+
"id BIGINT PRIMARY KEY, rowid BIGSERIAL, kind INT, request_id BIGINT NOT NULL, "
|
|
868
|
+
"payload TEXT NOT NULL, sequence INT, acked BOOLEAN NOT NULL DEFAULT FALSE, "
|
|
869
|
+
"UNIQUE (request_id, kind), UNIQUE (request_id, sequence))",
|
|
870
|
+
replies,
|
|
871
|
+
),
|
|
872
|
+
or_else=lambda: sql(
|
|
873
|
+
"CREATE TABLE IF NOT EXISTS {} ("
|
|
874
|
+
"id INTEGER PRIMARY KEY, kind INTEGER, request_id INTEGER NOT NULL, "
|
|
875
|
+
"payload TEXT NOT NULL, sequence INTEGER, acked BOOLEAN NOT NULL DEFAULT FALSE, "
|
|
876
|
+
"UNIQUE (request_id, kind), UNIQUE (request_id, sequence))",
|
|
877
|
+
replies,
|
|
878
|
+
),
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
def _index(sql: SqlClientService, name: str, table: str, columns: str) -> Effect[object, SqlError]:
|
|
883
|
+
index = sql.identifier(name)
|
|
884
|
+
target = sql.identifier(table)
|
|
885
|
+
cols = sql.unsafe(columns)
|
|
886
|
+
return sql.on_dialect_or_else(
|
|
887
|
+
mssql=lambda: sql(
|
|
888
|
+
"IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = {}) CREATE INDEX {} ON {} ({})",
|
|
889
|
+
name,
|
|
890
|
+
index,
|
|
891
|
+
target,
|
|
892
|
+
cols,
|
|
893
|
+
),
|
|
894
|
+
# MySQL has no IF NOT EXISTS for indexes: an existing one fails
|
|
895
|
+
mysql=lambda: sql("CREATE INDEX {} ON {} ({})", index, target, cols).unprepared.ignore(
|
|
896
|
+
log=False
|
|
897
|
+
),
|
|
898
|
+
or_else=lambda: sql("CREATE INDEX IF NOT EXISTS {} ON {} ({})", index, target, cols),
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def migrations(*, prefix: str = "cluster") -> migrator.Loader:
|
|
903
|
+
"""The message tables' migrations. PostgreSQL DDL is serialized by the
|
|
904
|
+
migrator; errors propagate (a retry in an aborted transaction cannot
|
|
905
|
+
succeed)."""
|
|
906
|
+
messages_table = f"{prefix}_messages"
|
|
907
|
+
replies_table = f"{prefix}_replies"
|
|
908
|
+
|
|
909
|
+
def create_tables() -> Generator[Effect[Any, Any, Any], Any]:
|
|
910
|
+
sql = (yield from SqlClient()).without_transforms()
|
|
911
|
+
yield from _create_messages(sql, messages_table)
|
|
912
|
+
yield from _index(
|
|
913
|
+
sql,
|
|
914
|
+
f"{messages_table}_shard_idx",
|
|
915
|
+
messages_table,
|
|
916
|
+
"shard_id, processed, last_read, deliver_at",
|
|
917
|
+
)
|
|
918
|
+
yield from _index(sql, f"{messages_table}_request_id_idx", messages_table, "request_id")
|
|
919
|
+
yield from _create_replies(sql, replies_table)
|
|
920
|
+
yield from _index(
|
|
921
|
+
sql, f"{replies_table}_request_lookup_idx", replies_table, "request_id, kind, acked"
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
def entity_type_size() -> Generator[Effect[Any, Any, Any], Any]:
|
|
925
|
+
sql = (yield from SqlClient()).without_transforms()
|
|
926
|
+
messages = sql.identifier(messages_table)
|
|
927
|
+
yield from sql.on_dialect_or_else(
|
|
928
|
+
mssql=lambda: sql(
|
|
929
|
+
"ALTER TABLE {} ALTER COLUMN entity_type VARCHAR(150) NOT NULL;", messages
|
|
930
|
+
),
|
|
931
|
+
mysql=lambda: sql(
|
|
932
|
+
"ALTER TABLE {} MODIFY entity_type VARCHAR(150) NOT NULL;", messages
|
|
933
|
+
).unprepared.ignore(log=False),
|
|
934
|
+
pg=lambda: sql("ALTER TABLE {} ALTER COLUMN entity_type TYPE VARCHAR(150);", messages),
|
|
935
|
+
or_else=lambda: void,
|
|
936
|
+
)
|
|
937
|
+
|
|
938
|
+
def pg_rowid_index() -> Generator[Effect[Any, Any, Any], Any]:
|
|
939
|
+
sql = (yield from SqlClient()).without_transforms()
|
|
940
|
+
yield from sql.on_dialect_or_else(
|
|
941
|
+
pg=lambda: _index(sql, f"{messages_table}_rowid_idx", messages_table, "rowid"),
|
|
942
|
+
or_else=lambda: void,
|
|
943
|
+
)
|
|
944
|
+
|
|
945
|
+
return migrator.from_record(
|
|
946
|
+
{
|
|
947
|
+
"0001_create_tables": gen(create_tables),
|
|
948
|
+
"0002_entity_type_size": gen(entity_type_size),
|
|
949
|
+
"0003_pg_messages_rowid_index": gen(pg_rowid_index),
|
|
950
|
+
}
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def run_migrations(*, prefix: str = "cluster") -> Effect[None, Never, SqlClient]:
|
|
955
|
+
"""Run the message migrations; a failure is a defect."""
|
|
956
|
+
return (
|
|
957
|
+
migrator.make()(loader=migrations(prefix=prefix), table=f"{prefix}_migrations")
|
|
958
|
+
.as_void()
|
|
959
|
+
.or_die()
|
|
960
|
+
)
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
def layer_migrations(*, prefix: str = "cluster") -> layer_.Layer[Never, Never, SqlClient]:
|
|
964
|
+
return layer_.effect_discard(run_migrations(prefix=prefix))
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def layer_storage(
|
|
968
|
+
*, prefix: str = "cluster"
|
|
969
|
+
) -> layer_.Layer[MessageStorage, Never, SqlClient | ShardingConfig]:
|
|
970
|
+
"""Message storage on migrated tables (with its own snowflake
|
|
971
|
+
generator)."""
|
|
972
|
+
return layer_.effect(
|
|
973
|
+
MessageStorage, make_encoded_storage(prefix=prefix).flat_map(make_encoded)
|
|
974
|
+
).pipe(layer_.provide(snowflake_.layer_generator))
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
def layer_with(
|
|
978
|
+
*, prefix: str = "cluster"
|
|
979
|
+
) -> layer_.Layer[MessageStorage, Never, SqlClient | ShardingConfig]:
|
|
980
|
+
"""``layer_storage`` after the migrations."""
|
|
981
|
+
return layer_storage(prefix=prefix).pipe(layer_.provide(layer_migrations(prefix=prefix)))
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
layer: layer_.Layer[MessageStorage, Never, SqlClient | ShardingConfig] = layer_with()
|
|
985
|
+
|
|
986
|
+
__all__ = [
|
|
987
|
+
"layer",
|
|
988
|
+
"layer_migrations",
|
|
989
|
+
"layer_storage",
|
|
990
|
+
"layer_with",
|
|
991
|
+
"make",
|
|
992
|
+
"make_encoded_storage",
|
|
993
|
+
"message_id_for_primary_key",
|
|
994
|
+
"migrations",
|
|
995
|
+
"run_migrations",
|
|
996
|
+
]
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
def _parse(text: str) -> Any:
|
|
1000
|
+
"""A stored JSON column (written by ``_json_text.stringify``)."""
|
|
1001
|
+
return _json_text.parse(text)
|