graphrefly 0.7.0__tar.gz → 0.9.0__tar.gz
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.
- {graphrefly-0.7.0 → graphrefly-0.9.0}/CHANGELOG.md +16 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/PKG-INFO +1 -1
- {graphrefly-0.7.0 → graphrefly-0.9.0}/docs/optimizations.md +4 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/docs/roadmap.md +16 -12
- {graphrefly-0.7.0 → graphrefly-0.9.0}/pyproject.toml +2 -1
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/compat/trio_runner.py +1 -1
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/core/dynamic_node.py +14 -7
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/core/node.py +23 -12
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/core/protocol.py +1 -1
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/core/timer.py +5 -1
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/extra/adapters.py +18 -21
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/extra/cascading_cache.py +17 -10
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/extra/resilience.py +4 -3
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/graph/graph.py +14 -7
- graphrefly-0.9.0/src/graphrefly/integrations/django.py +584 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/integrations/fastapi.py +45 -17
- graphrefly-0.9.0/src/graphrefly/patterns/__init__.py +23 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/patterns/ai.py +2 -2
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/patterns/cqrs.py +29 -26
- graphrefly-0.9.0/src/graphrefly/patterns/graphspec.py +1212 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/patterns/reactive_layout/measurement_adapters.py +1 -1
- graphrefly-0.9.0/src/graphrefly/patterns/reduction.py +656 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_core.py +1 -1
- graphrefly-0.9.0/tests/test_django.py +436 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_fastapi.py +5 -3
- graphrefly-0.9.0/tests/test_graphspec.py +612 -0
- graphrefly-0.9.0/tests/test_patterns_reduction.py +419 -0
- graphrefly-0.9.0/tests/test_reduction.py +421 -0
- graphrefly-0.7.0/src/graphrefly/patterns/__init__.py +0 -5
- {graphrefly-0.7.0 → graphrefly-0.9.0}/.claude/skills/dev-dispatch/SKILL.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/.claude/skills/parity/SKILL.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/.claude/skills/qa/SKILL.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/.gemini/skills/dev-dispatch/SKILL.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/.gemini/skills/parity/SKILL.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/.github/workflows/pages.yml +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/.github/workflows/release.yml +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/.gitignore +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/.mise.toml +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/CLAUDE.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/CONTRIBUTING.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/GEMINI.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/LICENSE +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/README.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/archive/docs/DESIGN-ARCHIVE-INDEX.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/archive/docs/SESSION-access-control-actor-guard.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/archive/docs/SESSION-cross-repo-implementation-audit.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/archive/docs/SESSION-demo-test-strategy.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/archive/docs/SESSION-graphrefly-spec-design.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/archive/docs/SESSION-serialization-memory-footprint.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/archive/docs/SESSION-tier2-parity-nonlocal-forward-inner.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/archive/docs/SESSION-universal-reduction-layer.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/benchmarks/py-baseline.json +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/docs/ADAPTER-CONTRACT.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/docs/benchmark.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/docs/docs-guidance.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/docs/test-guidance.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/examples/README.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/examples/basic_counter.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/llms.txt +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/__init__.py +1 -1
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/compat/__init__.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/compat/async_utils.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/compat/asyncio_runner.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/core/__init__.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/core/cancellation.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/core/clock.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/core/guard.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/core/meta.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/core/runner.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/core/subgraph_locks.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/core/sugar.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/core/versioning.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/extra/__init__.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/extra/backoff.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/extra/backpressure.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/extra/checkpoint.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/extra/composite.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/extra/cron.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/extra/data_structures.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/extra/sources.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/extra/tier1.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/extra/tier2.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/graph/__init__.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/integrations/__init__.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/patterns/memory.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/patterns/messaging.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/patterns/orchestration.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/patterns/reactive_layout/__init__.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/patterns/reactive_layout/reactive_block_layout.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/patterns/reactive_layout/reactive_layout.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/src/graphrefly/py.typed +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/bench_core.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/conftest.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_adapter_contract.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_adapters_ingest.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_adapters_storage.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_backpressure.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_cascading_cache.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_concurrency.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_dynamic_node.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_edge_cases.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_extra_composite.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_extra_data_structures.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_extra_resilience.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_extra_sources.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_extra_sources_http.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_extra_tier1.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_extra_tier2.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_graph.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_guard.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_measurement_adapters.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_patterns_ai.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_patterns_cqrs.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_patterns_memory.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_patterns_messaging.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_patterns_orchestration.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_perf_smoke.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_protocol.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_reactive_block_layout.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_reactive_layout.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_regressions.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_runner.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_smoke.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_sugar.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/tests/test_versioning.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/.gitignore +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/README.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/astro.config.mjs +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/content.config.ts +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/package.json +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/pnpm-lock.yaml +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/public/llms.txt +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/py-api-sidebar.mjs +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/scripts/gen_api_docs.py +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/scripts/sync-docs.mjs +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/components/GraphreflyHero.astro +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/components/Header.astro +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/components/MobileMenuFooter.astro +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/components/PyodidePlayground.tsx +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/components/Sidebar.astro +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/components/SiteTitle.astro +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/BackoffPreset.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/BackoffStrategy.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/CheckpointAdapter.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/CircuitBreaker.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/CircuitOpenError.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/DeferWhen.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/DictCheckpointAdapter.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/DistillBundle.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/EmitStrategy.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/Extraction.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/FileCheckpointAdapter.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/HttpBundle.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/JitterMode.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/MemoryCheckpointAdapter.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/Message.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/MessageType.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/Messages.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/NodeActions.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/NodeFn.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/NodeImpl.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/NodeStatus.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/PipeOperator.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/PubSubHub.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/ReactiveIndexBundle.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/ReactiveListBundle.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/ReactiveLogBundle.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/ReactiveMapBundle.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/SqliteCheckpointAdapter.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/SubscribeHints.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/TokenBucket.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/VerifiableBundle.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/Versioned.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/WithBreakerBundle.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/WithStatusBundle.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/audit.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/batch.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/buffer.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/buffer_count.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/buffer_time.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/cached.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/checkpoint_node_value.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/circuit_breaker.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/combine.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/concat.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/concat_map.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/constant.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/debounce.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/decorrelated_jitter.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/delay.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/derived.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/dispatch_messages.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/distill.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/distinct_until_changed.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/effect.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/element_at.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/emit_with_batch.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/empty.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/exhaust_map.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/exponential.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/fibonacci.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/filter.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/find.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/first.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/first_value_from.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/flat_map.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/for_each.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/from_any.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/from_async_iter.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/from_awaitable.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/from_cron.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/from_event_emitter.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/from_fs_watch.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/from_git_hook.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/from_http.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/from_iter.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/from_mcp.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/from_timer.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/from_webhook.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/from_websocket.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/gate.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/index.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/interval.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/is_batching.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/is_phase2_message.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/is_terminal_message.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/last.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/linear.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/log_slice.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/map.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/merge.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/message_tier.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/never.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/node.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/of.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/operator.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/pairwise.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/partition_for_batch.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/pausable.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/pipe.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/producer.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/propagates_to_meta.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/pubsub.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/race.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/rate_limiter.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/reactive_index.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/reactive_list.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/reactive_log.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/reactive_map.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/reduce.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/repeat.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/replay.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/rescue.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/resolve_backoff_preset.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/restore_graph_checkpoint.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/retry.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/sample.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/save_graph_checkpoint.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/scan.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/share.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/skip.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/start_with.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/state.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/subscribe.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/switch_map.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/take.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/take_until.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/take_while.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/tap.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/throttle.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/throw_error.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/timeout.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/to_array.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/to_list.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/to_sse.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/to_websocket.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/token_bucket.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/token_tracker.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/verifiable.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/window.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/window_count.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/window_time.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/with_breaker.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/with_latest_from.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/with_max_attempts.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/with_status.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/api/zip.md +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/index.mdx +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content/docs/lab/python.mdx +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/content.config.ts +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/env.d.ts +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/src/styles/custom.css +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/theme-prototypes.html +0 -0
- {graphrefly-0.7.0 → graphrefly-0.9.0}/website/tsconfig.json +0 -0
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v0.9.0 (2026-04-06)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- 8.3
|
|
10
|
+
([`39b0a9e`](https://github.com/graphrefly/graphrefly-py/commit/39b0a9e8e2a034a27352b2ea054182d58065903f))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## v0.8.0 (2026-04-06)
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
- 8.1 + django integration
|
|
18
|
+
([`5dcfd3d`](https://github.com/graphrefly/graphrefly-py/commit/5dcfd3d78561e7ae8cdc905f95f9b4922926881e))
|
|
19
|
+
|
|
20
|
+
|
|
5
21
|
## v0.7.0 (2026-04-06)
|
|
6
22
|
|
|
7
23
|
### Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graphrefly
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Reactive graph protocol for human + LLM co-operation. Composable nodes, glitch-free diamond resolution, two-phase push, durable streaming. Zero dependencies.
|
|
5
5
|
Project-URL: Homepage, https://py.graphrefly.dev
|
|
6
6
|
Project-URL: Repository, https://github.com/graphrefly/graphrefly-py
|
|
@@ -139,6 +139,10 @@ Union-find over node identity merges components when nodes list dependencies at
|
|
|
139
139
|
- **~~`to_json()` → `to_dict()` + `to_json_string()` (Phase 1.4, noted 2026-04-05, resolved 2026-04-05):~~** PY: `to_json()` renamed to `to_json_string()`; `to_dict()` added as alias of `snapshot()`; `to_json` alias removed (pre-1.0, no backward compat needed). TS: `toJSON()` renamed to `toObject()`; `toJSON()` kept as ECMAScript hook. Spec §3.8 updated.
|
|
140
140
|
- **~~Initial value, cached state, and equals interaction (all phases, noted 2026-04-05, resolved 2026-04-05):~~** Resolved with `_SENTINEL` / `NO_VALUE` sentinel (TS: `Symbol.for("graphrefly/NO_VALUE")`, PY: `_SENTINEL = object()`). Replaces the `_has_emitted_data` boolean flag entirely. One field (`_cached`) instead of two — impossible to desync. Key semantics: (1) When `initial` option is present (even as `None`), `_cached = initial` — `equals` IS called on first emission. (2) When `initial` is absent, `_cached = _SENTINEL` — first emission always DATA. (3) INVALIDATE / `reset_on_teardown` set `_cached = _SENTINEL`. (4) Resubscribable: terminal reset now also sets `_cached = _SENTINEL` — new subscriber always gets DATA. (5) Reconnect: cache retained → same-value emits RESOLVED — correct. (6) `get()` returns `None` when `_cached is _SENTINEL`. Spec §2.5 updated.
|
|
141
141
|
- **Auto-edge registration is local-only (Phase 1.1, noted 2026-04-05):** `Graph.add()` auto-registers edges for deps within the same `Graph` instance only. Cross-subgraph deps still require explicit `connect()`. Consistent with spec: cross-subgraph edges are explicit wiring.
|
|
142
|
+
- **GraphSpec cross-language parity (Phase 8.3, noted 2026-04-06, QA 2026-04-06):** Both TS and PY implement `compileSpec`/`compile_spec`, `decompileGraph`/`decompile_graph`, `llmCompose`/`llm_compose`, `llmRefine`/`llm_refine`, `specDiff`/`spec_diff`, and `validateSpec`/`validate_spec` in `patterns/graphspec`. Key alignment: (1) `GraphSpec` schema is identical JSON shape — `nodes`, `templates`, `feedback` top-level keys. TS uses TypeScript types; PY uses TypedDict. (2) `compile_spec` resolves nodes in dependency order (state/producer first, then derived/effect/operator). Catalog is passed explicitly (`GraphSpecCatalog`) — no global registry. (3) Template instantiation creates mounted subgraphs via `graph.mount()`. `$param` bindings resolve to top-level nodes. (4) Feedback edges wire via §8.1 `feedback()` primitive. (5) `decompile_graph` uses `describe(detail="standard")`, skips `__meta__` and `__feedback_*` internal nodes. Template detection via meta-based recovery (primary) + structural fingerprinting (fallback; includes dep names for accuracy). (6) `spec_diff` is pure JSON comparison — template-aware, feedback-aware. (7) LLM APIs (`llm_compose`/`llm_refine`) share identical system prompt and validation pipeline. (8) `validate_spec` checks bind targets exist in outer nodes, rejects feedback self-cycles, validates template param completeness. QA fixes: idempotent unsub in feedback(), deterministic output node selection in decompile, `contextlib.suppress` for connect dedup. Both repos: 35 tests each.
|
|
143
|
+
- **Feedback bare DATA to reentry/counter — deferred to 8.2 (Phase 8.1, noted 2026-04-06, decided 2026-04-06):** Both TS and PY `feedback()` send bare `[DATA]` to `reentry` and `counter` nodes without a preceding `DIRTY`. This is a deliberate protocol shortcut: the feedback subscriber operates outside the normal two-phase push because it is a subscribe-based bridge (not a node). Acceptable because feedback reentry targets are always state nodes (which re-derive DIRTY internally via `down()`). Will be resolved when feedback is rearchitected as a graph-visible bridge node in 8.2 — at that point the bridge node participates in two-phase push naturally.
|
|
144
|
+
- **`llm_compose`/`llm_refine` sync (PY) vs async (TS) — intentional divergence (Phase 8.3, noted 2026-04-06):** PY: synchronous, adapter must return `LLMResponse` directly. TS: `async function` returning `Promise<GraphSpec>`. PY design invariant: no `async def` / `Awaitable` in public APIs. TS spec §5.10 allows `await` at system boundaries (LLM adapter is external I/O, not reactive scheduling). Both are correct for their language idiom. Adapter contracts differ: PY adapters must be sync; TS adapters return Promise-compatible values.
|
|
145
|
+
- **Reduction primitives cross-language parity (Phase 8.1, noted 2026-04-06, QA 2026-04-06):** Both TS and PY implement `stratify`, `funnel`, `feedback`, `budget_gate`/`budgetGate`, and `scorer` in `patterns/reduction`. All five follow the orchestration factory pattern (`_base_meta`, `_register_step`). Key alignment: (1) `stratify` buffers DIRTY until DATA arrives — on classifier miss, emits `[DIRTY, RESOLVED]` to preserve spec §1.3.1 (both). (2) `funnel` bridges stages via `subscribe` forwarding DIRTY/DATA/RESOLVED/COMPLETE/ERROR to preserve two-phase protocol. TODO(8.2): replace with graph-visible bridge nodes. (3) `feedback` counter node is source of truth (resettable via `graph.set()`); uses `continue` (not `return`) on max_iterations so remaining batch messages process. Counter name is `__feedback_<condition>` to support multiple loops per graph. (4) `budget_gate`/`budgetGate` force-flushes all buffered items on terminal regardless of budget; sends RESUME before terminal if paused; forwards constraint ERROR downstream, silences constraint COMPLETE, forwards unknown constraint types via default. (5) `scorer` coerces `None`/`undefined` to 0 before multiplication (no TypeError/NaN divergence). TS `ScoredItem` is a plain object; PY `ScoredItem` is a class with `__slots__` and `__eq__`. Meta keys: `reduction: True`, `reduction_type: "<name>"`. Both repos: 22 tests each.
|
|
142
146
|
|
|
143
147
|
---
|
|
144
148
|
|
|
@@ -293,7 +293,7 @@ Composition layer over 3.2 (`reactive_log`), 4.1 (sagas), 4.2 (event bus), 4.3 (
|
|
|
293
293
|
### 5.1 — Framework compat
|
|
294
294
|
|
|
295
295
|
- [x] FastAPI integration
|
|
296
|
-
- [
|
|
296
|
+
- [x] Django integration
|
|
297
297
|
- [x] asyncio / trio Runner protocol
|
|
298
298
|
- [x] Async utilities: `to_async_iter`, `first_value_from_async`, `settled`
|
|
299
299
|
|
|
@@ -487,11 +487,11 @@ Reusable patterns for taking heterogeneous massive inputs and producing prioriti
|
|
|
487
487
|
|
|
488
488
|
Composable building blocks between sources and sinks.
|
|
489
489
|
|
|
490
|
-
- [
|
|
491
|
-
- [
|
|
492
|
-
- [
|
|
493
|
-
- [
|
|
494
|
-
- [
|
|
490
|
+
- [x] `stratify(source, rules)` → Graph — route input to different reduction branches based on classifier fn. Each branch gets independent operator chains. Rules are reactive — an LLM can rewrite them at runtime.
|
|
491
|
+
- [x] `funnel(sources, stages)` → Graph — multi-source merge with sequential reduction stages. Each stage is a named subgraph. Stages are pluggable — swap a stage by graph composition.
|
|
492
|
+
- [x] `feedback(graph, condition, reentry)` → Graph — introduce a cycle: when condition node fires, route output back to reentry point. Bounded by max iterations + budget constraints.
|
|
493
|
+
- [x] `budget_gate(source, constraints)` → Node — pass-through respecting reactive constraint nodes (token budget, network IO, cost ceiling). Backpressure via PAUSE/RESUME.
|
|
494
|
+
- [x] `scorer(sources, weights)` → Node — reactive multi-signal scoring. Weights are nodes (LLM or human can adjust live). Output: sorted, prioritized items with full score breakdown in meta.
|
|
495
495
|
|
|
496
496
|
### 8.2 — Domain templates (opinionated Graph factories)
|
|
497
497
|
|
|
@@ -501,15 +501,19 @@ Pre-wired graphs for common "info → action" domains. Users fork/extend.
|
|
|
501
501
|
- [ ] `issue_tracker_graph(opts)` → Graph — findings → extraction → verifiable assertions → regression detection → distillation → prioritized queue
|
|
502
502
|
- [ ] `content_moderation_graph(opts)` → Graph — ingest → LLM classification → human review → feedback → policy refinement
|
|
503
503
|
- [ ] `data_quality_graph(opts)` → Graph — DB/API ingest → schema validation → anomaly detection → drift alerting → remediation suggestions
|
|
504
|
+
- [ ] Rearchitect `feedback()` as graph-visible bridge node (replaces subscribe-based shortcut; enables proper DIRTY→DATA two-phase on reentry/counter; resolves bare-DATA protocol gap)
|
|
505
|
+
- [ ] Rearchitect `funnel()` bridges as graph-visible nodes (replaces subscribe forwarding; resolves §5.9 imperative trigger violation + teardown leak)
|
|
506
|
+
- [ ] `stratify` two-dep gating: gate classification on both source and rules settling (eliminates stale-rules race when both updated in same `batch()`)
|
|
504
507
|
|
|
505
508
|
### 8.3 — LLM graph composition
|
|
506
509
|
|
|
507
|
-
- [
|
|
508
|
-
- [
|
|
509
|
-
- [
|
|
510
|
-
- [
|
|
511
|
-
- [
|
|
512
|
-
- [
|
|
510
|
+
- [x] `GraphSpec` schema — JSON schema for declarative graph topology. Serializable, diffable.
|
|
511
|
+
- [x] `compile_spec(spec, catalog)` → Graph — instantiate from spec (dep-order resolution, templates via `mount()`, feedback via §8.1)
|
|
512
|
+
- [x] `decompile_graph(graph)` → GraphSpec — extract spec from running graph (meta-based + structural fingerprint template detection)
|
|
513
|
+
- [x] `llm_compose(problem, adapter, opts)` → GraphSpec — LLM generates topology from natural language
|
|
514
|
+
- [x] `llm_refine(spec, feedback, adapter)` → GraphSpec — LLM modifies existing topology
|
|
515
|
+
- [x] `spec_diff(spec_a, spec_b)` — structural diff between specs (template-aware, feedback-aware)
|
|
516
|
+
- [x] `validate_spec(spec)` → GraphSpecValidation — structural validation (types, deps, templates, feedback, bind targets)
|
|
513
517
|
|
|
514
518
|
### 8.4 — Audit & accountability
|
|
515
519
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "graphrefly"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.9.0"
|
|
4
4
|
description = "Reactive graph protocol for human + LLM co-operation. Composable nodes, glitch-free diamond resolution, two-phase push, durable streaming. Zero dependencies."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -60,6 +60,7 @@ dev = [
|
|
|
60
60
|
"ruff>=0.9",
|
|
61
61
|
"mypy>=1.14",
|
|
62
62
|
"croniter>=2.0",
|
|
63
|
+
"django>=4.2",
|
|
63
64
|
"fastapi>=0.100",
|
|
64
65
|
"httpx>=0.24",
|
|
65
66
|
"pillow>=12.2.0",
|
|
@@ -13,10 +13,19 @@ import threading
|
|
|
13
13
|
from collections.abc import Callable, Mapping
|
|
14
14
|
from contextlib import suppress
|
|
15
15
|
from types import MappingProxyType
|
|
16
|
-
from typing import Any
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from graphrefly.core.guard import MutationRecord
|
|
17
20
|
|
|
18
21
|
from graphrefly.core.node import _SENTINEL
|
|
19
|
-
from graphrefly.core.protocol import
|
|
22
|
+
from graphrefly.core.protocol import (
|
|
23
|
+
Messages,
|
|
24
|
+
MessageType,
|
|
25
|
+
emit_with_batch,
|
|
26
|
+
message_tier,
|
|
27
|
+
propagates_to_meta,
|
|
28
|
+
)
|
|
20
29
|
|
|
21
30
|
# ---------------------------------------------------------------------------
|
|
22
31
|
# Public types
|
|
@@ -184,7 +193,7 @@ class DynamicNodeImpl[T]:
|
|
|
184
193
|
self._on_resubscribe = on_resubscribe
|
|
185
194
|
self._auto_complete = complete_when_deps_complete
|
|
186
195
|
self._describe_kind = describe_kind
|
|
187
|
-
self._last_mutation:
|
|
196
|
+
self._last_mutation: MutationRecord | None = None
|
|
188
197
|
self._resubscribable = resubscribable
|
|
189
198
|
self._reset_on_teardown = reset_on_teardown
|
|
190
199
|
self._thread_safe = bool(thread_safe)
|
|
@@ -255,7 +264,7 @@ class DynamicNodeImpl[T]:
|
|
|
255
264
|
return self._meta
|
|
256
265
|
|
|
257
266
|
@property
|
|
258
|
-
def last_mutation(self) ->
|
|
267
|
+
def last_mutation(self) -> MutationRecord | None:
|
|
259
268
|
return self._last_mutation
|
|
260
269
|
|
|
261
270
|
@property
|
|
@@ -503,9 +512,7 @@ class DynamicNodeImpl[T]:
|
|
|
503
512
|
|
|
504
513
|
# singleDep DIRTY skip optimization
|
|
505
514
|
if self._can_skip_dirty():
|
|
506
|
-
has_phase2 = any(
|
|
507
|
-
m[0] is MessageType.DATA or m[0] is MessageType.RESOLVED for m in messages
|
|
508
|
-
)
|
|
515
|
+
has_phase2 = any(message_tier(m[0]) == 2 for m in messages)
|
|
509
516
|
if has_phase2:
|
|
510
517
|
filtered = [m for m in messages if m[0] is not MessageType.DIRTY]
|
|
511
518
|
if filtered:
|
|
@@ -14,10 +14,17 @@ from graphrefly.core.guard import (
|
|
|
14
14
|
Actor,
|
|
15
15
|
GuardAction,
|
|
16
16
|
GuardDenied,
|
|
17
|
+
MutationRecord,
|
|
17
18
|
normalize_actor,
|
|
18
19
|
record_mutation,
|
|
19
20
|
)
|
|
20
|
-
from graphrefly.core.protocol import
|
|
21
|
+
from graphrefly.core.protocol import (
|
|
22
|
+
Messages,
|
|
23
|
+
MessageType,
|
|
24
|
+
emit_with_batch,
|
|
25
|
+
message_tier,
|
|
26
|
+
propagates_to_meta,
|
|
27
|
+
)
|
|
21
28
|
from graphrefly.core.subgraph_locks import (
|
|
22
29
|
acquire_subgraph_write_lock_with_defer,
|
|
23
30
|
ensure_registered,
|
|
@@ -253,10 +260,10 @@ class NodeImpl[T]:
|
|
|
253
260
|
msg = "node option 'guard' must be callable or None"
|
|
254
261
|
raise TypeError(msg)
|
|
255
262
|
self._guard: Callable[[Actor, GuardAction], bool] | None = raw_guard
|
|
256
|
-
self._last_mutation:
|
|
263
|
+
self._last_mutation: MutationRecord | None = None
|
|
257
264
|
|
|
258
265
|
self._cache_lock = threading.Lock() if self._thread_safe else None
|
|
259
|
-
self._cached: Any = opts
|
|
266
|
+
self._cached: Any = opts.get("initial", _SENTINEL)
|
|
260
267
|
self._status: NodeStatus = "disconnected" if self._has_deps else "settled"
|
|
261
268
|
|
|
262
269
|
# Versioning (GRAPHREFLY-SPEC §7)
|
|
@@ -367,7 +374,7 @@ class NodeImpl[T]:
|
|
|
367
374
|
else:
|
|
368
375
|
self._cached = m[1] # type: ignore[misc]
|
|
369
376
|
if self._versioning is not None:
|
|
370
|
-
advance_version(self._versioning, m[1], self._hash_fn)
|
|
377
|
+
advance_version(self._versioning, m[1], self._hash_fn) # type: ignore[misc]
|
|
371
378
|
if t is MessageType.INVALIDATE:
|
|
372
379
|
# GRAPHREFLY-SPEC §1.2: clear cached state; do not auto-emit from here.
|
|
373
380
|
if self._cleanup is not None:
|
|
@@ -761,7 +768,7 @@ class NodeImpl[T]:
|
|
|
761
768
|
return None if v is _SENTINEL else v
|
|
762
769
|
|
|
763
770
|
@property
|
|
764
|
-
def last_mutation(self) ->
|
|
771
|
+
def last_mutation(self) -> MutationRecord | None:
|
|
765
772
|
"""Last non-internal ``write`` attribution (``actor``, ``timestamp_ns``), if any."""
|
|
766
773
|
return self._last_mutation
|
|
767
774
|
|
|
@@ -823,12 +830,7 @@ class NodeImpl[T]:
|
|
|
823
830
|
sink_messages = terminal_passthrough
|
|
824
831
|
self._handle_local_lifecycle(lifecycle_messages)
|
|
825
832
|
if self._can_skip_dirty():
|
|
826
|
-
has_phase2 =
|
|
827
|
-
for m in sink_messages:
|
|
828
|
-
t = m[0]
|
|
829
|
-
if t is MessageType.DATA or t is MessageType.RESOLVED:
|
|
830
|
-
has_phase2 = True
|
|
831
|
-
break
|
|
833
|
+
has_phase2 = any(message_tier(m[0]) == 2 for m in sink_messages)
|
|
832
834
|
if has_phase2:
|
|
833
835
|
filtered = [m for m in sink_messages if m[0] is not MessageType.DIRTY]
|
|
834
836
|
if filtered:
|
|
@@ -991,4 +993,13 @@ def node(
|
|
|
991
993
|
# Public alias for type hints
|
|
992
994
|
Node = NodeImpl
|
|
993
995
|
|
|
994
|
-
__all__ = [
|
|
996
|
+
__all__ = [
|
|
997
|
+
"NO_VALUE",
|
|
998
|
+
"Node",
|
|
999
|
+
"NodeActions",
|
|
1000
|
+
"NodeFn",
|
|
1001
|
+
"NodeImpl",
|
|
1002
|
+
"NodeStatus",
|
|
1003
|
+
"SubscribeHints",
|
|
1004
|
+
"node",
|
|
1005
|
+
]
|
|
@@ -359,7 +359,7 @@ def _emit_partition(
|
|
|
359
359
|
# skip partition_for_batch allocation entirely.
|
|
360
360
|
if len(messages) == 1:
|
|
361
361
|
t = messages[0][0]
|
|
362
|
-
if t
|
|
362
|
+
if message_tier(t) == 2:
|
|
363
363
|
if _should_defer_phase2(bs, defer_when):
|
|
364
364
|
|
|
365
365
|
def _emit_single() -> None:
|
|
@@ -5,6 +5,10 @@ from_timer creates a new Node per reset)."""
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
7
|
import threading
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Callable
|
|
8
12
|
|
|
9
13
|
|
|
10
14
|
class ResettableTimer:
|
|
@@ -16,7 +20,7 @@ class ResettableTimer:
|
|
|
16
20
|
self._timer: threading.Timer | None = None
|
|
17
21
|
self._lock = threading.Lock()
|
|
18
22
|
|
|
19
|
-
def start(self, delay_seconds: float, callback:
|
|
23
|
+
def start(self, delay_seconds: float, callback: Callable[[], Any]) -> None:
|
|
20
24
|
"""Schedule callback after delay_seconds. Cancels any pending timer."""
|
|
21
25
|
with self._lock:
|
|
22
26
|
if self._timer is not None:
|
|
@@ -2022,6 +2022,7 @@ def from_csv(
|
|
|
2022
2022
|
def drain() -> None:
|
|
2023
2023
|
try:
|
|
2024
2024
|
headers: list[str] | None = list(columns) if columns else None
|
|
2025
|
+
rows_iter: Iterable[list[str]]
|
|
2025
2026
|
if parse_line is not None:
|
|
2026
2027
|
rows_iter = (parse_line(line) for line in source)
|
|
2027
2028
|
else:
|
|
@@ -2475,7 +2476,7 @@ def from_nats(
|
|
|
2475
2476
|
async for msg in sub:
|
|
2476
2477
|
if not active[0]:
|
|
2477
2478
|
return
|
|
2478
|
-
actions.emit(_nats_msg_to_nats_message(msg, deserialize))
|
|
2479
|
+
actions.emit(_nats_msg_to_nats_message(msg, deserialize))
|
|
2479
2480
|
# Iterator exhausted — subscription closed (inline, matching TS pattern).
|
|
2480
2481
|
if active[0]:
|
|
2481
2482
|
actions.down([(MessageType.COMPLETE,)])
|
|
@@ -2501,7 +2502,7 @@ def from_nats(
|
|
|
2501
2502
|
for msg in sub_or_coro:
|
|
2502
2503
|
if not active[0]:
|
|
2503
2504
|
return
|
|
2504
|
-
actions.emit(_nats_msg_to_nats_message(msg, deserialize))
|
|
2505
|
+
actions.emit(_nats_msg_to_nats_message(msg, deserialize))
|
|
2505
2506
|
# Iterator exhausted — subscription closed.
|
|
2506
2507
|
if active[0]:
|
|
2507
2508
|
actions.down([(MessageType.COMPLETE,)])
|
|
@@ -2512,10 +2513,10 @@ def from_nats(
|
|
|
2512
2513
|
t = threading.Thread(target=_run, daemon=True)
|
|
2513
2514
|
t.start()
|
|
2514
2515
|
|
|
2515
|
-
def
|
|
2516
|
+
def _sync_cleanup() -> None:
|
|
2516
2517
|
active[0] = False
|
|
2517
2518
|
|
|
2518
|
-
return
|
|
2519
|
+
return _sync_cleanup
|
|
2519
2520
|
|
|
2520
2521
|
return node(start, describe_kind="producer", complete_when_deps_complete=False)
|
|
2521
2522
|
|
|
@@ -2769,12 +2770,12 @@ def to_rabbitmq(
|
|
|
2769
2770
|
if msg[0] is MessageType.DATA:
|
|
2770
2771
|
value = msg[1] if len(msg) > 1 else None
|
|
2771
2772
|
try:
|
|
2772
|
-
rk = routing_key_extractor(value)
|
|
2773
|
+
rk = routing_key_extractor(value)
|
|
2773
2774
|
except Exception as err:
|
|
2774
2775
|
handler(SinkTransportError(stage="routing_key", error=err, value=value))
|
|
2775
2776
|
return True
|
|
2776
2777
|
try:
|
|
2777
|
-
body = serialize(value)
|
|
2778
|
+
body = serialize(value)
|
|
2778
2779
|
except Exception as err:
|
|
2779
2780
|
handler(SinkTransportError(stage="serialize", error=err, value=value))
|
|
2780
2781
|
return True
|
|
@@ -2908,7 +2909,7 @@ def to_file(
|
|
|
2908
2909
|
if msg[0] is MessageType.DATA:
|
|
2909
2910
|
value = msg[1] if len(msg) > 1 else None
|
|
2910
2911
|
try:
|
|
2911
|
-
line = serialize(value)
|
|
2912
|
+
line = serialize(value)
|
|
2912
2913
|
except Exception as err:
|
|
2913
2914
|
handler(SinkTransportError(stage="serialize", error=err, value=value))
|
|
2914
2915
|
return True
|
|
@@ -3004,15 +3005,11 @@ def to_csv(
|
|
|
3004
3005
|
header_written[0] = True
|
|
3005
3006
|
header = delimiter.join(_escape_csv_field(c, delimiter) for c in columns)
|
|
3006
3007
|
data = delimiter.join(
|
|
3007
|
-
_escape_csv_field(cell_extractor(row, c), delimiter)
|
|
3008
|
-
for c in columns
|
|
3008
|
+
_escape_csv_field(cell_extractor(row, c), delimiter) for c in columns
|
|
3009
3009
|
)
|
|
3010
3010
|
return header + "\n" + data + "\n"
|
|
3011
3011
|
return (
|
|
3012
|
-
delimiter.join(
|
|
3013
|
-
_escape_csv_field(cell_extractor(row, c), delimiter) # type: ignore[misc]
|
|
3014
|
-
for c in columns
|
|
3015
|
-
)
|
|
3012
|
+
delimiter.join(_escape_csv_field(cell_extractor(row, c), delimiter) for c in columns)
|
|
3016
3013
|
+ "\n"
|
|
3017
3014
|
)
|
|
3018
3015
|
|
|
@@ -3108,7 +3105,7 @@ def to_clickhouse(
|
|
|
3108
3105
|
if msg[0] is MessageType.DATA:
|
|
3109
3106
|
value = msg[1] if len(msg) > 1 else None
|
|
3110
3107
|
try:
|
|
3111
|
-
transformed = transform(value)
|
|
3108
|
+
transformed = transform(value)
|
|
3112
3109
|
except Exception as err:
|
|
3113
3110
|
handler(SinkTransportError(stage="serialize", error=err, value=value))
|
|
3114
3111
|
return True
|
|
@@ -3224,7 +3221,7 @@ def to_s3(
|
|
|
3224
3221
|
else:
|
|
3225
3222
|
body = json.dumps(batch_data)
|
|
3226
3223
|
content_type = "application/json"
|
|
3227
|
-
key = key_generator(seq, wall_clock_ns())
|
|
3224
|
+
key = key_generator(seq, wall_clock_ns())
|
|
3228
3225
|
try:
|
|
3229
3226
|
client.put_object(Bucket=bucket, Key=key, Body=body, ContentType=content_type)
|
|
3230
3227
|
except Exception as err:
|
|
@@ -3254,7 +3251,7 @@ def to_s3(
|
|
|
3254
3251
|
if msg[0] is MessageType.DATA:
|
|
3255
3252
|
value = msg[1] if len(msg) > 1 else None
|
|
3256
3253
|
try:
|
|
3257
|
-
transformed = transform(value)
|
|
3254
|
+
transformed = transform(value)
|
|
3258
3255
|
except Exception as err:
|
|
3259
3256
|
handler(SinkTransportError(stage="serialize", error=err, value=value))
|
|
3260
3257
|
return True
|
|
@@ -3330,7 +3327,7 @@ def to_postgres(
|
|
|
3330
3327
|
if msg[0] is MessageType.DATA:
|
|
3331
3328
|
value = msg[1] if len(msg) > 1 else None
|
|
3332
3329
|
try:
|
|
3333
|
-
sql, params = to_sql(value, table)
|
|
3330
|
+
sql, params = to_sql(value, table)
|
|
3334
3331
|
except Exception as err:
|
|
3335
3332
|
handler(SinkTransportError(stage="serialize", error=err, value=value))
|
|
3336
3333
|
return True
|
|
@@ -3390,7 +3387,7 @@ def to_mongo(
|
|
|
3390
3387
|
if msg[0] is MessageType.DATA:
|
|
3391
3388
|
value = msg[1] if len(msg) > 1 else None
|
|
3392
3389
|
try:
|
|
3393
|
-
doc = to_document(value)
|
|
3390
|
+
doc = to_document(value)
|
|
3394
3391
|
except Exception as err:
|
|
3395
3392
|
handler(SinkTransportError(stage="serialize", error=err, value=value))
|
|
3396
3393
|
return True
|
|
@@ -3456,7 +3453,7 @@ def to_loki(
|
|
|
3456
3453
|
if msg[0] is MessageType.DATA:
|
|
3457
3454
|
value = msg[1] if len(msg) > 1 else None
|
|
3458
3455
|
try:
|
|
3459
|
-
line = to_line(value)
|
|
3456
|
+
line = to_line(value)
|
|
3460
3457
|
except Exception as err:
|
|
3461
3458
|
handler(SinkTransportError(stage="serialize", error=err, value=value))
|
|
3462
3459
|
return True
|
|
@@ -3523,7 +3520,7 @@ def to_tempo(
|
|
|
3523
3520
|
if msg[0] is MessageType.DATA:
|
|
3524
3521
|
value = msg[1] if len(msg) > 1 else None
|
|
3525
3522
|
try:
|
|
3526
|
-
spans = to_resource_spans(value)
|
|
3523
|
+
spans = to_resource_spans(value)
|
|
3527
3524
|
except Exception as err:
|
|
3528
3525
|
handler(SinkTransportError(stage="serialize", error=err, value=value))
|
|
3529
3526
|
return True
|
|
@@ -3792,7 +3789,7 @@ def to_sqlite(
|
|
|
3792
3789
|
if msg[0] is MessageType.DATA:
|
|
3793
3790
|
value = msg[1] if len(msg) > 1 else None
|
|
3794
3791
|
try:
|
|
3795
|
-
sql, params = to_sql(value, table)
|
|
3792
|
+
sql, params = to_sql(value, table)
|
|
3796
3793
|
except Exception as err:
|
|
3797
3794
|
handler(SinkTransportError(stage="serialize", error=err, value=value))
|
|
3798
3795
|
return True
|
|
@@ -5,6 +5,9 @@ from __future__ import annotations
|
|
|
5
5
|
from collections import OrderedDict
|
|
6
6
|
from typing import TYPE_CHECKING, Any, Generic, Protocol, TypeVar, runtime_checkable
|
|
7
7
|
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
|
|
8
11
|
from graphrefly.core.protocol import MessageType
|
|
9
12
|
from graphrefly.core.sugar import state
|
|
10
13
|
|
|
@@ -155,7 +158,7 @@ class CascadingCache(Generic[V]): # noqa: UP046
|
|
|
155
158
|
|
|
156
159
|
def __init__(
|
|
157
160
|
self,
|
|
158
|
-
tiers:
|
|
161
|
+
tiers: Sequence[CacheTier],
|
|
159
162
|
*,
|
|
160
163
|
max_size: int = 0,
|
|
161
164
|
eviction: EvictionPolicy | None = None,
|
|
@@ -171,7 +174,7 @@ class CascadingCache(Generic[V]): # noqa: UP046
|
|
|
171
174
|
for i in range(hit_tier):
|
|
172
175
|
tier = self._tiers[i]
|
|
173
176
|
if _tier_has_save(tier):
|
|
174
|
-
tier.save(key, value)
|
|
177
|
+
tier.save(key, value)
|
|
175
178
|
|
|
176
179
|
def _cascade(self, key: str, nd: Node[Any]) -> None:
|
|
177
180
|
for tier_index, tier in enumerate(self._tiers):
|
|
@@ -199,10 +202,10 @@ class CascadingCache(Generic[V]): # noqa: UP046
|
|
|
199
202
|
# Demote to deepest tier with save before evicting
|
|
200
203
|
for i in range(len(self._tiers) - 1, -1, -1):
|
|
201
204
|
if _tier_has_save(self._tiers[i]):
|
|
202
|
-
self._tiers[i].save(victim, value)
|
|
205
|
+
self._tiers[i].save(victim, value)
|
|
203
206
|
for j in range(i):
|
|
204
207
|
if _tier_has_clear(self._tiers[j]):
|
|
205
|
-
self._tiers[j].clear(victim)
|
|
208
|
+
self._tiers[j].clear(victim)
|
|
206
209
|
break
|
|
207
210
|
nd.down([(MessageType.TEARDOWN,)])
|
|
208
211
|
del self._entries[victim]
|
|
@@ -236,10 +239,9 @@ class CascadingCache(Generic[V]): # noqa: UP046
|
|
|
236
239
|
if self._write_through:
|
|
237
240
|
for tier in self._tiers:
|
|
238
241
|
if _tier_has_save(tier):
|
|
239
|
-
tier.save(key, value)
|
|
242
|
+
tier.save(key, value)
|
|
240
243
|
elif self._tiers and _tier_has_save(self._tiers[0]):
|
|
241
|
-
self._tiers[0].save(key, value)
|
|
242
|
-
|
|
244
|
+
self._tiers[0].save(key, value)
|
|
243
245
|
if key in self._entries:
|
|
244
246
|
self._entries[key].down([(MessageType.DATA, value)])
|
|
245
247
|
if self._eviction is not None:
|
|
@@ -271,7 +273,7 @@ class CascadingCache(Generic[V]): # noqa: UP046
|
|
|
271
273
|
self._eviction.delete(key)
|
|
272
274
|
for tier in self._tiers:
|
|
273
275
|
if _tier_has_clear(tier):
|
|
274
|
-
tier.clear(key)
|
|
276
|
+
tier.clear(key)
|
|
275
277
|
|
|
276
278
|
def has(self, key: str) -> bool:
|
|
277
279
|
"""Check if a key is in the in-memory entries."""
|
|
@@ -284,7 +286,7 @@ class CascadingCache(Generic[V]): # noqa: UP046
|
|
|
284
286
|
|
|
285
287
|
|
|
286
288
|
def cascading_cache(
|
|
287
|
-
tiers:
|
|
289
|
+
tiers: Sequence[CacheTier],
|
|
288
290
|
*,
|
|
289
291
|
max_size: int = 0,
|
|
290
292
|
eviction: EvictionPolicy | None = None,
|
|
@@ -398,5 +400,10 @@ def tiered_storage(
|
|
|
398
400
|
```
|
|
399
401
|
"""
|
|
400
402
|
tiers = [_CheckpointTier(a) for a in adapters]
|
|
401
|
-
inner = CascadingCache(
|
|
403
|
+
inner: CascadingCache[Any] = CascadingCache(
|
|
404
|
+
tiers,
|
|
405
|
+
max_size=max_size,
|
|
406
|
+
eviction=eviction,
|
|
407
|
+
write_through=True,
|
|
408
|
+
)
|
|
402
409
|
return TieredStorage(inner)
|
|
@@ -796,7 +796,7 @@ def fallback(source: Node[Any], fb: Any) -> Node[Any]:
|
|
|
796
796
|
actions.down([m])
|
|
797
797
|
|
|
798
798
|
unsub_holder[0] = source.subscribe(sink)
|
|
799
|
-
unsub = unsub_holder[0]
|
|
799
|
+
unsub: Callable[[], None] = unsub_holder[0] # type: ignore[assignment]
|
|
800
800
|
|
|
801
801
|
def cleanup() -> None:
|
|
802
802
|
unsub()
|
|
@@ -853,7 +853,8 @@ def timeout(source: Node[Any], timeout_ns: int) -> Node[Any]:
|
|
|
853
853
|
done[0] = True
|
|
854
854
|
# §5.10: ResettableTimer (not from_timer) — resettable
|
|
855
855
|
# deadline; from_timer adds Node overhead per DATA reset.
|
|
856
|
-
unsub_holder[0]
|
|
856
|
+
if unsub_holder[0] is not None:
|
|
857
|
+
unsub_holder[0]()
|
|
857
858
|
actions.down([(MessageType.ERROR, TimeoutError(timeout_ns))])
|
|
858
859
|
|
|
859
860
|
to_timer.start(delay_s, fire)
|
|
@@ -890,7 +891,7 @@ def timeout(source: Node[Any], timeout_ns: int) -> Node[Any]:
|
|
|
890
891
|
|
|
891
892
|
arm_timer()
|
|
892
893
|
unsub_holder[0] = source.subscribe(sink)
|
|
893
|
-
unsub = unsub_holder[0]
|
|
894
|
+
unsub: Callable[[], None] = unsub_holder[0] # type: ignore[assignment]
|
|
894
895
|
|
|
895
896
|
def cleanup() -> None:
|
|
896
897
|
done[0] = True
|
|
@@ -10,11 +10,11 @@ import threading
|
|
|
10
10
|
from collections import deque
|
|
11
11
|
from contextlib import contextmanager, suppress
|
|
12
12
|
from dataclasses import dataclass, field
|
|
13
|
-
from typing import TYPE_CHECKING, Any, ClassVar
|
|
13
|
+
from typing import TYPE_CHECKING, Any, ClassVar, cast
|
|
14
14
|
|
|
15
15
|
from graphrefly.core.clock import monotonic_ns
|
|
16
16
|
from graphrefly.core.guard import GuardDenied, normalize_actor
|
|
17
|
-
from graphrefly.core.meta import describe_node, resolve_describe_fields
|
|
17
|
+
from graphrefly.core.meta import DescribeDetail, describe_node, resolve_describe_fields
|
|
18
18
|
from graphrefly.core.node import NodeImpl
|
|
19
19
|
from graphrefly.core.protocol import Messages, MessageType, is_batching, message_tier
|
|
20
20
|
from graphrefly.core.sugar import state
|
|
@@ -23,7 +23,7 @@ if TYPE_CHECKING:
|
|
|
23
23
|
from collections.abc import Callable, Iterator
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
class DescribeResult(dict):
|
|
26
|
+
class DescribeResult(dict[str, Any]):
|
|
27
27
|
"""Dict subclass returned by :meth:`Graph.describe`.
|
|
28
28
|
|
|
29
29
|
Provides an ``expand()`` method for re-reading the live graph at a higher
|
|
@@ -34,7 +34,7 @@ class DescribeResult(dict):
|
|
|
34
34
|
def expand(self, detail_or_fields: Any = None) -> DescribeResult:
|
|
35
35
|
"""Re-read the live graph at a higher detail level or with explicit fields."""
|
|
36
36
|
fn = object.__getattribute__(self, "_expand_fn")
|
|
37
|
-
return fn(detail_or_fields)
|
|
37
|
+
return cast("DescribeResult", fn(detail_or_fields))
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
@dataclass(frozen=True, slots=True)
|
|
@@ -1050,7 +1050,7 @@ class Graph:
|
|
|
1050
1050
|
detail: str | None = None,
|
|
1051
1051
|
fields: list[str] | None = None,
|
|
1052
1052
|
format: str | None = None,
|
|
1053
|
-
) ->
|
|
1053
|
+
) -> DescribeResult:
|
|
1054
1054
|
"""Static structure snapshot (GRAPHREFLY-SPEC §3.6, Appendix B).
|
|
1055
1055
|
|
|
1056
1056
|
``nodes`` keys are qualified paths (including ``::__meta__::`` for companions).
|
|
@@ -1099,7 +1099,7 @@ class Graph:
|
|
|
1099
1099
|
if format == "spec":
|
|
1100
1100
|
include_fields: set[str] | None = {"type", "deps"}
|
|
1101
1101
|
else:
|
|
1102
|
-
include_fields = resolve_describe_fields(detail, fields)
|
|
1102
|
+
include_fields = resolve_describe_fields(cast("DescribeDetail | None", detail), fields)
|
|
1103
1103
|
|
|
1104
1104
|
targets = _collect_observe_targets(self, "")
|
|
1105
1105
|
paths_by_id = {id(n): p for p, n in targets}
|
|
@@ -2057,7 +2057,14 @@ class Graph:
|
|
|
2057
2057
|
va = na.get(key)
|
|
2058
2058
|
vb = nb.get(key)
|
|
2059
2059
|
if va != vb:
|
|
2060
|
-
changed_nodes.append(
|
|
2060
|
+
changed_nodes.append(
|
|
2061
|
+
GraphDiffNodeChange(
|
|
2062
|
+
path=p,
|
|
2063
|
+
field=key,
|
|
2064
|
+
from_value=va,
|
|
2065
|
+
to_value=vb,
|
|
2066
|
+
)
|
|
2067
|
+
)
|
|
2061
2068
|
|
|
2062
2069
|
a_edges = {(e["from"], e["to"]) for e in a.get("edges", [])}
|
|
2063
2070
|
b_edges = {(e["from"], e["to"]) for e in b.get("edges", [])}
|