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,692 @@
|
|
|
1
|
+
"""A journal in a SQL database (Effect's ``eventlog/SqlEventJournal``),
|
|
2
|
+
on SQLite or PostgreSQL.
|
|
3
|
+
|
|
4
|
+
Entries live in ``entry_table``; ``<entry_table>_sequence`` numbers them in
|
|
5
|
+
insertion order (uploads checkpoint that number, since ids order creation,
|
|
6
|
+
not insertion), ``<entry_table>_sources`` records the remote each imported
|
|
7
|
+
entry came from, and the cursors and checkpoints tables keep each remote's
|
|
8
|
+
download and upload progress. Several processes may share the database:
|
|
9
|
+
local ids sort after the latest stored id, and on PostgreSQL writers
|
|
10
|
+
serialize on a table lock so checkpoints cannot skip a commit.
|
|
11
|
+
|
|
12
|
+
A database written by a release that kept a ``remotes_table`` row per
|
|
13
|
+
entry per remote is migrated in one transaction when opened.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from collections.abc import Callable, Sequence
|
|
19
|
+
from typing import Any, Literal, Never, cast
|
|
20
|
+
|
|
21
|
+
from pyfect._core import Effect, Restore, uninterruptible_mask
|
|
22
|
+
from pyfect.clock import Clock, ClockService
|
|
23
|
+
from pyfect.effect import die, gen, succeed
|
|
24
|
+
from pyfect.eventlog._conflicts import ConflictIndex
|
|
25
|
+
from pyfect.eventlog.event_journal import (
|
|
26
|
+
Compact,
|
|
27
|
+
Entry,
|
|
28
|
+
EntryId,
|
|
29
|
+
EntryInput,
|
|
30
|
+
EventJournal,
|
|
31
|
+
EventJournalError,
|
|
32
|
+
EventJournalService,
|
|
33
|
+
RemoteAck,
|
|
34
|
+
RemoteEntry,
|
|
35
|
+
RemoteId,
|
|
36
|
+
ReplayBatch,
|
|
37
|
+
StoreId,
|
|
38
|
+
WithLock,
|
|
39
|
+
advance_sequence,
|
|
40
|
+
entry_id_millis,
|
|
41
|
+
entry_order,
|
|
42
|
+
make_entries,
|
|
43
|
+
next_sequence_after,
|
|
44
|
+
warn_if_ahead,
|
|
45
|
+
)
|
|
46
|
+
from pyfect.layer import Layer
|
|
47
|
+
from pyfect.layer import effect as layer_effect
|
|
48
|
+
from pyfect.option import Option, Some
|
|
49
|
+
from pyfect.persistence._columns import column_bytes, column_int
|
|
50
|
+
from pyfect.pubsub import PubSub, Subscription
|
|
51
|
+
from pyfect.scope import Scope
|
|
52
|
+
from pyfect.sql.client import SqlClient, SqlClientService
|
|
53
|
+
from pyfect.sql.connection import Row
|
|
54
|
+
from pyfect.sql.error import SqlError
|
|
55
|
+
from pyfect.sql.statement import Fragment, Identifier
|
|
56
|
+
|
|
57
|
+
__all__ = ["SqlError", "layer", "make"]
|
|
58
|
+
|
|
59
|
+
type _Dialect = Literal["sqlite", "pg"]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _dialect(sql: SqlClientService) -> _Dialect:
|
|
63
|
+
match sql.dialect:
|
|
64
|
+
case "sqlite" | "pg":
|
|
65
|
+
return sql.dialect
|
|
66
|
+
case _:
|
|
67
|
+
pass
|
|
68
|
+
raise ValueError(f"SqlEventJournal supports SQLite and PostgreSQL, not {sql.dialect}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _to_entry(row: Row) -> Entry:
|
|
72
|
+
return Entry(
|
|
73
|
+
id=EntryId(column_bytes(row["id"])),
|
|
74
|
+
event=str(row["event"]),
|
|
75
|
+
primary_key=str(row["primary_key"]),
|
|
76
|
+
payload=column_bytes(row["payload"]),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _to_ordered_entries(rows: Sequence[Row]) -> list[Entry]:
|
|
81
|
+
# Timestamp ties sort by id bytes the same way on every dialect.
|
|
82
|
+
return sorted(map(_to_entry, rows), key=entry_order)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _to_entry_row(entry: Entry) -> dict[str, object]:
|
|
86
|
+
return {
|
|
87
|
+
"id": entry.id,
|
|
88
|
+
"event": entry.event,
|
|
89
|
+
"primary_key": entry.primary_key,
|
|
90
|
+
"payload": entry.payload,
|
|
91
|
+
"timestamp": entry_id_millis(entry.id),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class _CallbackFailure:
|
|
96
|
+
"""A write callback's own error, kept apart from the journal's SQL
|
|
97
|
+
errors."""
|
|
98
|
+
|
|
99
|
+
__slots__ = ("error",)
|
|
100
|
+
|
|
101
|
+
def __init__(self, error: object) -> None:
|
|
102
|
+
self.error = error
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _journal_error(method: str) -> Callable[[object], EventJournalError]:
|
|
106
|
+
return lambda cause: EventJournalError(method, cause)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def make(
|
|
110
|
+
*,
|
|
111
|
+
entry_table: str = "effect_event_journal",
|
|
112
|
+
remotes_table: str = "effect_event_remotes",
|
|
113
|
+
remote_cursors_table: str | None = None,
|
|
114
|
+
) -> Effect[EventJournalService, SqlError, SqlClient]:
|
|
115
|
+
"""A journal in the ``SqlClient``'s database, creating or migrating its
|
|
116
|
+
tables (Effect's ``SqlEventJournal.make``). Fails with ``ValueError``
|
|
117
|
+
as a defect on a dialect other than SQLite or PostgreSQL."""
|
|
118
|
+
|
|
119
|
+
def build():
|
|
120
|
+
sql = (yield from SqlClient()).without_transforms()
|
|
121
|
+
dialect = _dialect(sql)
|
|
122
|
+
tables = _Tables(
|
|
123
|
+
sql,
|
|
124
|
+
entry_table,
|
|
125
|
+
remotes_table,
|
|
126
|
+
remote_cursors_table or f"{remotes_table}_cursors",
|
|
127
|
+
)
|
|
128
|
+
yield from _open_schema(sql, dialect, tables).with_tracer_enabled(False)
|
|
129
|
+
pubsub = yield from PubSub[Entry].unbounded()
|
|
130
|
+
clock = yield from Clock()
|
|
131
|
+
return _SqlJournal(sql, dialect, tables, pubsub, clock)
|
|
132
|
+
|
|
133
|
+
return gen(build)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def layer(
|
|
137
|
+
*,
|
|
138
|
+
entry_table: str = "effect_event_journal",
|
|
139
|
+
remotes_table: str = "effect_event_remotes",
|
|
140
|
+
remote_cursors_table: str | None = None,
|
|
141
|
+
) -> Layer[EventJournal, SqlError, SqlClient]:
|
|
142
|
+
"""The ``EventJournal`` of ``make``."""
|
|
143
|
+
return layer_effect(
|
|
144
|
+
EventJournal,
|
|
145
|
+
make(
|
|
146
|
+
entry_table=entry_table,
|
|
147
|
+
remotes_table=remotes_table,
|
|
148
|
+
remote_cursors_table=remote_cursors_table,
|
|
149
|
+
),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class _Tables:
|
|
154
|
+
__slots__ = (
|
|
155
|
+
"checkpoints",
|
|
156
|
+
"cursors",
|
|
157
|
+
"entries",
|
|
158
|
+
"entry_name",
|
|
159
|
+
"remotes",
|
|
160
|
+
"remotes_name",
|
|
161
|
+
"sequence",
|
|
162
|
+
"sources",
|
|
163
|
+
"sources_name",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def __init__(
|
|
167
|
+
self, sql: SqlClientService, entry_table: str, remotes_table: str, cursors_table: str
|
|
168
|
+
) -> None:
|
|
169
|
+
self.entry_name = entry_table
|
|
170
|
+
self.remotes_name = remotes_table
|
|
171
|
+
self.sources_name = f"{entry_table}_sources"
|
|
172
|
+
self.entries: Identifier = sql.identifier(entry_table)
|
|
173
|
+
self.remotes: Identifier = sql.identifier(remotes_table)
|
|
174
|
+
self.sequence: Identifier = sql.identifier(f"{entry_table}_sequence")
|
|
175
|
+
self.sources: Identifier = sql.identifier(self.sources_name)
|
|
176
|
+
self.checkpoints: Identifier = sql.identifier(f"{remotes_table}_checkpoints")
|
|
177
|
+
self.cursors: Identifier = sql.identifier(cursors_table)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _quoted(sql: SqlClientService, table: str) -> str:
|
|
181
|
+
# to_regclass folds unquoted names to lowercase, so pass the quoted name.
|
|
182
|
+
return sql("{}", sql.identifier(table)).compile(True)[0]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _table_exists(sql: SqlClientService, dialect: _Dialect, table: str) -> Effect[bool, SqlError]:
|
|
186
|
+
if dialect == "pg":
|
|
187
|
+
# Reading the catalog refreshes cached names after concurrent DDL.
|
|
188
|
+
query = sql(
|
|
189
|
+
"SELECT 1 AS found FROM pg_class WHERE oid = to_regclass({})", _quoted(sql, table)
|
|
190
|
+
)
|
|
191
|
+
else:
|
|
192
|
+
query = sql(
|
|
193
|
+
"SELECT 1 AS found FROM sqlite_master WHERE type = 'table' AND name = {}", table
|
|
194
|
+
)
|
|
195
|
+
return query.map(lambda rows: len(rows) > 0)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _create_schema(sql: SqlClientService, dialect: _Dialect, t: _Tables) -> Effect[None, SqlError]:
|
|
199
|
+
event_index = sql.identifier(f"{t.entry_name}_event_idx")
|
|
200
|
+
timestamp_index = sql.identifier(f"{t.entry_name}_timestamp_idx")
|
|
201
|
+
pg = dialect == "pg"
|
|
202
|
+
|
|
203
|
+
def body():
|
|
204
|
+
if pg:
|
|
205
|
+
yield from sql(
|
|
206
|
+
"CREATE TABLE IF NOT EXISTS {} (id BYTEA PRIMARY KEY, event TEXT NOT NULL, "
|
|
207
|
+
"primary_key TEXT NOT NULL, payload BYTEA NOT NULL, timestamp BIGINT NOT NULL)",
|
|
208
|
+
t.entries,
|
|
209
|
+
)
|
|
210
|
+
# Insertion sequences let uploads include older entries imported later.
|
|
211
|
+
yield from sql(
|
|
212
|
+
"CREATE TABLE IF NOT EXISTS {} (sequence BIGSERIAL PRIMARY KEY, "
|
|
213
|
+
"entry_id BYTEA NOT NULL UNIQUE)",
|
|
214
|
+
t.sequence,
|
|
215
|
+
)
|
|
216
|
+
yield from sql(
|
|
217
|
+
"CREATE TABLE IF NOT EXISTS {} (remote_id BYTEA PRIMARY KEY, "
|
|
218
|
+
"sequence BIGINT NOT NULL)",
|
|
219
|
+
t.checkpoints,
|
|
220
|
+
)
|
|
221
|
+
yield from sql(
|
|
222
|
+
"CREATE TABLE IF NOT EXISTS {} (remote_id BYTEA PRIMARY KEY, "
|
|
223
|
+
"next_sequence INT NOT NULL)",
|
|
224
|
+
t.cursors,
|
|
225
|
+
)
|
|
226
|
+
# The remote each imported entry came from, so uploads skip it.
|
|
227
|
+
yield from sql(
|
|
228
|
+
"CREATE TABLE IF NOT EXISTS {} (entry_id BYTEA PRIMARY KEY, "
|
|
229
|
+
"remote_id BYTEA NOT NULL)",
|
|
230
|
+
t.sources,
|
|
231
|
+
)
|
|
232
|
+
# B-tree entries are limited to about 2.7 KB, so index hashes of
|
|
233
|
+
# the unbounded columns.
|
|
234
|
+
yield from sql(
|
|
235
|
+
"CREATE INDEX IF NOT EXISTS {} ON {} (md5(event), md5(primary_key), timestamp)",
|
|
236
|
+
event_index,
|
|
237
|
+
t.entries,
|
|
238
|
+
)
|
|
239
|
+
else:
|
|
240
|
+
yield from sql(
|
|
241
|
+
"CREATE TABLE IF NOT EXISTS {} (id BLOB PRIMARY KEY, event TEXT NOT NULL, "
|
|
242
|
+
"primary_key TEXT NOT NULL, payload BLOB NOT NULL, timestamp INTEGER NOT NULL)",
|
|
243
|
+
t.entries,
|
|
244
|
+
)
|
|
245
|
+
yield from sql(
|
|
246
|
+
"CREATE TABLE IF NOT EXISTS {} (sequence INTEGER PRIMARY KEY, "
|
|
247
|
+
"entry_id BLOB NOT NULL UNIQUE)",
|
|
248
|
+
t.sequence,
|
|
249
|
+
)
|
|
250
|
+
yield from sql(
|
|
251
|
+
"CREATE TABLE IF NOT EXISTS {} (remote_id BLOB PRIMARY KEY, "
|
|
252
|
+
"sequence INTEGER NOT NULL)",
|
|
253
|
+
t.checkpoints,
|
|
254
|
+
)
|
|
255
|
+
yield from sql(
|
|
256
|
+
"CREATE TABLE IF NOT EXISTS {} (remote_id BLOB PRIMARY KEY, "
|
|
257
|
+
"next_sequence INT NOT NULL)",
|
|
258
|
+
t.cursors,
|
|
259
|
+
)
|
|
260
|
+
yield from sql(
|
|
261
|
+
"CREATE TABLE IF NOT EXISTS {} (entry_id BLOB PRIMARY KEY, "
|
|
262
|
+
"remote_id BLOB NOT NULL)",
|
|
263
|
+
t.sources,
|
|
264
|
+
)
|
|
265
|
+
yield from sql(
|
|
266
|
+
"CREATE INDEX IF NOT EXISTS {} ON {} (event, primary_key, timestamp)",
|
|
267
|
+
event_index,
|
|
268
|
+
t.entries,
|
|
269
|
+
)
|
|
270
|
+
yield from sql(
|
|
271
|
+
"CREATE INDEX IF NOT EXISTS {} ON {} (timestamp)", timestamp_index, t.entries
|
|
272
|
+
)
|
|
273
|
+
# Number existing entries in creation order. ``WHERE true`` keeps
|
|
274
|
+
# SQLite from parsing ON CONFLICT as a join constraint.
|
|
275
|
+
yield from sql(
|
|
276
|
+
"INSERT INTO {} (entry_id) SELECT id FROM {} WHERE true "
|
|
277
|
+
"ORDER BY timestamp ASC, id ASC ON CONFLICT DO NOTHING",
|
|
278
|
+
t.sequence,
|
|
279
|
+
t.entries,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
return gen(body)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _import_remotes(sql: SqlClientService, t: _Tables) -> Effect[None, SqlError]:
|
|
286
|
+
# Earlier releases kept a remotes row per entry per remote. An entry
|
|
287
|
+
# known to several remotes keeps one of them as its source, and a remote
|
|
288
|
+
# without a cursor resumes after its highest downloaded sequence.
|
|
289
|
+
def body():
|
|
290
|
+
yield from sql(
|
|
291
|
+
"INSERT INTO {} (entry_id, remote_id) SELECT entry_id, remote_id FROM {} "
|
|
292
|
+
"WHERE true ON CONFLICT DO NOTHING",
|
|
293
|
+
t.sources,
|
|
294
|
+
t.remotes,
|
|
295
|
+
)
|
|
296
|
+
yield from sql(
|
|
297
|
+
"INSERT INTO {} (remote_id, next_sequence) SELECT remote_id, MAX(sequence) + 1 "
|
|
298
|
+
"FROM {} GROUP BY remote_id ON CONFLICT DO NOTHING",
|
|
299
|
+
t.cursors,
|
|
300
|
+
t.remotes,
|
|
301
|
+
)
|
|
302
|
+
yield from sql("DROP TABLE {}", t.remotes)
|
|
303
|
+
|
|
304
|
+
return gen(body)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _open_schema(sql: SqlClientService, dialect: _Dialect, t: _Tables) -> Effect[None, SqlError]:
|
|
308
|
+
# Both steps run in one transaction, so a complete schema has the sources
|
|
309
|
+
# table and no remotes table. Checking that first lets complete databases
|
|
310
|
+
# open read-only; PostgreSQL also locks and rechecks so concurrent opens
|
|
311
|
+
# migrate once.
|
|
312
|
+
def state():
|
|
313
|
+
sources = yield from _table_exists(sql, dialect, t.sources_name)
|
|
314
|
+
remotes = yield from _table_exists(sql, dialect, t.remotes_name)
|
|
315
|
+
return sources, remotes
|
|
316
|
+
|
|
317
|
+
def migrate():
|
|
318
|
+
if dialect == "pg":
|
|
319
|
+
yield from sql("SELECT pg_advisory_xact_lock(hashtext({}))", _quoted(sql, t.entry_name))
|
|
320
|
+
sources, remotes = yield from gen(state)
|
|
321
|
+
if not sources:
|
|
322
|
+
yield from _create_schema(sql, dialect, t)
|
|
323
|
+
if remotes:
|
|
324
|
+
yield from _import_remotes(sql, t)
|
|
325
|
+
|
|
326
|
+
def body():
|
|
327
|
+
sources, remotes = yield from gen(state)
|
|
328
|
+
if sources and not remotes:
|
|
329
|
+
return
|
|
330
|
+
yield from sql.with_transaction(gen(migrate))
|
|
331
|
+
|
|
332
|
+
return gen(body)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class _SqlJournal(EventJournalService):
|
|
336
|
+
def __init__(
|
|
337
|
+
self,
|
|
338
|
+
sql: SqlClientService,
|
|
339
|
+
dialect: _Dialect,
|
|
340
|
+
tables: _Tables,
|
|
341
|
+
pubsub: PubSub[Entry],
|
|
342
|
+
clock: ClockService,
|
|
343
|
+
) -> None:
|
|
344
|
+
self._sql = sql
|
|
345
|
+
self._dialect = dialect
|
|
346
|
+
self._t = tables
|
|
347
|
+
self._pubsub = pubsub
|
|
348
|
+
self._clock = clock
|
|
349
|
+
|
|
350
|
+
# --- statements -----------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
@property
|
|
353
|
+
def _lock_writers(self) -> Effect[None, SqlError]:
|
|
354
|
+
# Serialize writers so checkpoints cannot skip out-of-order commits.
|
|
355
|
+
# Lock before callbacks in both paths to avoid projection-row deadlocks.
|
|
356
|
+
if self._dialect == "pg":
|
|
357
|
+
return self._sql(
|
|
358
|
+
"LOCK TABLE {} IN SHARE ROW EXCLUSIVE MODE", self._t.sequence
|
|
359
|
+
).as_void()
|
|
360
|
+
return succeed(None)
|
|
361
|
+
|
|
362
|
+
def _insert(self, table: object, rows: list[dict[str, object]]) -> Effect[object, SqlError]:
|
|
363
|
+
"""Insert ``rows`` (none is nothing to do: ``sql.insert`` needs one)."""
|
|
364
|
+
if not rows:
|
|
365
|
+
return succeed(None)
|
|
366
|
+
sql = self._sql
|
|
367
|
+
return sql("INSERT INTO {} {} ON CONFLICT DO NOTHING", table, sql.insert(rows))
|
|
368
|
+
|
|
369
|
+
def _insert_entries(self, entries: Sequence[Entry]) -> Effect[object, SqlError]:
|
|
370
|
+
return self._insert(self._t.entries, [_to_entry_row(entry) for entry in entries])
|
|
371
|
+
|
|
372
|
+
def _insert_sequence(self, entries: Sequence[Entry]) -> Effect[object, SqlError]:
|
|
373
|
+
return self._insert(self._t.sequence, [{"entry_id": entry.id} for entry in entries])
|
|
374
|
+
|
|
375
|
+
def _insert_sources(
|
|
376
|
+
self, entries: Sequence[Entry], remote_id: RemoteId
|
|
377
|
+
) -> Effect[object, SqlError]:
|
|
378
|
+
rows: list[dict[str, object]] = [
|
|
379
|
+
{"entry_id": entry.id, "remote_id": remote_id} for entry in entries
|
|
380
|
+
]
|
|
381
|
+
return self._insert(self._t.sources, rows)
|
|
382
|
+
|
|
383
|
+
def _read_cursor(self, remote_id: RemoteId) -> Effect[int, SqlError]:
|
|
384
|
+
return self._sql(
|
|
385
|
+
"SELECT next_sequence FROM {} WHERE remote_id = {}", self._t.cursors, remote_id
|
|
386
|
+
).map(lambda rows: column_int(rows[0]["next_sequence"]) if rows else 0)
|
|
387
|
+
|
|
388
|
+
def _write_cursor(self, remote_id: RemoteId, next: int) -> Effect[object, SqlError]:
|
|
389
|
+
# Cursors only move forward.
|
|
390
|
+
sql = self._sql
|
|
391
|
+
return sql(
|
|
392
|
+
"INSERT INTO {} {} ON CONFLICT (remote_id) DO UPDATE "
|
|
393
|
+
"SET next_sequence = excluded.next_sequence "
|
|
394
|
+
"WHERE {}.next_sequence < excluded.next_sequence",
|
|
395
|
+
self._t.cursors,
|
|
396
|
+
sql.insert({"remote_id": remote_id, "next_sequence": next}),
|
|
397
|
+
self._t.cursors,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
@property
|
|
401
|
+
def _latest_entry_ids(self) -> Effect[list[EntryId], SqlError]:
|
|
402
|
+
# Other processes share the database but not the entry id sequence.
|
|
403
|
+
# Ids in the latest millisecond are few, and comparing them here
|
|
404
|
+
# avoids ordering id bytes in SQL.
|
|
405
|
+
return self._sql(
|
|
406
|
+
"SELECT id FROM {} WHERE timestamp = (SELECT MAX(timestamp) FROM {})",
|
|
407
|
+
self._t.entries,
|
|
408
|
+
self._t.entries,
|
|
409
|
+
).map(lambda rows: [EntryId(column_bytes(row["id"])) for row in rows])
|
|
410
|
+
|
|
411
|
+
def _conflicts(self, index: ConflictIndex, primary_keys: list[str]) -> Fragment:
|
|
412
|
+
sql = self._sql
|
|
413
|
+
clauses: list[Fragment] = []
|
|
414
|
+
if self._dialect == "pg":
|
|
415
|
+
# Match the hash index; the exact predicates rule out collisions.
|
|
416
|
+
clauses.append(
|
|
417
|
+
sql(
|
|
418
|
+
"md5(event) IN ({})",
|
|
419
|
+
sql.csv([sql("md5({})", event) for event in index.events]),
|
|
420
|
+
)
|
|
421
|
+
)
|
|
422
|
+
clauses.append(
|
|
423
|
+
sql(
|
|
424
|
+
"md5(primary_key) IN ({})",
|
|
425
|
+
sql.csv([sql("md5({})", key) for key in primary_keys]),
|
|
426
|
+
)
|
|
427
|
+
)
|
|
428
|
+
clauses.append(sql.in_("event", index.events))
|
|
429
|
+
clauses.append(sql.in_("primary_key", primary_keys))
|
|
430
|
+
# A coarse filter: the index compares ids exactly.
|
|
431
|
+
clauses.append(sql("timestamp >= {}", index.oldest_millis))
|
|
432
|
+
return sql.and_(clauses)
|
|
433
|
+
|
|
434
|
+
# --- the service ----------------------------------------------------------------
|
|
435
|
+
|
|
436
|
+
@property
|
|
437
|
+
def entries(self) -> Effect[list[Entry], EventJournalError]:
|
|
438
|
+
return (
|
|
439
|
+
self._sql("SELECT * FROM {} ORDER BY timestamp ASC", self._t.entries)
|
|
440
|
+
.with_tracer_enabled(False)
|
|
441
|
+
.map(_to_ordered_entries)
|
|
442
|
+
.map_error(_journal_error("entries"))
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
def entries_by_id(
|
|
446
|
+
self, ids: Sequence[EntryId], /
|
|
447
|
+
) -> Effect[list[Entry | None], EventJournalError]:
|
|
448
|
+
if not ids:
|
|
449
|
+
return succeed([])
|
|
450
|
+
sql = self._sql
|
|
451
|
+
|
|
452
|
+
def pick(rows: list[Row]) -> list[Entry | None]:
|
|
453
|
+
by_id = {entry.id: entry for entry in map(_to_entry, rows)}
|
|
454
|
+
return [by_id.get(entry_id) for entry_id in ids]
|
|
455
|
+
|
|
456
|
+
return (
|
|
457
|
+
sql("SELECT * FROM {} WHERE {}", self._t.entries, sql.in_("id", list(ids)))
|
|
458
|
+
.with_tracer_enabled(False)
|
|
459
|
+
.map(pick)
|
|
460
|
+
.map_error(_journal_error("entriesById"))
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
def write_many[A, E, R](
|
|
464
|
+
self,
|
|
465
|
+
entries: Sequence[EntryInput],
|
|
466
|
+
effect: Callable[[list[Entry]], Effect[A, E, R]],
|
|
467
|
+
/,
|
|
468
|
+
) -> Effect[A, EventJournalError | E, R]:
|
|
469
|
+
sql = self._sql
|
|
470
|
+
|
|
471
|
+
def unwrap(error: object) -> Any:
|
|
472
|
+
if isinstance(error, _CallbackFailure):
|
|
473
|
+
return error.error
|
|
474
|
+
if isinstance(error, EventJournalError):
|
|
475
|
+
return error
|
|
476
|
+
return EventJournalError("writeMany", error)
|
|
477
|
+
|
|
478
|
+
def masked(restore: Restore) -> Effect[A, Any, R]:
|
|
479
|
+
def body():
|
|
480
|
+
yield from self._lock_writers
|
|
481
|
+
# Rows visible here include an enclosing transaction's
|
|
482
|
+
# uncommitted ones, which local ids must also sort after.
|
|
483
|
+
after = yield from self._latest_entry_ids
|
|
484
|
+
committed = yield from make_entries(entries, after)
|
|
485
|
+
result = yield from restore(effect(committed)).map_error(_CallbackFailure)
|
|
486
|
+
yield from self._insert_entries(committed)
|
|
487
|
+
yield from self._insert_sequence(committed)
|
|
488
|
+
return result, committed
|
|
489
|
+
|
|
490
|
+
def publish(written: tuple[A, list[Entry]]) -> Effect[A]:
|
|
491
|
+
result, committed = written
|
|
492
|
+
return self._pubsub.publish_all(committed).as_(result)
|
|
493
|
+
|
|
494
|
+
return sql.with_transaction(gen(body)).map_error(unwrap).flat_map(publish)
|
|
495
|
+
|
|
496
|
+
return uninterruptible_mask(masked).with_tracer_enabled(False)
|
|
497
|
+
|
|
498
|
+
def _write_from_remote(
|
|
499
|
+
self,
|
|
500
|
+
remote_id: RemoteId,
|
|
501
|
+
remote_entries: Sequence[RemoteEntry],
|
|
502
|
+
effect: ReplayBatch,
|
|
503
|
+
compact: Compact | None,
|
|
504
|
+
) -> Effect[list[Entry], Any]:
|
|
505
|
+
sql = self._sql
|
|
506
|
+
t = self._t
|
|
507
|
+
|
|
508
|
+
def body():
|
|
509
|
+
yield from self._lock_writers
|
|
510
|
+
existing: set[bytes] = set()
|
|
511
|
+
if remote_entries:
|
|
512
|
+
rows = yield from sql(
|
|
513
|
+
"SELECT id FROM {} WHERE {}",
|
|
514
|
+
t.entries,
|
|
515
|
+
sql.in_("id", [remote_entry.entry.id for remote_entry in remote_entries]),
|
|
516
|
+
)
|
|
517
|
+
existing.update(column_bytes(row["id"]) for row in rows)
|
|
518
|
+
# Overlapping batches can repeat an entry; the first copy wins.
|
|
519
|
+
incoming: set[bytes] = set()
|
|
520
|
+
uncommitted: list[RemoteEntry] = []
|
|
521
|
+
for remote_entry in remote_entries:
|
|
522
|
+
entry_id = remote_entry.entry.id
|
|
523
|
+
if entry_id in existing or entry_id in incoming:
|
|
524
|
+
continue
|
|
525
|
+
incoming.add(entry_id)
|
|
526
|
+
uncommitted.append(remote_entry)
|
|
527
|
+
duplicates = [
|
|
528
|
+
remote_entry.entry
|
|
529
|
+
for remote_entry in remote_entries
|
|
530
|
+
if remote_entry.entry.id in existing
|
|
531
|
+
]
|
|
532
|
+
compacted = (
|
|
533
|
+
(yield from compact(uncommitted))
|
|
534
|
+
if compact is not None
|
|
535
|
+
else [remote_entry.entry for remote_entry in uncommitted]
|
|
536
|
+
)
|
|
537
|
+
if compacted:
|
|
538
|
+
index = ConflictIndex(compacted)
|
|
539
|
+
primary_keys = list(dict.fromkeys(entry.primary_key for entry in compacted))
|
|
540
|
+
stored = yield from sql(
|
|
541
|
+
"SELECT * FROM {} WHERE {}", t.entries, self._conflicts(index, primary_keys)
|
|
542
|
+
)
|
|
543
|
+
for row in stored:
|
|
544
|
+
index.add(_to_entry(row))
|
|
545
|
+
yield from effect(index.to_batch())
|
|
546
|
+
# Keep the incoming batch out of conflict queries and persist only
|
|
547
|
+
# after compaction and every callback have succeeded.
|
|
548
|
+
if uncommitted:
|
|
549
|
+
imported = [remote_entry.entry for remote_entry in uncommitted]
|
|
550
|
+
yield from self._insert_entries(imported)
|
|
551
|
+
yield from self._insert_sequence(imported)
|
|
552
|
+
yield from self._insert_sources(imported, remote_id)
|
|
553
|
+
# The change stream is ordered, so every earlier sequence is known.
|
|
554
|
+
if remote_entries:
|
|
555
|
+
yield from self._write_cursor(remote_id, next_sequence_after(remote_entries))
|
|
556
|
+
yield from warn_if_ahead(
|
|
557
|
+
self._clock, (remote_entry.entry.id for remote_entry in uncommitted)
|
|
558
|
+
)
|
|
559
|
+
return duplicates
|
|
560
|
+
|
|
561
|
+
return gen(body)
|
|
562
|
+
|
|
563
|
+
def write_from_remote(
|
|
564
|
+
self,
|
|
565
|
+
*,
|
|
566
|
+
remote_id: RemoteId,
|
|
567
|
+
entries: Sequence[RemoteEntry],
|
|
568
|
+
effect: ReplayBatch,
|
|
569
|
+
compact: Compact | None = None,
|
|
570
|
+
) -> Effect[list[Entry], EventJournalError]:
|
|
571
|
+
def wrap(error: object) -> EventJournalError:
|
|
572
|
+
if isinstance(error, EventJournalError):
|
|
573
|
+
return error
|
|
574
|
+
return EventJournalError("writeFromRemote", error)
|
|
575
|
+
|
|
576
|
+
return (
|
|
577
|
+
self._sql.with_transaction(self._write_from_remote(remote_id, entries, effect, compact))
|
|
578
|
+
.with_tracer_enabled(False)
|
|
579
|
+
.map_error(wrap)
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
def with_remote_uncommitted[A, E, R](
|
|
583
|
+
self, remote_id: RemoteId, f: Callable[[list[Entry]], Effect[A, E, R]], /
|
|
584
|
+
) -> Effect[Option[A], EventJournalError | E, R]:
|
|
585
|
+
sql = self._sql
|
|
586
|
+
t = self._t
|
|
587
|
+
journal_error = _journal_error("withRemoteUncommited")
|
|
588
|
+
|
|
589
|
+
def body():
|
|
590
|
+
# An upper bound, so entries committed during the upload stay pending.
|
|
591
|
+
bounds = yield from sql(
|
|
592
|
+
"SELECT COALESCE((SELECT sequence FROM {} WHERE remote_id = {}), 0) AS checkpoint, "
|
|
593
|
+
"COALESCE((SELECT MAX(sequence) FROM {}), 0) AS latest",
|
|
594
|
+
t.checkpoints,
|
|
595
|
+
remote_id,
|
|
596
|
+
t.sequence,
|
|
597
|
+
).map_error(journal_error)
|
|
598
|
+
checkpoint = column_int(bounds[0]["checkpoint"])
|
|
599
|
+
latest = column_int(bounds[0]["latest"])
|
|
600
|
+
if latest <= checkpoint:
|
|
601
|
+
return None
|
|
602
|
+
rows = yield from sql(
|
|
603
|
+
"SELECT e.* FROM {} s JOIN {} e ON e.id = s.entry_id "
|
|
604
|
+
"WHERE s.sequence > {} AND s.sequence <= {} AND NOT EXISTS ("
|
|
605
|
+
"SELECT 1 FROM {} src WHERE src.entry_id = s.entry_id AND src.remote_id = {})",
|
|
606
|
+
t.sequence,
|
|
607
|
+
t.entries,
|
|
608
|
+
checkpoint,
|
|
609
|
+
latest,
|
|
610
|
+
t.sources,
|
|
611
|
+
remote_id,
|
|
612
|
+
).map_error(journal_error)
|
|
613
|
+
entries = _to_ordered_entries(rows)
|
|
614
|
+
# Uploads can complete out of order; the checkpoint only moves forward.
|
|
615
|
+
save_checkpoint = sql(
|
|
616
|
+
"INSERT INTO {} {} ON CONFLICT (remote_id) DO UPDATE "
|
|
617
|
+
"SET sequence = excluded.sequence WHERE {}.sequence < excluded.sequence",
|
|
618
|
+
t.checkpoints,
|
|
619
|
+
sql.insert({"remote_id": remote_id, "sequence": latest}),
|
|
620
|
+
t.checkpoints,
|
|
621
|
+
).map_error(journal_error)
|
|
622
|
+
|
|
623
|
+
def upload(restore: Restore) -> Effect[Option[A], Any, R]:
|
|
624
|
+
run: Effect[Option[A], E, R] = f(entries).map(Some) if entries else succeed(None)
|
|
625
|
+
return restore(run).tap(lambda _: save_checkpoint)
|
|
626
|
+
|
|
627
|
+
return (yield from uninterruptible_mask(upload))
|
|
628
|
+
|
|
629
|
+
return gen(body).with_tracer_enabled(False)
|
|
630
|
+
|
|
631
|
+
def write_remote_acks(
|
|
632
|
+
self, *, remote_id: RemoteId, acks: Sequence[RemoteAck], first_sequence: int | None = None
|
|
633
|
+
) -> Effect[None, EventJournalError]:
|
|
634
|
+
if not acks:
|
|
635
|
+
return succeed(None)
|
|
636
|
+
|
|
637
|
+
def body():
|
|
638
|
+
next = yield from self._read_cursor(remote_id)
|
|
639
|
+
yield from self._write_cursor(
|
|
640
|
+
remote_id, advance_sequence(next, acks, first_sequence or 0)
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
return (
|
|
644
|
+
self._sql.with_transaction(gen(body))
|
|
645
|
+
.with_tracer_enabled(False)
|
|
646
|
+
.map_error(_journal_error("writeRemoteAcks"))
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
def next_remote_sequence(self, remote_id: RemoteId, /) -> Effect[int, EventJournalError]:
|
|
650
|
+
return (
|
|
651
|
+
self._read_cursor(remote_id)
|
|
652
|
+
.with_tracer_enabled(False)
|
|
653
|
+
.map_error(_journal_error("nextRemoteSequence"))
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
@property
|
|
657
|
+
def changes(self) -> Effect[Subscription[Entry], Never, Scope]:
|
|
658
|
+
return self._pubsub.subscribe()
|
|
659
|
+
|
|
660
|
+
@property
|
|
661
|
+
def destroy(self) -> Effect[None, EventJournalError]:
|
|
662
|
+
sql = self._sql
|
|
663
|
+
t = self._t
|
|
664
|
+
|
|
665
|
+
def body():
|
|
666
|
+
for table in (t.entries, t.sequence, t.sources, t.checkpoints, t.cursors):
|
|
667
|
+
yield from sql("DROP TABLE {}", table)
|
|
668
|
+
|
|
669
|
+
return gen(body).with_tracer_enabled(False).map_error(_journal_error("destroy"))
|
|
670
|
+
|
|
671
|
+
def with_lock(self, store_id: StoreId, /) -> WithLock:
|
|
672
|
+
return _TransactionLock(self._sql)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
class _TransactionLock:
|
|
676
|
+
"""A store's lock is a transaction of the journal's client."""
|
|
677
|
+
|
|
678
|
+
__slots__ = ("_sql",)
|
|
679
|
+
|
|
680
|
+
def __init__(self, sql: SqlClientService) -> None:
|
|
681
|
+
self._sql = sql
|
|
682
|
+
|
|
683
|
+
def __call__[A, E, R](self, effect: Effect[A, E, R], /) -> Effect[A, E, R]:
|
|
684
|
+
def defect(error: object) -> Effect[Never]:
|
|
685
|
+
return die(cast("BaseException", error))
|
|
686
|
+
|
|
687
|
+
return cast(
|
|
688
|
+
"Effect[A, E, R]",
|
|
689
|
+
self._sql.with_transaction(effect).catch_if(
|
|
690
|
+
lambda error: isinstance(error, SqlError), defect
|
|
691
|
+
),
|
|
692
|
+
)
|