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,841 @@
|
|
|
1
|
+
"""Runner storage in a SQL database (Effect's ``SqlRunnerStorage``).
|
|
2
|
+
|
|
3
|
+
Runners are rows of ``<prefix>_runners``. Shard locks are PostgreSQL or
|
|
4
|
+
MySQL advisory locks held by one reserved connection (rebuilt when a
|
|
5
|
+
statement on it fails), or, on SQLite and SQL Server (or with
|
|
6
|
+
``shard_lock_disable_advisory``), leases in ``<prefix>_locks`` that
|
|
7
|
+
expire after ``shard_lock_expiration``. Every lock operation has a
|
|
8
|
+
deadline of the effective lock refresh interval.
|
|
9
|
+
|
|
10
|
+
MySQL and SQL Server statements are ported but not run by the tests.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import math
|
|
16
|
+
import random
|
|
17
|
+
from collections.abc import Callable, Generator, Sequence
|
|
18
|
+
from typing import Any, Never, cast
|
|
19
|
+
|
|
20
|
+
from pyfect import layer as layer_
|
|
21
|
+
from pyfect._core import Effect
|
|
22
|
+
from pyfect._fiber import Fiber
|
|
23
|
+
from pyfect._js import text_encode
|
|
24
|
+
from pyfect.cause import Cause
|
|
25
|
+
from pyfect.cluster._resource_ref import ResourceRef
|
|
26
|
+
from pyfect.cluster._shard_lock import effective_interval
|
|
27
|
+
from pyfect.cluster.cluster_error import PersistenceError
|
|
28
|
+
from pyfect.cluster.runner_storage import (
|
|
29
|
+
RunnerStorage,
|
|
30
|
+
RunnerStorageService,
|
|
31
|
+
make_encoded,
|
|
32
|
+
)
|
|
33
|
+
from pyfect.cluster.sharding_config import ShardingConfig, ShardingConfigService, shard_group_config
|
|
34
|
+
from pyfect.duration import Seconds
|
|
35
|
+
from pyfect.effect import gen, provide_service, suspend, void
|
|
36
|
+
from pyfect.exit import Exit
|
|
37
|
+
from pyfect.scope import CloseableScope, Scope, fork_in
|
|
38
|
+
from pyfect.sql import migrator
|
|
39
|
+
from pyfect.sql.client import SqlClient, SqlClientService
|
|
40
|
+
from pyfect.sql.connection import Connection
|
|
41
|
+
from pyfect.sql.error import SqlError
|
|
42
|
+
from pyfect.sql.statement import Fragment, Statement
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def postgres_lock_namespace(prefix: str) -> int:
|
|
46
|
+
"""The advisory-lock namespace of ``prefix``: FNV-1a of
|
|
47
|
+
``effect-cluster:<prefix>`` as a signed 32-bit integer. A persistent
|
|
48
|
+
wire format shared with Effect: it must never change."""
|
|
49
|
+
h = 0x811C9DC5
|
|
50
|
+
for byte in text_encode(f"effect-cluster:{prefix}"):
|
|
51
|
+
h = ((h ^ byte) * 0x01000193) & 0xFFFFFFFF
|
|
52
|
+
return h - 0x1_0000_0000 if h >= 0x8000_0000 else h
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
type _Held = tuple[Connection, int]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class _Storage:
|
|
59
|
+
"""The ``EncodedRunnerStorage`` of one client."""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
*,
|
|
64
|
+
sql: SqlClientService,
|
|
65
|
+
config: ShardingConfigService,
|
|
66
|
+
prefix: str,
|
|
67
|
+
layer_scope: CloseableScope,
|
|
68
|
+
lock_conn: ResourceRef[_Held] | None,
|
|
69
|
+
disable_advisory: bool,
|
|
70
|
+
) -> None:
|
|
71
|
+
self.sql = sql
|
|
72
|
+
self.prefix = prefix
|
|
73
|
+
self.layer_scope = layer_scope
|
|
74
|
+
self.lock_conn = lock_conn
|
|
75
|
+
self.disable_advisory = disable_advisory
|
|
76
|
+
self.deadline = effective_interval(config)
|
|
77
|
+
self.lock_namespace = postgres_lock_namespace(prefix)
|
|
78
|
+
# pg_locks shows the signed int4 key as an unsigned oid
|
|
79
|
+
self.lock_namespace_oid = self.lock_namespace & 0xFFFFFFFF
|
|
80
|
+
self.runners = sql.identifier(f"{prefix}_runners")
|
|
81
|
+
self.locks = sql.identifier(f"{prefix}_locks")
|
|
82
|
+
self.now: Fragment = sql.on_dialect_or_else(
|
|
83
|
+
pg=lambda: sql.literal("NOW()"),
|
|
84
|
+
mysql=lambda: sql.literal("NOW()"),
|
|
85
|
+
mssql=lambda: sql.literal("GETDATE()"),
|
|
86
|
+
or_else=lambda: sql.literal("CURRENT_TIMESTAMP"),
|
|
87
|
+
)
|
|
88
|
+
self.pg_lock_order = sql.literal('ORDER BY shard_id COLLATE "C"')
|
|
89
|
+
seconds = sql.unsafe(str(math.ceil(config.shard_lock_expiration)))
|
|
90
|
+
self.expires_seconds = seconds
|
|
91
|
+
now = self.now
|
|
92
|
+
self.lock_expires_at: Fragment = sql.on_dialect_or_else(
|
|
93
|
+
pg=lambda: sql("{} - INTERVAL '{} seconds'", now, seconds),
|
|
94
|
+
mysql=lambda: sql("DATE_SUB({}, INTERVAL {} SECOND)", now, seconds),
|
|
95
|
+
mssql=lambda: sql("DATEADD(SECOND, -{}, {})", seconds, now),
|
|
96
|
+
or_else=lambda: sql("datetime({}, '-{} seconds')", now, seconds),
|
|
97
|
+
)
|
|
98
|
+
self.lock_numbers: dict[str, int] = {}
|
|
99
|
+
self.shard_index: dict[str, int] = {}
|
|
100
|
+
self.lock_names: dict[str, str] = {}
|
|
101
|
+
index = 0
|
|
102
|
+
for i, group in enumerate(shard_group_config(config).available):
|
|
103
|
+
base = (i + 1) * 1_000_000
|
|
104
|
+
for shard in range(1, config.shards_per_group + 1):
|
|
105
|
+
shard_id = f"{group}:{shard}"
|
|
106
|
+
self.lock_numbers[shard_id] = base + shard
|
|
107
|
+
self.shard_index[shard_id] = index
|
|
108
|
+
self.lock_names[shard_id] = f"{prefix}.{shard_id}"
|
|
109
|
+
index += 1
|
|
110
|
+
|
|
111
|
+
# --- helpers ----------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
def bool_(self, value: bool) -> object:
|
|
114
|
+
return self.sql.on_dialect_or_else(
|
|
115
|
+
mssql=lambda: int(value), sqlite=lambda: int(value), or_else=lambda: value
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def wrap_string(self, text: str) -> str:
|
|
119
|
+
return self.sql.on_dialect_or_else(mssql=lambda: f"N'{text}'", or_else=lambda: f"'{text}'")
|
|
120
|
+
|
|
121
|
+
def string_literal(self, text: str) -> Statement[Any]:
|
|
122
|
+
return self.sql.unsafe(self.wrap_string(text))
|
|
123
|
+
|
|
124
|
+
def string_list(self, texts: Sequence[str]) -> Statement[Any]:
|
|
125
|
+
return self.sql.unsafe("(" + ",".join(self.wrap_string(t) for t in texts) + ")")
|
|
126
|
+
|
|
127
|
+
def with_deadline[A, E](self, operation: Effect[A, E]) -> Effect[A, E]:
|
|
128
|
+
"""``operation`` on a fiber of the layer's scope, joined with the
|
|
129
|
+
deadline: an operation stuck in an uninterruptible region (closing
|
|
130
|
+
an unresponsive connection) finishes detached."""
|
|
131
|
+
|
|
132
|
+
def body() -> Generator[Effect[Any, Any, Any], Any, A]:
|
|
133
|
+
fiber: Fiber[A, E] = yield from fork_in(
|
|
134
|
+
operation, self.layer_scope, start_immediately=True
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def stop() -> Effect[None]:
|
|
138
|
+
if fiber.poll_unsafe() is not None:
|
|
139
|
+
return void
|
|
140
|
+
return fork_in(
|
|
141
|
+
fiber.interrupt(), self.layer_scope, start_immediately=True
|
|
142
|
+
).as_void()
|
|
143
|
+
|
|
144
|
+
return (yield from fiber.join().timeout(self.deadline).ensuring(suspend(stop)))
|
|
145
|
+
|
|
146
|
+
return gen(body)
|
|
147
|
+
|
|
148
|
+
def rebuild_lock_conn(self, from_: _Held | None = None) -> Effect[None]:
|
|
149
|
+
rebuild = None if self.lock_conn is None else self.lock_conn.rebuild_unsafe(from_=from_)
|
|
150
|
+
if rebuild is None:
|
|
151
|
+
return void
|
|
152
|
+
# A stalled release must not block later rebuilds.
|
|
153
|
+
return fork_in(
|
|
154
|
+
self.with_deadline(rebuild).exit(), self.layer_scope, start_immediately=True
|
|
155
|
+
).as_void()
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def _rebuild_on_failure[A, E](
|
|
159
|
+
operation: Effect[A, E], rebuild: Callable[[], Effect[None]]
|
|
160
|
+
) -> Effect[A, E]:
|
|
161
|
+
"""``operation``, running ``rebuild`` when it fails. An interrupt
|
|
162
|
+
is not a broken connection: a rebuild on shutdown can lose its
|
|
163
|
+
``pg_advisory_unlock_all`` and pool a session holding the locks."""
|
|
164
|
+
|
|
165
|
+
def failed(cause: Cause[Any]) -> Effect[None]:
|
|
166
|
+
return void if cause.has_interrupts_only else rebuild()
|
|
167
|
+
|
|
168
|
+
return operation.on_error(failed)
|
|
169
|
+
|
|
170
|
+
def use_lock_conn[A, E](self, f: Callable[[Connection, int], Effect[A, E]]) -> Effect[A, E]:
|
|
171
|
+
"""``f`` on the reserved connection (PostgreSQL and MySQL, which
|
|
172
|
+
always have one); a failure replaces it."""
|
|
173
|
+
lock_conn = cast("ResourceRef[_Held]", self.lock_conn)
|
|
174
|
+
|
|
175
|
+
def run(held: _Held) -> Effect[A, E]:
|
|
176
|
+
return self._rebuild_on_failure(
|
|
177
|
+
f(held[0], held[1]), lambda: self.rebuild_lock_conn(held)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
return lock_conn.await_.flat_map(run)
|
|
181
|
+
|
|
182
|
+
def exec(self, statement: Statement[Any]) -> Effect[object, SqlError]:
|
|
183
|
+
if self.lock_conn is None:
|
|
184
|
+
return statement.raw
|
|
185
|
+
query, params = statement.compile()
|
|
186
|
+
return self.use_lock_conn(lambda conn, _pid: conn.execute_raw(query, params))
|
|
187
|
+
|
|
188
|
+
def exec_values(
|
|
189
|
+
self, statement: Statement[Any]
|
|
190
|
+
) -> Effect[Sequence[tuple[object, ...]], SqlError]:
|
|
191
|
+
if self.lock_conn is None:
|
|
192
|
+
return statement.values
|
|
193
|
+
query, params = statement.compile()
|
|
194
|
+
return self.use_lock_conn(lambda conn, _pid: conn.execute_values(query, params))
|
|
195
|
+
|
|
196
|
+
def exec_unprepared(self, statement: Statement[Any]) -> Effect[Sequence[Any], SqlError]:
|
|
197
|
+
if self.lock_conn is None:
|
|
198
|
+
return statement.values_unprepared
|
|
199
|
+
query, params = statement.compile()
|
|
200
|
+
return self.use_lock_conn(lambda conn, _pid: conn.execute_unprepared(query, params, None))
|
|
201
|
+
|
|
202
|
+
def with_lock_operation_deadline[A, E](self, operation: Effect[A, E]) -> Effect[A, E]:
|
|
203
|
+
"""``operation`` with the deadline; on failure the connection ready
|
|
204
|
+
at entry is replaced (or a failed rebuild retried)."""
|
|
205
|
+
|
|
206
|
+
def run() -> Effect[A, E]:
|
|
207
|
+
ready = None if self.lock_conn is None else self.lock_conn.get_unsafe()
|
|
208
|
+
|
|
209
|
+
def rebuild() -> Effect[None]:
|
|
210
|
+
if ready is not None:
|
|
211
|
+
return self.rebuild_lock_conn(ready)
|
|
212
|
+
if self.lock_conn is not None and self.lock_conn.get_unsafe() is None:
|
|
213
|
+
return self.rebuild_lock_conn()
|
|
214
|
+
return void
|
|
215
|
+
|
|
216
|
+
return self._rebuild_on_failure(self.with_deadline(operation), rebuild)
|
|
217
|
+
|
|
218
|
+
return suspend(run)
|
|
219
|
+
|
|
220
|
+
# --- statements -------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
def lock_values(self, address: str, shard_ids: Sequence[str]) -> Fragment:
|
|
223
|
+
sql = self.sql
|
|
224
|
+
return sql.csv(
|
|
225
|
+
[
|
|
226
|
+
sql("({}, {}, {})", self.string_literal(s), self.string_literal(address), self.now)
|
|
227
|
+
for s in shard_ids
|
|
228
|
+
]
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
def acquired_locks(self, address: str, shard_ids: Sequence[str]) -> Effect[list[str], SqlError]:
|
|
232
|
+
statement = self.sql(
|
|
233
|
+
"SELECT shard_id FROM {} WHERE address = {} AND acquired_at >= {} AND shard_id IN {}",
|
|
234
|
+
self.locks,
|
|
235
|
+
address,
|
|
236
|
+
self.lock_expires_at,
|
|
237
|
+
self.string_list(shard_ids),
|
|
238
|
+
)
|
|
239
|
+
return self.exec_values(statement).map(lambda rows: [str(r[0]) for r in rows])
|
|
240
|
+
|
|
241
|
+
def acquire_lock(self, address: str, shard_ids: Sequence[str]) -> Effect[list[str], SqlError]:
|
|
242
|
+
sql = self.sql
|
|
243
|
+
if sql.dialect == "pg":
|
|
244
|
+
if self.disable_advisory:
|
|
245
|
+
statement = sql(
|
|
246
|
+
"INSERT INTO {} (shard_id, address, acquired_at) "
|
|
247
|
+
"SELECT * FROM (VALUES {}) AS requested (shard_id, address, acquired_at) {} "
|
|
248
|
+
"ON CONFLICT (shard_id) DO UPDATE SET address = {}, acquired_at = {} "
|
|
249
|
+
"WHERE {}.address = {} OR {}.acquired_at < {}",
|
|
250
|
+
self.locks,
|
|
251
|
+
self.lock_values(address, shard_ids),
|
|
252
|
+
self.pg_lock_order,
|
|
253
|
+
address,
|
|
254
|
+
self.now,
|
|
255
|
+
self.locks,
|
|
256
|
+
address,
|
|
257
|
+
self.locks,
|
|
258
|
+
self.lock_expires_at,
|
|
259
|
+
)
|
|
260
|
+
return self.exec(statement).flat_map(
|
|
261
|
+
lambda _: self.acquired_locks(address, shard_ids)
|
|
262
|
+
)
|
|
263
|
+
return self.use_lock_conn(lambda conn, pid: self._pg_try_locks(conn, pid, shard_ids))
|
|
264
|
+
if sql.dialect == "mysql":
|
|
265
|
+
if self.disable_advisory:
|
|
266
|
+
statement = sql(
|
|
267
|
+
"INSERT INTO {} (shard_id, address, acquired_at) VALUES {} "
|
|
268
|
+
"ON DUPLICATE KEY UPDATE "
|
|
269
|
+
"address = IF(address = VALUES(address) OR acquired_at < {}, "
|
|
270
|
+
"VALUES(address), address), "
|
|
271
|
+
"acquired_at = IF(address = VALUES(address) OR acquired_at < {}, "
|
|
272
|
+
"VALUES(acquired_at), acquired_at)",
|
|
273
|
+
self.locks,
|
|
274
|
+
self.lock_values(address, shard_ids),
|
|
275
|
+
self.lock_expires_at,
|
|
276
|
+
self.lock_expires_at,
|
|
277
|
+
)
|
|
278
|
+
return self.exec_unprepared(statement).flat_map(
|
|
279
|
+
lambda _: self.acquired_locks(address, shard_ids)
|
|
280
|
+
)
|
|
281
|
+
return self.use_lock_conn(lambda conn, pid: self._mysql_get_locks(conn, pid, shard_ids))
|
|
282
|
+
if sql.dialect == "mssql":
|
|
283
|
+
statement = sql(
|
|
284
|
+
"MERGE {} WITH (HOLDLOCK) AS target "
|
|
285
|
+
"USING (SELECT * FROM (VALUES {})) AS source (shard_id, address, acquired_at) "
|
|
286
|
+
"ON target.shard_id = source.shard_id "
|
|
287
|
+
"WHEN MATCHED AND (target.address = source.address OR "
|
|
288
|
+
"DATEDIFF(SECOND, target.acquired_at, {}) > {}) THEN "
|
|
289
|
+
"UPDATE SET address = source.address, acquired_at = source.acquired_at "
|
|
290
|
+
"WHEN NOT MATCHED THEN INSERT (shard_id, address, acquired_at) "
|
|
291
|
+
"VALUES (source.shard_id, source.address, source.acquired_at);",
|
|
292
|
+
self.locks,
|
|
293
|
+
self.lock_values(address, shard_ids),
|
|
294
|
+
self.now,
|
|
295
|
+
self.expires_seconds,
|
|
296
|
+
)
|
|
297
|
+
return sql.with_transaction(
|
|
298
|
+
statement.flat_map(lambda _: self.acquired_locks(address, shard_ids))
|
|
299
|
+
)
|
|
300
|
+
statement = sql(
|
|
301
|
+
"WITH source(shard_id, address, acquired_at) AS (VALUES {}) "
|
|
302
|
+
"INSERT INTO {} (shard_id, address, acquired_at) "
|
|
303
|
+
"SELECT source.shard_id, source.address, source.acquired_at FROM source "
|
|
304
|
+
"WHERE NOT EXISTS (SELECT 1 FROM {} WHERE shard_id = source.shard_id "
|
|
305
|
+
"AND address != {} "
|
|
306
|
+
"AND (strftime('%s', {}) - strftime('%s', acquired_at)) <= {}) "
|
|
307
|
+
"ON CONFLICT(shard_id) DO UPDATE SET address = {}, acquired_at = {}",
|
|
308
|
+
self.lock_values(address, shard_ids),
|
|
309
|
+
self.locks,
|
|
310
|
+
self.locks,
|
|
311
|
+
address,
|
|
312
|
+
self.now,
|
|
313
|
+
self.expires_seconds,
|
|
314
|
+
address,
|
|
315
|
+
self.now,
|
|
316
|
+
)
|
|
317
|
+
return sql.with_transaction(
|
|
318
|
+
statement.flat_map(lambda _: self.acquired_locks(address, shard_ids))
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
def _pg_try_locks(
|
|
322
|
+
self, conn: Connection, pid: int, shard_ids: Sequence[str]
|
|
323
|
+
) -> Effect[list[str], SqlError]:
|
|
324
|
+
def body() -> Generator[Effect[Any, Any, Any], Any, list[str]]:
|
|
325
|
+
acquired: list[str] = []
|
|
326
|
+
to_acquire = {self.lock_numbers[s]: s for s in shard_ids}
|
|
327
|
+
taken = yield from conn.execute_values(
|
|
328
|
+
"SELECT objid FROM pg_locks WHERE locktype = 'advisory' AND granted = true "
|
|
329
|
+
f"AND classid = {self.lock_namespace_oid} AND objsubid = 2 AND pid = {pid} "
|
|
330
|
+
"ORDER BY objid",
|
|
331
|
+
[],
|
|
332
|
+
)
|
|
333
|
+
for lock_number, *_rest in taken:
|
|
334
|
+
shard_id = to_acquire.pop(int(str(lock_number)), None)
|
|
335
|
+
if shard_id is not None:
|
|
336
|
+
acquired.append(shard_id)
|
|
337
|
+
if not to_acquire:
|
|
338
|
+
return acquired
|
|
339
|
+
locks = ", ".join(
|
|
340
|
+
f'pg_try_advisory_lock({self.lock_namespace}, {number}) AS "{shard_id}"'
|
|
341
|
+
for number, shard_id in to_acquire.items()
|
|
342
|
+
)
|
|
343
|
+
rows = yield from conn.execute_unprepared(f"SELECT {locks}", [], None)
|
|
344
|
+
for shard_id, got in rows[0].items():
|
|
345
|
+
if got:
|
|
346
|
+
acquired.append(shard_id)
|
|
347
|
+
return acquired
|
|
348
|
+
|
|
349
|
+
return gen(body)
|
|
350
|
+
|
|
351
|
+
def _mysql_get_locks(
|
|
352
|
+
self, conn: Connection, pid: int, shard_ids: Sequence[str]
|
|
353
|
+
) -> Effect[list[str], SqlError]:
|
|
354
|
+
def body() -> Generator[Effect[Any, Any, Any], Any, list[str]]:
|
|
355
|
+
all_taken = ", ".join(
|
|
356
|
+
f"IS_USED_LOCK('{name}') AS \"{shard_id}\""
|
|
357
|
+
for shard_id, name in self.lock_names.items()
|
|
358
|
+
)
|
|
359
|
+
taken = (yield from conn.execute_values(f"SELECT {all_taken}", []))[0]
|
|
360
|
+
acquired: list[str] = []
|
|
361
|
+
to_acquire: list[str] = []
|
|
362
|
+
for shard_id in shard_ids:
|
|
363
|
+
if taken[self.shard_index[shard_id]] == pid:
|
|
364
|
+
acquired.append(shard_id)
|
|
365
|
+
else:
|
|
366
|
+
to_acquire.append(shard_id)
|
|
367
|
+
if not to_acquire:
|
|
368
|
+
return acquired
|
|
369
|
+
locks = ", ".join(f"GET_LOCK('{self.lock_names[s]}', 0) AS \"{s}\"" for s in to_acquire)
|
|
370
|
+
results = (yield from conn.execute_values(f"SELECT {locks}", []))[0]
|
|
371
|
+
for shard_id, result in zip(to_acquire, results, strict=True):
|
|
372
|
+
if result == 1:
|
|
373
|
+
acquired.append(shard_id)
|
|
374
|
+
return acquired
|
|
375
|
+
|
|
376
|
+
return gen(body)
|
|
377
|
+
|
|
378
|
+
def refresh_shards(self, address: str, shard_ids: Sequence[str]) -> Effect[list[str], SqlError]:
|
|
379
|
+
sql = self.sql
|
|
380
|
+
shards = self.string_list(shard_ids)
|
|
381
|
+
|
|
382
|
+
def first_column(rows: Sequence[Any]) -> list[str]:
|
|
383
|
+
return [str(r[0]) for r in rows]
|
|
384
|
+
|
|
385
|
+
if sql.dialect == "pg":
|
|
386
|
+
if not self.disable_advisory:
|
|
387
|
+
return self.acquire_lock(address, shard_ids)
|
|
388
|
+
statement = sql(
|
|
389
|
+
"WITH locked AS (SELECT shard_id FROM {} WHERE address = {} AND shard_id IN {} "
|
|
390
|
+
"{} FOR UPDATE) "
|
|
391
|
+
"UPDATE {} SET acquired_at = {} "
|
|
392
|
+
"WHERE address = {} AND shard_id IN (SELECT shard_id FROM locked) "
|
|
393
|
+
"RETURNING shard_id",
|
|
394
|
+
self.locks,
|
|
395
|
+
address,
|
|
396
|
+
shards,
|
|
397
|
+
self.pg_lock_order,
|
|
398
|
+
self.locks,
|
|
399
|
+
self.now,
|
|
400
|
+
address,
|
|
401
|
+
)
|
|
402
|
+
return self.exec_values(statement).map(first_column)
|
|
403
|
+
if sql.dialect == "mysql":
|
|
404
|
+
if not self.disable_advisory:
|
|
405
|
+
return self.acquire_lock(address, shard_ids)
|
|
406
|
+
statement = sql(
|
|
407
|
+
"UPDATE {} SET acquired_at = {} WHERE address = {} AND shard_id IN {}; "
|
|
408
|
+
"SELECT shard_id FROM {} WHERE address = {} AND shard_id IN {}",
|
|
409
|
+
self.locks,
|
|
410
|
+
self.now,
|
|
411
|
+
address,
|
|
412
|
+
shards,
|
|
413
|
+
self.locks,
|
|
414
|
+
address,
|
|
415
|
+
shards,
|
|
416
|
+
)
|
|
417
|
+
return self.exec_unprepared(statement).map(
|
|
418
|
+
lambda results: [str(row["shard_id"]) for row in results[1]]
|
|
419
|
+
)
|
|
420
|
+
if sql.dialect == "mssql":
|
|
421
|
+
statement = sql(
|
|
422
|
+
"UPDATE {} SET acquired_at = {} OUTPUT inserted.shard_id "
|
|
423
|
+
"WHERE address = {} AND shard_id IN {}",
|
|
424
|
+
self.locks,
|
|
425
|
+
self.now,
|
|
426
|
+
address,
|
|
427
|
+
shards,
|
|
428
|
+
)
|
|
429
|
+
return self.exec_values(statement).map(first_column)
|
|
430
|
+
statement = sql(
|
|
431
|
+
"UPDATE {} SET acquired_at = {} WHERE address = {} AND shard_id IN {} "
|
|
432
|
+
"RETURNING shard_id",
|
|
433
|
+
self.locks,
|
|
434
|
+
self.now,
|
|
435
|
+
address,
|
|
436
|
+
shards,
|
|
437
|
+
)
|
|
438
|
+
return self.exec_values(statement).map(first_column)
|
|
439
|
+
|
|
440
|
+
def release_shard(self, address: str, shard_id: str) -> Effect[object, SqlError]:
|
|
441
|
+
sql = self.sql
|
|
442
|
+
delete = sql(
|
|
443
|
+
"DELETE FROM {} WHERE address = {} AND shard_id = {}", self.locks, address, shard_id
|
|
444
|
+
)
|
|
445
|
+
if sql.dialect in ("pg", "mysql") and self.disable_advisory:
|
|
446
|
+
return self.exec(delete)
|
|
447
|
+
if sql.dialect == "pg":
|
|
448
|
+
number = self.lock_numbers[shard_id]
|
|
449
|
+
|
|
450
|
+
def pg(conn: Connection, _pid: int) -> Effect[None, SqlError]:
|
|
451
|
+
def body() -> Generator[Effect[Any, Any, Any], Any]:
|
|
452
|
+
for _ in range(5):
|
|
453
|
+
yield from conn.execute_raw(
|
|
454
|
+
f"SELECT pg_advisory_unlock({self.lock_namespace}, {number})", []
|
|
455
|
+
)
|
|
456
|
+
taken = yield from conn.execute_values(
|
|
457
|
+
"SELECT 1 FROM pg_locks WHERE locktype = 'advisory' AND granted = true "
|
|
458
|
+
f"AND classid = {self.lock_namespace_oid} AND objid = {number} "
|
|
459
|
+
"AND objsubid = 2 AND pid = pg_backend_pid()",
|
|
460
|
+
[],
|
|
461
|
+
)
|
|
462
|
+
if not taken:
|
|
463
|
+
return
|
|
464
|
+
yield from conn.execute_raw("SELECT pg_advisory_unlock_all()", [])
|
|
465
|
+
|
|
466
|
+
return gen(body)
|
|
467
|
+
|
|
468
|
+
return self.use_lock_conn(pg)
|
|
469
|
+
if sql.dialect == "mysql":
|
|
470
|
+
name = self.lock_names[shard_id]
|
|
471
|
+
|
|
472
|
+
def mysql(conn: Connection, pid: int) -> Effect[None, SqlError]:
|
|
473
|
+
def body() -> Generator[Effect[Any, Any, Any], Any]:
|
|
474
|
+
while True:
|
|
475
|
+
yield from conn.execute_raw(f"SELECT RELEASE_LOCK('{name}')", [])
|
|
476
|
+
taken = yield from conn.execute_values(f"SELECT IS_USED_LOCK('{name}')", [])
|
|
477
|
+
if not taken or taken[0][0] != pid:
|
|
478
|
+
return
|
|
479
|
+
|
|
480
|
+
return gen(body)
|
|
481
|
+
|
|
482
|
+
return self.use_lock_conn(mysql)
|
|
483
|
+
return delete
|
|
484
|
+
|
|
485
|
+
def release_all_shards(self, address: str) -> Effect[object, SqlError]:
|
|
486
|
+
sql = self.sql
|
|
487
|
+
if sql.dialect == "pg":
|
|
488
|
+
if self.disable_advisory:
|
|
489
|
+
return self.exec(
|
|
490
|
+
sql(
|
|
491
|
+
"WITH locked AS (SELECT shard_id FROM {} WHERE address = {} {} FOR UPDATE) "
|
|
492
|
+
"DELETE FROM {} WHERE address = {} "
|
|
493
|
+
"AND shard_id IN (SELECT shard_id FROM locked)",
|
|
494
|
+
self.locks,
|
|
495
|
+
address,
|
|
496
|
+
self.pg_lock_order,
|
|
497
|
+
self.locks,
|
|
498
|
+
address,
|
|
499
|
+
)
|
|
500
|
+
)
|
|
501
|
+
return self.exec(sql("SELECT pg_advisory_unlock_all()"))
|
|
502
|
+
if sql.dialect == "mysql":
|
|
503
|
+
if self.disable_advisory:
|
|
504
|
+
return self.exec(sql("DELETE FROM {} WHERE address = {}", self.locks, address))
|
|
505
|
+
return self.exec(sql("SELECT RELEASE_ALL_LOCKS()"))
|
|
506
|
+
return sql("DELETE FROM {} WHERE address = {}", self.locks, address)
|
|
507
|
+
|
|
508
|
+
def insert_runner(
|
|
509
|
+
self, address: str, runner: str, healthy: bool
|
|
510
|
+
) -> Effect[Sequence[Any], SqlError]:
|
|
511
|
+
sql = self.sql
|
|
512
|
+
if sql.dialect == "mssql":
|
|
513
|
+
return sql(
|
|
514
|
+
"MERGE {} AS target USING (SELECT {} AS address, {} AS runner, "
|
|
515
|
+
"{} AS last_heartbeat, {} AS healthy) AS source ON target.address = source.address "
|
|
516
|
+
"WHEN MATCHED THEN UPDATE SET runner = source.runner, "
|
|
517
|
+
"last_heartbeat = source.last_heartbeat, healthy = source.healthy "
|
|
518
|
+
"WHEN NOT MATCHED THEN INSERT (address, runner, last_heartbeat, healthy) "
|
|
519
|
+
"VALUES (source.address, source.runner, source.last_heartbeat, source.healthy) "
|
|
520
|
+
"OUTPUT INSERTED.machine_id;",
|
|
521
|
+
self.runners,
|
|
522
|
+
address,
|
|
523
|
+
runner,
|
|
524
|
+
self.now,
|
|
525
|
+
self.bool_(healthy),
|
|
526
|
+
).values
|
|
527
|
+
if sql.dialect == "mysql":
|
|
528
|
+
return sql(
|
|
529
|
+
"INSERT INTO {} (address, runner, last_heartbeat, healthy) VALUES ({}, {}, {}, {}) "
|
|
530
|
+
"ON DUPLICATE KEY UPDATE runner = VALUES(runner), "
|
|
531
|
+
"last_heartbeat = VALUES(last_heartbeat), healthy = VALUES(healthy); "
|
|
532
|
+
"SELECT machine_id FROM {} WHERE address = {};",
|
|
533
|
+
self.runners,
|
|
534
|
+
address,
|
|
535
|
+
runner,
|
|
536
|
+
self.now,
|
|
537
|
+
healthy,
|
|
538
|
+
self.runners,
|
|
539
|
+
address,
|
|
540
|
+
).unprepared.map(_mysql_machine_id)
|
|
541
|
+
if sql.dialect == "pg":
|
|
542
|
+
return sql(
|
|
543
|
+
"INSERT INTO {} (address, runner, last_heartbeat, healthy) VALUES ({}, {}, {}, {}) "
|
|
544
|
+
"ON CONFLICT (address) DO UPDATE SET runner = EXCLUDED.runner, "
|
|
545
|
+
"last_heartbeat = EXCLUDED.last_heartbeat, healthy = EXCLUDED.healthy "
|
|
546
|
+
"RETURNING machine_id",
|
|
547
|
+
self.runners,
|
|
548
|
+
address,
|
|
549
|
+
runner,
|
|
550
|
+
self.now,
|
|
551
|
+
healthy,
|
|
552
|
+
).values
|
|
553
|
+
return sql(
|
|
554
|
+
"INSERT INTO {} (address, runner, last_heartbeat, healthy) VALUES ({}, {}, {}, {}) "
|
|
555
|
+
"ON CONFLICT(address) DO UPDATE SET runner = excluded.runner, "
|
|
556
|
+
"last_heartbeat = excluded.last_heartbeat, healthy = excluded.healthy "
|
|
557
|
+
"RETURNING machine_id;",
|
|
558
|
+
self.runners,
|
|
559
|
+
address,
|
|
560
|
+
runner,
|
|
561
|
+
self.now,
|
|
562
|
+
self.bool_(healthy),
|
|
563
|
+
).values
|
|
564
|
+
|
|
565
|
+
# --- EncodedRunnerStorage ---------------------------------------------------------
|
|
566
|
+
|
|
567
|
+
@property
|
|
568
|
+
def get_runners(self) -> Effect[list[tuple[str, bool]], PersistenceError]:
|
|
569
|
+
statement = self.sql(
|
|
570
|
+
"SELECT runner, healthy FROM {} WHERE last_heartbeat > {}",
|
|
571
|
+
self.runners,
|
|
572
|
+
self.lock_expires_at,
|
|
573
|
+
)
|
|
574
|
+
return PersistenceError.refail(
|
|
575
|
+
statement.values.map(lambda rows: [(str(r[0]), bool(r[1])) for r in rows])
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
def register(
|
|
579
|
+
self, address: str, runner: str, healthy: bool, /
|
|
580
|
+
) -> Effect[int, PersistenceError]:
|
|
581
|
+
return PersistenceError.refail(
|
|
582
|
+
self.insert_runner(address, runner, healthy).map(lambda rows: int(rows[0][0]))
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
def unregister(self, address: str, /) -> Effect[None, PersistenceError]:
|
|
586
|
+
statement = self.sql(
|
|
587
|
+
"DELETE FROM {} WHERE address = {} OR last_heartbeat < {}",
|
|
588
|
+
self.runners,
|
|
589
|
+
address,
|
|
590
|
+
self.lock_expires_at,
|
|
591
|
+
)
|
|
592
|
+
return PersistenceError.refail(statement.as_void())
|
|
593
|
+
|
|
594
|
+
def set_runner_health(self, address: str, healthy: bool, /) -> Effect[None, PersistenceError]:
|
|
595
|
+
statement = self.sql(
|
|
596
|
+
"UPDATE {} SET healthy = {} WHERE address = {}",
|
|
597
|
+
self.runners,
|
|
598
|
+
self.bool_(healthy),
|
|
599
|
+
address,
|
|
600
|
+
)
|
|
601
|
+
return PersistenceError.refail(statement.as_void())
|
|
602
|
+
|
|
603
|
+
def acquire(
|
|
604
|
+
self, address: str, shard_ids: Sequence[str], /
|
|
605
|
+
) -> Effect[list[str], PersistenceError]:
|
|
606
|
+
return PersistenceError.refail(
|
|
607
|
+
self.with_lock_operation_deadline(self.acquire_lock(address, shard_ids))
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
def refresh(
|
|
611
|
+
self, address: str, shard_ids: Sequence[str], /
|
|
612
|
+
) -> Effect[list[str], PersistenceError]:
|
|
613
|
+
heartbeat = self.sql(
|
|
614
|
+
"UPDATE {} SET last_heartbeat = {} WHERE address = {}", self.runners, self.now, address
|
|
615
|
+
)
|
|
616
|
+
if not shard_ids:
|
|
617
|
+
# The liveness probe while lock storage is unhealthy: on the
|
|
618
|
+
# shared pool, so a wedged reserved connection cannot block it.
|
|
619
|
+
return PersistenceError.refail(self.with_deadline(heartbeat).as_([]))
|
|
620
|
+
return PersistenceError.refail(
|
|
621
|
+
self.with_lock_operation_deadline(
|
|
622
|
+
self.exec(heartbeat).flat_map(lambda _: self.refresh_shards(address, shard_ids))
|
|
623
|
+
)
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
def release(self, address: str, shard_id: str, /) -> Effect[None, PersistenceError]:
|
|
627
|
+
return PersistenceError.refail(
|
|
628
|
+
self.with_lock_operation_deadline(self.release_shard(address, shard_id)).as_void()
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
def release_all(self, address: str, /) -> Effect[None, PersistenceError]:
|
|
632
|
+
return PersistenceError.refail(
|
|
633
|
+
self.with_lock_operation_deadline(self.release_all_shards(address)).as_void()
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _mysql_machine_id(results: Sequence[Any]) -> list[list[object]]:
|
|
638
|
+
"""The machine id of a MySQL multi-statement upsert's second result."""
|
|
639
|
+
return [[results[1][0]["machine_id"]]]
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _acquire_lock_conn(
|
|
643
|
+
sql: SqlClientService, disable_advisory: bool, deadline: Seconds
|
|
644
|
+
) -> Callable[[CloseableScope], Effect[_Held]] | None:
|
|
645
|
+
"""How to reserve the lock connection (PostgreSQL and MySQL only)."""
|
|
646
|
+
|
|
647
|
+
def unlock_on_close(scope: CloseableScope, conn: Connection, query: str) -> Effect[None]:
|
|
648
|
+
def finalizer(_: Exit[Any, Any]) -> Effect[None]:
|
|
649
|
+
return conn.execute_raw(query, []).timeout(deadline).interruptible().ignore_cause()
|
|
650
|
+
|
|
651
|
+
return scope.add_finalizer(finalizer)
|
|
652
|
+
|
|
653
|
+
def reserve(scope: CloseableScope) -> Effect[Connection]:
|
|
654
|
+
return sql.reserve.or_die().pipe(provide_service(Scope, scope))
|
|
655
|
+
|
|
656
|
+
def pg(scope: CloseableScope) -> Effect[_Held]:
|
|
657
|
+
def body() -> Generator[Effect[Any, Any, Any], Any, _Held]:
|
|
658
|
+
conn = yield from reserve(scope)
|
|
659
|
+
if disable_advisory:
|
|
660
|
+
return (conn, 0)
|
|
661
|
+
pid = (yield from conn.execute_values("SELECT pg_backend_pid()", []))[0][0]
|
|
662
|
+
yield from unlock_on_close(scope, conn, "SELECT pg_advisory_unlock_all()")
|
|
663
|
+
return (conn, int(str(pid)))
|
|
664
|
+
|
|
665
|
+
return gen(body).or_die()
|
|
666
|
+
|
|
667
|
+
def mysql(scope: CloseableScope) -> Effect[_Held]:
|
|
668
|
+
def body() -> Generator[Effect[Any, Any, Any], Any, _Held]:
|
|
669
|
+
conn = yield from reserve(scope)
|
|
670
|
+
if disable_advisory:
|
|
671
|
+
return (conn, 0)
|
|
672
|
+
# IS_USED_LOCK names the connection id, also under Vitess
|
|
673
|
+
pid: int | None = None
|
|
674
|
+
while pid is None:
|
|
675
|
+
name = f"cluster:pid:{random.randrange(2**53)}"
|
|
676
|
+
taken = (
|
|
677
|
+
yield from conn.execute_values(
|
|
678
|
+
f"SELECT GET_LOCK('{name}', 10), IS_USED_LOCK('{name}')", []
|
|
679
|
+
)
|
|
680
|
+
)[0]
|
|
681
|
+
if taken[0] is None:
|
|
682
|
+
continue
|
|
683
|
+
pid = int(str(taken[1]))
|
|
684
|
+
yield from unlock_on_close(scope, conn, "SELECT RELEASE_ALL_LOCKS()")
|
|
685
|
+
return (conn, pid)
|
|
686
|
+
|
|
687
|
+
return gen(body).or_die()
|
|
688
|
+
|
|
689
|
+
return sql.on_dialect_or_else(pg=lambda: pg, mysql=lambda: mysql, or_else=lambda: None)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def make_storage(
|
|
693
|
+
*, prefix: str = "cluster"
|
|
694
|
+
) -> Effect[RunnerStorageService, Never, SqlClient | ShardingConfig | Scope]:
|
|
695
|
+
"""Runner storage on the tables of ``prefix`` (already migrated)."""
|
|
696
|
+
|
|
697
|
+
def body() -> Generator[Effect[Any, Any, Any], Any, RunnerStorageService]:
|
|
698
|
+
config = yield from ShardingConfig()
|
|
699
|
+
sql = (yield from SqlClient()).without_transforms()
|
|
700
|
+
layer_scope = yield from Scope()
|
|
701
|
+
# PGlite runs every statement on one session: it gets table leases
|
|
702
|
+
# on the shared client.
|
|
703
|
+
single_session = sql.driver == "pglite"
|
|
704
|
+
disable_advisory = config.shard_lock_disable_advisory or single_session
|
|
705
|
+
acquire = (
|
|
706
|
+
None
|
|
707
|
+
if single_session
|
|
708
|
+
else _acquire_lock_conn(sql, disable_advisory, effective_interval(config))
|
|
709
|
+
)
|
|
710
|
+
lock_conn = None
|
|
711
|
+
if acquire is not None:
|
|
712
|
+
lock_conn = yield from ResourceRef.make(layer_scope, acquire)
|
|
713
|
+
storage = _Storage(
|
|
714
|
+
sql=sql,
|
|
715
|
+
config=config,
|
|
716
|
+
prefix=prefix,
|
|
717
|
+
layer_scope=layer_scope,
|
|
718
|
+
lock_conn=lock_conn,
|
|
719
|
+
disable_advisory=disable_advisory,
|
|
720
|
+
)
|
|
721
|
+
return make_encoded(storage)
|
|
722
|
+
|
|
723
|
+
return gen(body).with_tracer_enabled(False)
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def migrations(*, prefix: str = "cluster") -> migrator.Loader:
|
|
727
|
+
"""The runner tables' migrations (existing deployments may have the
|
|
728
|
+
tables without a migration history: they are created if absent)."""
|
|
729
|
+
runners_table = f"{prefix}_runners"
|
|
730
|
+
locks_table = f"{prefix}_locks"
|
|
731
|
+
|
|
732
|
+
def create() -> Generator[Effect[Any, Any, Any], Any]:
|
|
733
|
+
sql = (yield from SqlClient()).without_transforms()
|
|
734
|
+
runners = sql.identifier(runners_table)
|
|
735
|
+
locks = sql.identifier(locks_table)
|
|
736
|
+
yield from sql.on_dialect_or_else(
|
|
737
|
+
mssql=lambda: sql(
|
|
738
|
+
"IF OBJECT_ID(N'{}', N'U') IS NULL CREATE TABLE {} ("
|
|
739
|
+
"machine_id INT IDENTITY PRIMARY KEY, address VARCHAR(255) NOT NULL, "
|
|
740
|
+
"runner TEXT NOT NULL, healthy BIT NOT NULL DEFAULT 1, "
|
|
741
|
+
"last_heartbeat DATETIME NOT NULL DEFAULT GETDATE(), UNIQUE(address))",
|
|
742
|
+
runners,
|
|
743
|
+
runners,
|
|
744
|
+
),
|
|
745
|
+
mysql=lambda: sql(
|
|
746
|
+
"CREATE TABLE IF NOT EXISTS {} ("
|
|
747
|
+
"machine_id INT AUTO_INCREMENT PRIMARY KEY, address VARCHAR(255) NOT NULL, "
|
|
748
|
+
"runner TEXT NOT NULL, healthy BOOLEAN NOT NULL DEFAULT TRUE, "
|
|
749
|
+
"last_heartbeat DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(address))",
|
|
750
|
+
runners,
|
|
751
|
+
),
|
|
752
|
+
pg=lambda: sql(
|
|
753
|
+
"CREATE TABLE IF NOT EXISTS {} ("
|
|
754
|
+
"machine_id SERIAL PRIMARY KEY, address VARCHAR(255) NOT NULL, "
|
|
755
|
+
"runner TEXT NOT NULL, healthy BOOLEAN NOT NULL DEFAULT TRUE, "
|
|
756
|
+
"last_heartbeat TIMESTAMP NOT NULL DEFAULT NOW(), UNIQUE(address))",
|
|
757
|
+
runners,
|
|
758
|
+
),
|
|
759
|
+
or_else=lambda: sql(
|
|
760
|
+
"CREATE TABLE IF NOT EXISTS {} ("
|
|
761
|
+
"machine_id INTEGER PRIMARY KEY AUTOINCREMENT, address TEXT NOT NULL, "
|
|
762
|
+
"runner TEXT NOT NULL, healthy INTEGER NOT NULL DEFAULT 1, "
|
|
763
|
+
"last_heartbeat DATETIME NOT NULL DEFAULT (CURRENT_TIMESTAMP), UNIQUE(address))",
|
|
764
|
+
runners,
|
|
765
|
+
),
|
|
766
|
+
)
|
|
767
|
+
yield from sql.on_dialect_or_else(
|
|
768
|
+
mssql=lambda: sql(
|
|
769
|
+
"IF OBJECT_ID(N'{}', N'U') IS NULL CREATE TABLE {} ("
|
|
770
|
+
"shard_id VARCHAR(50) PRIMARY KEY, address VARCHAR(255) NOT NULL, "
|
|
771
|
+
"acquired_at DATETIME NOT NULL)",
|
|
772
|
+
locks,
|
|
773
|
+
locks,
|
|
774
|
+
),
|
|
775
|
+
mysql=lambda: sql(
|
|
776
|
+
"CREATE TABLE IF NOT EXISTS {} (shard_id VARCHAR(50) PRIMARY KEY, "
|
|
777
|
+
"address VARCHAR(255) NOT NULL, acquired_at DATETIME NOT NULL)",
|
|
778
|
+
locks,
|
|
779
|
+
),
|
|
780
|
+
pg=lambda: sql(
|
|
781
|
+
"CREATE TABLE IF NOT EXISTS {} (shard_id VARCHAR(50) PRIMARY KEY, "
|
|
782
|
+
"address VARCHAR(255) NOT NULL, acquired_at TIMESTAMP NOT NULL)",
|
|
783
|
+
locks,
|
|
784
|
+
),
|
|
785
|
+
or_else=lambda: sql(
|
|
786
|
+
"CREATE TABLE IF NOT EXISTS {} (shard_id TEXT PRIMARY KEY, "
|
|
787
|
+
"address TEXT NOT NULL, acquired_at DATETIME NOT NULL)",
|
|
788
|
+
locks,
|
|
789
|
+
),
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
return migrator.from_record({"0001_create_tables": gen(create)})
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def run_migrations(*, prefix: str = "cluster") -> Effect[None, Never, SqlClient]:
|
|
796
|
+
"""Run the runner migrations; a failure is a defect. Message and runner
|
|
797
|
+
migration ids overlap, so they keep separate histories."""
|
|
798
|
+
return (
|
|
799
|
+
migrator.make()(loader=migrations(prefix=prefix), table=f"{prefix}_runner_migrations")
|
|
800
|
+
.as_void()
|
|
801
|
+
.or_die()
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def make(
|
|
806
|
+
*, prefix: str = "cluster"
|
|
807
|
+
) -> Effect[RunnerStorageService, Never, SqlClient | ShardingConfig | Scope]:
|
|
808
|
+
"""Run the migrations, then ``make_storage``."""
|
|
809
|
+
return run_migrations(prefix=prefix).flat_map(lambda _: make_storage(prefix=prefix))
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
def layer_migrations(*, prefix: str = "cluster") -> layer_.Layer[Never, Never, SqlClient]:
|
|
813
|
+
return layer_.effect_discard(run_migrations(prefix=prefix))
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def layer_storage(
|
|
817
|
+
*, prefix: str = "cluster"
|
|
818
|
+
) -> layer_.Layer[RunnerStorage, Never, SqlClient | ShardingConfig]:
|
|
819
|
+
return layer_.effect(RunnerStorage, make_storage(prefix=prefix))
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
def layer_with(
|
|
823
|
+
*, prefix: str = "cluster"
|
|
824
|
+
) -> layer_.Layer[RunnerStorage, Never, SqlClient | ShardingConfig]:
|
|
825
|
+
return layer_.effect(RunnerStorage, make(prefix=prefix))
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
layer: layer_.Layer[RunnerStorage, Never, SqlClient | ShardingConfig] = layer_with()
|
|
829
|
+
|
|
830
|
+
__all__ = [
|
|
831
|
+
"effective_interval",
|
|
832
|
+
"layer",
|
|
833
|
+
"layer_migrations",
|
|
834
|
+
"layer_storage",
|
|
835
|
+
"layer_with",
|
|
836
|
+
"make",
|
|
837
|
+
"make_storage",
|
|
838
|
+
"migrations",
|
|
839
|
+
"postgres_lock_namespace",
|
|
840
|
+
"run_migrations",
|
|
841
|
+
]
|