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,201 @@
|
|
|
1
|
+
"""An HTTP request over ASGI: the scope and ``http.request`` messages as a
|
|
2
|
+
``WebRequest``, the handler's ``WebResponse`` as ``http.response.*``
|
|
3
|
+
messages.
|
|
4
|
+
|
|
5
|
+
One task reads ``receive`` for the whole request. It buffers body chunks
|
|
6
|
+
until ``HIGH_WATER_MARK`` bytes wait unread, and an ``http.disconnect``
|
|
7
|
+
before the response is complete fires the request's signal, which
|
|
8
|
+
interrupts the app (a client abort) and stops a streamed body. While the
|
|
9
|
+
buffer is full nothing is read, so a disconnect is seen once the app reads
|
|
10
|
+
on.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
from collections import deque
|
|
17
|
+
from collections.abc import AsyncIterator
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from pyfect.http._field import is_field_value, is_token
|
|
21
|
+
from pyfect.http.abort import AbortController
|
|
22
|
+
from pyfect.http.asgi import _scope
|
|
23
|
+
from pyfect.http.asgi._types import Message, Receive, Scope, Send
|
|
24
|
+
from pyfect.http.web import WebRequest, WebResponse
|
|
25
|
+
from pyfect.http.web_handler import WebHandler
|
|
26
|
+
|
|
27
|
+
HIGH_WATER_MARK = 64 * 1024
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ClientDisconnectedError(ConnectionError):
|
|
31
|
+
"""The client went away before the request body ended."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class _Inbound:
|
|
35
|
+
__slots__ = (
|
|
36
|
+
"_changed",
|
|
37
|
+
"_chunks",
|
|
38
|
+
"_controller",
|
|
39
|
+
"_ended",
|
|
40
|
+
"_receive",
|
|
41
|
+
"_room",
|
|
42
|
+
"_size",
|
|
43
|
+
"_task",
|
|
44
|
+
"disconnected",
|
|
45
|
+
"responded",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def __init__(self, receive: Receive, controller: AbortController) -> None:
|
|
49
|
+
self._receive = receive
|
|
50
|
+
self._controller = controller
|
|
51
|
+
self._chunks: deque[bytes] = deque()
|
|
52
|
+
self._size = 0
|
|
53
|
+
self._ended = False
|
|
54
|
+
self._changed = asyncio.Event()
|
|
55
|
+
self._room = asyncio.Event()
|
|
56
|
+
self._room.set()
|
|
57
|
+
self.disconnected = False
|
|
58
|
+
self.responded = False
|
|
59
|
+
self._task = asyncio.get_running_loop().create_task(self._pump())
|
|
60
|
+
|
|
61
|
+
async def _pump(self) -> None:
|
|
62
|
+
while True:
|
|
63
|
+
await self._room.wait()
|
|
64
|
+
message = await self._receive()
|
|
65
|
+
if message["type"] == "http.disconnect":
|
|
66
|
+
self.disconnected = True
|
|
67
|
+
self._changed.set()
|
|
68
|
+
if not self.responded:
|
|
69
|
+
self._controller.abort()
|
|
70
|
+
return
|
|
71
|
+
if message["type"] != "http.request" or self._ended:
|
|
72
|
+
continue
|
|
73
|
+
chunk: bytes = message.get("body", b"")
|
|
74
|
+
if chunk:
|
|
75
|
+
self._chunks.append(chunk)
|
|
76
|
+
self._size += len(chunk)
|
|
77
|
+
if self._size >= HIGH_WATER_MARK:
|
|
78
|
+
self._room.clear()
|
|
79
|
+
if not message.get("more_body", False):
|
|
80
|
+
self._ended = True
|
|
81
|
+
self._changed.set()
|
|
82
|
+
|
|
83
|
+
async def chunks(self) -> AsyncIterator[bytes]:
|
|
84
|
+
while True:
|
|
85
|
+
if self._chunks:
|
|
86
|
+
chunk = self._chunks.popleft()
|
|
87
|
+
self._size -= len(chunk)
|
|
88
|
+
if self._size < HIGH_WATER_MARK:
|
|
89
|
+
self._room.set()
|
|
90
|
+
yield chunk
|
|
91
|
+
elif self._ended:
|
|
92
|
+
return
|
|
93
|
+
elif self.disconnected:
|
|
94
|
+
raise ClientDisconnectedError("the client disconnected")
|
|
95
|
+
else:
|
|
96
|
+
self._changed.clear()
|
|
97
|
+
await self._changed.wait()
|
|
98
|
+
|
|
99
|
+
def close(self) -> None:
|
|
100
|
+
self._task.cancel()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def request_of(
|
|
104
|
+
scope: Scope, body: AsyncIterator[bytes] | None, controller: AbortController
|
|
105
|
+
) -> WebRequest | None:
|
|
106
|
+
"""The request, or ``None`` when its target is not origin-form."""
|
|
107
|
+
lines = _scope.header_lines(scope)
|
|
108
|
+
url = _scope.url(scope, lines)
|
|
109
|
+
if url is None:
|
|
110
|
+
return None
|
|
111
|
+
return WebRequest(
|
|
112
|
+
url,
|
|
113
|
+
method=scope.get("method", "GET"),
|
|
114
|
+
headers=lines,
|
|
115
|
+
body=body if _scope.has_body(scope, lines) else None,
|
|
116
|
+
signal=controller.signal,
|
|
117
|
+
remote_address=_scope.remote_address(scope),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class InvalidResponseHeaderError(ValueError):
|
|
122
|
+
"""A response header that cannot be sent (Node's ``writeHead`` throws)."""
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def encode_headers(lines: tuple[tuple[str, str], ...]) -> list[tuple[bytes, bytes]]:
|
|
126
|
+
"""The header lines as ASGI sends them; an invalid one raises
|
|
127
|
+
``InvalidResponseHeaderError``."""
|
|
128
|
+
encoded: list[tuple[bytes, bytes]] = []
|
|
129
|
+
for name, value in lines:
|
|
130
|
+
if not is_token(name) or not is_field_value(value):
|
|
131
|
+
raise InvalidResponseHeaderError(f"Invalid response header: {name}")
|
|
132
|
+
try:
|
|
133
|
+
encoded.append((name.encode("latin-1"), value.encode("latin-1")))
|
|
134
|
+
except UnicodeEncodeError as cause:
|
|
135
|
+
raise InvalidResponseHeaderError(f"Invalid response header: {name}") from cause
|
|
136
|
+
return encoded
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def _aclose(chunks: AsyncIterator[bytes]) -> None:
|
|
140
|
+
close: Any = getattr(chunks, "aclose", None)
|
|
141
|
+
if close is not None:
|
|
142
|
+
await close()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
async def send_response(send: Send, response: WebResponse, inbound: _Inbound) -> None:
|
|
146
|
+
try:
|
|
147
|
+
headers = encode_headers(response.header_lines)
|
|
148
|
+
except InvalidResponseHeaderError:
|
|
149
|
+
# Answered with a 500, then reported by the server.
|
|
150
|
+
await send({"type": "http.response.start", "status": 500, "headers": []})
|
|
151
|
+
inbound.responded = True
|
|
152
|
+
await send({"type": "http.response.body", "body": b""})
|
|
153
|
+
await _aclose(response.chunks())
|
|
154
|
+
raise
|
|
155
|
+
start: Message = {"type": "http.response.start", "status": response.status, "headers": headers}
|
|
156
|
+
body = response.body
|
|
157
|
+
if body is None or isinstance(body, bytes):
|
|
158
|
+
await send(start)
|
|
159
|
+
inbound.responded = True
|
|
160
|
+
await send({"type": "http.response.body", "body": body or b""})
|
|
161
|
+
return
|
|
162
|
+
chunks = response.chunks()
|
|
163
|
+
try:
|
|
164
|
+
await send(start)
|
|
165
|
+
async for chunk in chunks:
|
|
166
|
+
if inbound.disconnected:
|
|
167
|
+
return
|
|
168
|
+
if chunk:
|
|
169
|
+
await send({"type": "http.response.body", "body": chunk, "more_body": True})
|
|
170
|
+
inbound.responded = True
|
|
171
|
+
await send({"type": "http.response.body", "body": b""})
|
|
172
|
+
finally:
|
|
173
|
+
await _aclose(chunks)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
async def serve_http(
|
|
177
|
+
handler: WebHandler,
|
|
178
|
+
scope: Scope,
|
|
179
|
+
receive: Receive,
|
|
180
|
+
send: Send,
|
|
181
|
+
controller: AbortController,
|
|
182
|
+
) -> None:
|
|
183
|
+
"""Answer one ``http`` scope with ``handler``. ``controller`` aborts the
|
|
184
|
+
request: it interrupts the app and ends a streamed body."""
|
|
185
|
+
inbound = _Inbound(receive, controller)
|
|
186
|
+
try:
|
|
187
|
+
request = request_of(scope, inbound.chunks(), controller)
|
|
188
|
+
response = await handler(request) if request is not None else WebResponse(status=400)
|
|
189
|
+
await send_response(send, response, inbound)
|
|
190
|
+
except asyncio.CancelledError:
|
|
191
|
+
controller.abort()
|
|
192
|
+
raise
|
|
193
|
+
except OSError:
|
|
194
|
+
# The server could not send: the client is gone.
|
|
195
|
+
controller.abort()
|
|
196
|
+
except Exception:
|
|
197
|
+
# A body stopped because the client went away ends quietly.
|
|
198
|
+
if not controller.signal.aborted:
|
|
199
|
+
raise
|
|
200
|
+
finally:
|
|
201
|
+
inbound.close()
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Another framework's ASGI application (FastAPI, Starlette...) as an HTTP
|
|
2
|
+
app: ``from_asgi`` answers each request with it, and ``lifespan`` runs its
|
|
3
|
+
startup and shutdown with a scope. Both await the application on the
|
|
4
|
+
fiber's asyncio loop, so they need an asyncio run (``run_main``,
|
|
5
|
+
``run_async``, the ASGI and uvicorn servers)."""
|
|
6
|
+
# pyright: reportPrivateUsage=false
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
from collections.abc import AsyncIterator, Mapping
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any
|
|
14
|
+
from urllib.parse import urlsplit
|
|
15
|
+
|
|
16
|
+
from pyfect._core import Effect, _WithFiber
|
|
17
|
+
from pyfect._fiber import Fiber
|
|
18
|
+
from pyfect.effect import fail, from_async, try_async
|
|
19
|
+
from pyfect.errors import TaggedError
|
|
20
|
+
from pyfect.http._foreign_path import decode_path
|
|
21
|
+
from pyfect.http.abort import AbortController
|
|
22
|
+
from pyfect.http.asgi._types import AsgiApplication, Message
|
|
23
|
+
from pyfect.http.server_error import HttpServerError, InternalError
|
|
24
|
+
from pyfect.http.server_request import HttpServerRequest, ServerRequest, to_url
|
|
25
|
+
from pyfect.http.server_response import HttpServerResponse
|
|
26
|
+
from pyfect.http.web import WebResponse
|
|
27
|
+
from pyfect.http.web_handler import request_to_web, response_from_web
|
|
28
|
+
from pyfect.result import Err, Ok
|
|
29
|
+
from pyfect.scope import Scope, acquire_release
|
|
30
|
+
|
|
31
|
+
_ASGI = {"version": "3.0", "spec_version": "2.4"}
|
|
32
|
+
_QUEUED_CHUNKS = 16
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AsgiNoResponseError(RuntimeError):
|
|
36
|
+
"""The application returned without starting a response."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _root_path(request: ServerRequest) -> tuple[str, str]:
|
|
40
|
+
"""The original path, and the part of it a router's prefix removed."""
|
|
41
|
+
original = urlsplit(request.original_url).path or "/"
|
|
42
|
+
path = request.url.split("?", 1)[0] or "/"
|
|
43
|
+
if path == "/":
|
|
44
|
+
return original, original.removesuffix("/")
|
|
45
|
+
if original.endswith(path):
|
|
46
|
+
return original, original[: len(original) - len(path)]
|
|
47
|
+
return original, ""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _scope(request: ServerRequest, state: Mapping[str, Any] | None) -> dict[str, Any]:
|
|
51
|
+
original, root = _root_path(request)
|
|
52
|
+
query = request.url.split("?", 1)[1] if "?" in request.url else ""
|
|
53
|
+
url = to_url(request)
|
|
54
|
+
scheme = url.scheme if url is not None else "http"
|
|
55
|
+
server = (
|
|
56
|
+
None
|
|
57
|
+
if url is None or url.hostname is None
|
|
58
|
+
else (url.hostname, url.port or (443 if scheme == "https" else 80))
|
|
59
|
+
)
|
|
60
|
+
remote = request.remote_address
|
|
61
|
+
scope: dict[str, Any] = {
|
|
62
|
+
"type": "http",
|
|
63
|
+
"asgi": _ASGI,
|
|
64
|
+
"http_version": "1.1",
|
|
65
|
+
"method": request.method,
|
|
66
|
+
"scheme": scheme,
|
|
67
|
+
"path": decode_path(original),
|
|
68
|
+
"raw_path": original.encode("latin-1", "replace"),
|
|
69
|
+
"root_path": decode_path(root),
|
|
70
|
+
"query_string": query.encode("latin-1", "replace"),
|
|
71
|
+
"headers": [
|
|
72
|
+
(name.encode("latin-1"), value.encode("latin-1"))
|
|
73
|
+
for name, value in request.headers.items()
|
|
74
|
+
],
|
|
75
|
+
"client": None if remote is None else (remote, 0),
|
|
76
|
+
"server": server,
|
|
77
|
+
"extensions": {},
|
|
78
|
+
}
|
|
79
|
+
if state is not None:
|
|
80
|
+
scope["state"] = dict(state)
|
|
81
|
+
return scope
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class _Exchange:
|
|
85
|
+
"""One call of the application: ``receive`` reads the request body,
|
|
86
|
+
``send`` hands the response over."""
|
|
87
|
+
|
|
88
|
+
def __init__(self, body: AsyncIterator[bytes] | None) -> None:
|
|
89
|
+
loop = asyncio.get_running_loop()
|
|
90
|
+
self._body = body
|
|
91
|
+
self._body_done = False
|
|
92
|
+
self.finished = asyncio.Event()
|
|
93
|
+
self.started: asyncio.Future[tuple[int, list[tuple[str, str]]]] = loop.create_future()
|
|
94
|
+
self.chunks: asyncio.Queue[bytes | None] = asyncio.Queue(_QUEUED_CHUNKS)
|
|
95
|
+
|
|
96
|
+
async def receive(self) -> Message:
|
|
97
|
+
if not self._body_done:
|
|
98
|
+
if self._body is not None:
|
|
99
|
+
try:
|
|
100
|
+
chunk = await anext(self._body)
|
|
101
|
+
except StopAsyncIteration:
|
|
102
|
+
pass
|
|
103
|
+
else:
|
|
104
|
+
return {"type": "http.request", "body": chunk, "more_body": True}
|
|
105
|
+
self._body_done = True
|
|
106
|
+
return {"type": "http.request", "body": b"", "more_body": False}
|
|
107
|
+
await self.finished.wait()
|
|
108
|
+
return {"type": "http.disconnect"}
|
|
109
|
+
|
|
110
|
+
async def send(self, message: Message) -> None:
|
|
111
|
+
if message["type"] == "http.response.start" and not self.started.done():
|
|
112
|
+
headers: list[tuple[bytes, bytes]] = message.get("headers", [])
|
|
113
|
+
self.started.set_result(
|
|
114
|
+
(
|
|
115
|
+
message["status"],
|
|
116
|
+
[(name.decode("latin-1"), value.decode("latin-1")) for name, value in headers],
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
elif message["type"] == "http.response.body" and not self.finished.is_set():
|
|
120
|
+
chunk: bytes = message.get("body", b"")
|
|
121
|
+
if chunk:
|
|
122
|
+
await self.chunks.put(chunk)
|
|
123
|
+
if not message.get("more_body", False):
|
|
124
|
+
self.finished.set()
|
|
125
|
+
await self.chunks.put(None)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
async def _body(exchange: _Exchange, task: asyncio.Task[None]) -> AsyncIterator[bytes]:
|
|
129
|
+
try:
|
|
130
|
+
while True:
|
|
131
|
+
getting = asyncio.ensure_future(exchange.chunks.get())
|
|
132
|
+
await asyncio.wait((getting, task), return_when=asyncio.FIRST_COMPLETED)
|
|
133
|
+
if not getting.done():
|
|
134
|
+
getting.cancel()
|
|
135
|
+
# The application ended (or raised) before ending the body.
|
|
136
|
+
task.result()
|
|
137
|
+
raise AsgiNoResponseError("the ASGI application did not end the response")
|
|
138
|
+
chunk = getting.result()
|
|
139
|
+
if chunk is None:
|
|
140
|
+
return
|
|
141
|
+
yield chunk
|
|
142
|
+
finally:
|
|
143
|
+
exchange.finished.set()
|
|
144
|
+
if not task.done():
|
|
145
|
+
task.cancel()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
async def _call(
|
|
149
|
+
app: AsgiApplication, scope: dict[str, Any], body: AsyncIterator[bytes] | None
|
|
150
|
+
) -> WebResponse:
|
|
151
|
+
exchange = _Exchange(body)
|
|
152
|
+
task = asyncio.ensure_future(app(scope, exchange.receive, exchange.send))
|
|
153
|
+
try:
|
|
154
|
+
await asyncio.wait((exchange.started, task), return_when=asyncio.FIRST_COMPLETED)
|
|
155
|
+
except asyncio.CancelledError:
|
|
156
|
+
exchange.finished.set()
|
|
157
|
+
task.cancel()
|
|
158
|
+
raise
|
|
159
|
+
if not exchange.started.done():
|
|
160
|
+
task.result()
|
|
161
|
+
raise AsgiNoResponseError("the ASGI application returned without a response")
|
|
162
|
+
status, headers = exchange.started.result()
|
|
163
|
+
return WebResponse(_body(exchange, task), status=status, headers=headers)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def from_asgi(
|
|
167
|
+
app: AsgiApplication, *, state: Mapping[str, Any] | None = None
|
|
168
|
+
) -> Effect[HttpServerResponse, HttpServerError, HttpServerRequest]:
|
|
169
|
+
"""An app answering with the ASGI application ``app`` (Effect's
|
|
170
|
+
``fromWebHandler`` for ASGI). Mounted under a router prefix
|
|
171
|
+
(``router.prefix_route``), the prefix is the scope's ``root_path``;
|
|
172
|
+
``state`` is the scope's ``state`` (what ``lifespan`` gives).
|
|
173
|
+
Interrupting the app cancels the call."""
|
|
174
|
+
|
|
175
|
+
def run(fiber: Fiber[Any, Any]) -> Effect[HttpServerResponse, HttpServerError]:
|
|
176
|
+
request = fiber._context.get_unsafe(HttpServerRequest)
|
|
177
|
+
controller = AbortController()
|
|
178
|
+
match request_to_web(request, signal=controller.signal, context=fiber._context):
|
|
179
|
+
case Err(reason):
|
|
180
|
+
return fail(HttpServerError(reason))
|
|
181
|
+
case Ok(web):
|
|
182
|
+
pass
|
|
183
|
+
scope = _scope(request, state)
|
|
184
|
+
body = None if web.body is None else web.chunks()
|
|
185
|
+
|
|
186
|
+
async def call() -> WebResponse:
|
|
187
|
+
try:
|
|
188
|
+
return await _call(app, scope, body)
|
|
189
|
+
except asyncio.CancelledError:
|
|
190
|
+
controller.abort()
|
|
191
|
+
raise
|
|
192
|
+
|
|
193
|
+
def on_error(cause: Exception) -> HttpServerError:
|
|
194
|
+
return HttpServerError(InternalError(request, "HttpApp.fromAsgi: Error in app", cause))
|
|
195
|
+
|
|
196
|
+
return try_async(call, on_error).map(response_from_web)
|
|
197
|
+
|
|
198
|
+
return _WithFiber(run)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@dataclass(frozen=True)
|
|
202
|
+
class AsgiLifespanError(TaggedError):
|
|
203
|
+
"""The application's lifespan startup failed."""
|
|
204
|
+
|
|
205
|
+
message: str
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class _Lifespan:
|
|
209
|
+
"""A running ``lifespan`` scope."""
|
|
210
|
+
|
|
211
|
+
def __init__(self, app: AsgiApplication) -> None:
|
|
212
|
+
self.state: dict[str, Any] = {}
|
|
213
|
+
self._inbox: asyncio.Queue[Message] = asyncio.Queue()
|
|
214
|
+
self._outbox: asyncio.Queue[Message] = asyncio.Queue()
|
|
215
|
+
scope = {"type": "lifespan", "asgi": _ASGI, "state": self.state}
|
|
216
|
+
self._task = asyncio.ensure_future(app(scope, self._inbox.get, self._outbox.put))
|
|
217
|
+
self._supported = True
|
|
218
|
+
|
|
219
|
+
async def _reply(self) -> Message | None:
|
|
220
|
+
"""The application's next message; ``None`` once it has ended."""
|
|
221
|
+
replying = asyncio.ensure_future(self._outbox.get())
|
|
222
|
+
await asyncio.wait((replying, self._task), return_when=asyncio.FIRST_COMPLETED)
|
|
223
|
+
if replying.done():
|
|
224
|
+
return replying.result()
|
|
225
|
+
replying.cancel()
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
async def startup(self) -> None:
|
|
229
|
+
await self._inbox.put({"type": "lifespan.startup"})
|
|
230
|
+
reply = await self._reply()
|
|
231
|
+
if reply is None:
|
|
232
|
+
# An application without lifespan support raises or returns.
|
|
233
|
+
self._supported = False
|
|
234
|
+
if not self._task.cancelled():
|
|
235
|
+
self._task.exception()
|
|
236
|
+
return
|
|
237
|
+
if reply["type"] == "lifespan.startup.failed":
|
|
238
|
+
raise AsgiLifespanError(reply.get("message", ""))
|
|
239
|
+
|
|
240
|
+
async def shutdown(self) -> None:
|
|
241
|
+
if self._supported and not self._task.done():
|
|
242
|
+
await self._inbox.put({"type": "lifespan.shutdown"})
|
|
243
|
+
await self._reply()
|
|
244
|
+
if not self._task.done():
|
|
245
|
+
self._task.cancel()
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def lifespan(app: AsgiApplication) -> Effect[dict[str, Any], AsgiLifespanError, Scope]:
|
|
249
|
+
"""Start ``app``'s lifespan (its startup handlers) and shut it down when
|
|
250
|
+
the scope closes; succeeds with the lifespan ``state`` to pass to
|
|
251
|
+
``from_asgi``. An application without lifespan support starts at once."""
|
|
252
|
+
|
|
253
|
+
async def start() -> _Lifespan:
|
|
254
|
+
running = _Lifespan(app)
|
|
255
|
+
await running.startup()
|
|
256
|
+
return running
|
|
257
|
+
|
|
258
|
+
def on_error(cause: Exception) -> AsgiLifespanError:
|
|
259
|
+
return cause if isinstance(cause, AsgiLifespanError) else AsgiLifespanError(str(cause))
|
|
260
|
+
|
|
261
|
+
return acquire_release(
|
|
262
|
+
try_async(start, on_error), lambda running, _exit: from_async(running.shutdown)
|
|
263
|
+
).map(lambda running: running.state)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""What a request's ASGI scope says: its target relative to the mount
|
|
2
|
+
point (``root_path``), absolute URL, headers and peer."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
|
|
8
|
+
from pyfect.http._authority import (
|
|
9
|
+
LOCALHOST,
|
|
10
|
+
Authority,
|
|
11
|
+
OriginForm,
|
|
12
|
+
absolute_url,
|
|
13
|
+
parse_authority,
|
|
14
|
+
parse_origin_form,
|
|
15
|
+
)
|
|
16
|
+
from pyfect.http.asgi._types import Scope
|
|
17
|
+
|
|
18
|
+
# What ``quote`` keeps of a path: RFC 3986 ``pchar``s and ``/``.
|
|
19
|
+
_PATH_SAFE = "/:@!$&'()*+,;=-._~%"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _strip_prefix(path: str, prefix: str) -> str:
|
|
23
|
+
if prefix and path.startswith(prefix) and path[len(prefix) : len(prefix) + 1] in ("", "/"):
|
|
24
|
+
return path[len(prefix) :] or "/"
|
|
25
|
+
return path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def target(scope: Scope) -> OriginForm | None:
|
|
29
|
+
"""The path and query, without the ``root_path`` the app is mounted at
|
|
30
|
+
(servers and Starlette's ``Mount`` keep it in ``path``); ``None`` when
|
|
31
|
+
the path has no leading ``/`` (a server may hand on a raw target)."""
|
|
32
|
+
root = quote(scope.get("root_path", ""), safe=_PATH_SAFE)
|
|
33
|
+
raw: bytes | None = scope.get("raw_path")
|
|
34
|
+
path = raw.decode("latin-1") if raw else quote(scope["path"], safe=_PATH_SAFE)
|
|
35
|
+
path = _strip_prefix(path.split("?", 1)[0], root) or "/"
|
|
36
|
+
query: bytes = scope.get("query_string", b"")
|
|
37
|
+
return parse_origin_form(f"{path}?{query.decode('latin-1')}" if query else path)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def header_lines(scope: Scope) -> list[tuple[str, str]]:
|
|
41
|
+
pairs: list[tuple[bytes, bytes]] = scope.get("headers", [])
|
|
42
|
+
return [(name.decode("latin-1").lower(), value.decode("latin-1")) for name, value in pairs]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _host(scope: Scope, lines: list[tuple[str, str]]) -> Authority:
|
|
46
|
+
"""The ``host`` header, or the server's address when there is none or
|
|
47
|
+
it is no authority (the routed path comes from the scope alone)."""
|
|
48
|
+
for name, value in lines:
|
|
49
|
+
if name == "host":
|
|
50
|
+
authority = parse_authority(value)
|
|
51
|
+
if authority is not None:
|
|
52
|
+
return authority
|
|
53
|
+
break
|
|
54
|
+
server: tuple[str, int | None] | None = scope.get("server")
|
|
55
|
+
if server is None:
|
|
56
|
+
return LOCALHOST
|
|
57
|
+
host, port = server
|
|
58
|
+
host = f"[{host}]" if ":" in host else host
|
|
59
|
+
return parse_authority(host if port is None else f"{host}:{port}") or LOCALHOST
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def url(scope: Scope, lines: list[tuple[str, str]]) -> str | None:
|
|
63
|
+
"""The absolute URL: ``http`` or ``https`` (``ws`` and ``wss`` too);
|
|
64
|
+
``None`` when the target is not origin-form (answered with 400)."""
|
|
65
|
+
scheme = "https" if scope.get("scheme") in ("https", "wss") else "http"
|
|
66
|
+
origin = target(scope)
|
|
67
|
+
return None if origin is None else absolute_url(scheme, _host(scope, lines), origin)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def remote_address(scope: Scope) -> str | None:
|
|
71
|
+
client: tuple[str, int] | None = scope.get("client")
|
|
72
|
+
return None if client is None else client[0]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def has_body(scope: Scope, lines: list[tuple[str, str]]) -> bool:
|
|
76
|
+
"""Whether the request has a body: framed by its headers over HTTP/1,
|
|
77
|
+
possible on any request with content over HTTP/2 and 3."""
|
|
78
|
+
for name, value in lines:
|
|
79
|
+
if name == "transfer-encoding":
|
|
80
|
+
return True
|
|
81
|
+
if name == "content-length":
|
|
82
|
+
return value.strip() != "0"
|
|
83
|
+
return scope.get("http_version", "1.1") not in ("1.0", "1.1") and scope.get("method") not in (
|
|
84
|
+
"GET",
|
|
85
|
+
"HEAD",
|
|
86
|
+
)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Requests of an ASGI connection (``http`` and ``websocket`` scopes)
|
|
2
|
+
answered by an app, each on its own fiber."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pyfect._core import Effect
|
|
9
|
+
from pyfect.context import Context
|
|
10
|
+
from pyfect.http.abort import AbortController
|
|
11
|
+
from pyfect.http.asgi._http import serve_http
|
|
12
|
+
from pyfect.http.asgi._types import Receive, Scope, Send
|
|
13
|
+
from pyfect.http.asgi._web_socket import serve_websocket
|
|
14
|
+
from pyfect.http.http_effect import HttpMiddleware
|
|
15
|
+
from pyfect.http.server_response import HttpServerResponse
|
|
16
|
+
from pyfect.http.web_handler import Fork, to_web_handler_with
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class UnsupportedScopeError(RuntimeError):
|
|
20
|
+
"""An ASGI scope type the app does not serve."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AsgiHandler:
|
|
24
|
+
"""An ASGI app answering ``http`` and ``websocket`` scopes with
|
|
25
|
+
``app``: ``fork`` starts each request's fiber with ``context`` and the
|
|
26
|
+
request."""
|
|
27
|
+
|
|
28
|
+
__slots__ = ("_app", "_context", "_fork", "_middleware", "_open", "_web")
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
app: Effect[HttpServerResponse, Any, Any],
|
|
33
|
+
middleware: HttpMiddleware | None,
|
|
34
|
+
context: Context[Any],
|
|
35
|
+
fork: Fork,
|
|
36
|
+
) -> None:
|
|
37
|
+
self._app = app
|
|
38
|
+
self._middleware = middleware
|
|
39
|
+
self._context = context
|
|
40
|
+
self._fork = fork
|
|
41
|
+
self._web = to_web_handler_with(context, fork=fork)(app, middleware)
|
|
42
|
+
self._open: set[AbortController] = set()
|
|
43
|
+
|
|
44
|
+
def end_responses(self) -> None:
|
|
45
|
+
"""Abort every ``http`` request in flight, on the server's loop: a
|
|
46
|
+
handler still running is interrupted, and a streamed body ends where
|
|
47
|
+
it is. A server calls it once ``graceful_shutdown_timeout`` has
|
|
48
|
+
passed, so an endless body (SSE) cannot hold its shutdown open."""
|
|
49
|
+
for controller in tuple(self._open):
|
|
50
|
+
controller.abort()
|
|
51
|
+
|
|
52
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
53
|
+
match scope["type"]:
|
|
54
|
+
case "http":
|
|
55
|
+
controller = AbortController()
|
|
56
|
+
self._open.add(controller)
|
|
57
|
+
try:
|
|
58
|
+
await serve_http(self._web, scope, receive, send, controller)
|
|
59
|
+
finally:
|
|
60
|
+
self._open.discard(controller)
|
|
61
|
+
case "websocket":
|
|
62
|
+
await serve_websocket(
|
|
63
|
+
scope,
|
|
64
|
+
receive,
|
|
65
|
+
send,
|
|
66
|
+
app=self._app,
|
|
67
|
+
middleware=self._middleware,
|
|
68
|
+
context=self._context,
|
|
69
|
+
fork=self._fork,
|
|
70
|
+
)
|
|
71
|
+
case other:
|
|
72
|
+
raise UnsupportedScopeError(f"unsupported ASGI scope type: {other!r}")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""The ASGI 3 interface (asgiref's ``Scope``, ``Receive``, ``Send`` and
|
|
2
|
+
``ASGI3Application``), loosely typed as servers send it."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from collections.abc import Awaitable, Callable, MutableMapping
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
type Scope = MutableMapping[str, Any]
|
|
10
|
+
type Message = MutableMapping[str, Any]
|
|
11
|
+
type Receive = Callable[[], Awaitable[Message]]
|
|
12
|
+
type Send = Callable[[Message], Awaitable[None]]
|
|
13
|
+
type AsgiApplication = Callable[[Scope, Receive, Send], Awaitable[None]]
|