Simbiot Harness v0.1: тактовая лестница на Strands + CQRS/ES-спина

- harness/es.py: append-only event store + проекции (образец — кооп-портал)
- harness/levels.py: уровни 1с/1м/1ч/1д, контекст ∝ масштабу
- harness/scheduler.py: уровневые потоки, идемпотентные тики, absorbed-стабы
- harness/isolated_runner.py + _llm_worker.py: LLM-тик в сабпроцессе
  (глобальный asyncio-рантайм Strands не переживает 2+ create_harness)
- cli.py: init/task/tasks/status/events + requeue (написан самим минутным агентом)
- 9router: отдельный ключ sk-simbiot, модель auto (glm-5.3 первым)
This commit is contained in:
Simbiot Harness 2026-09-29 15:28:50 +00:00
commit b60439152b
10884 changed files with 1125375 additions and 0 deletions

Binary file not shown.

426
architecture.html Normal file
View file

@ -0,0 +1,426 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simbiot Harness — архитектура v0.1 (ревью)</title>
<style>
:root{
--paper:#faf8f3; --ink:#15150f; --carbon:#1c1b16; --signal:#FF4F00;
--dim:#6b675e; --line:#d8d3c8; --card:#f3f0e8; --ok:#1d7a3e; --warn:#a3690f;
}
*{margin:0;padding:0;box-sizing:border-box}
html{scroll-behavior:smooth}
body{background:var(--paper);color:var(--ink);font:16px/1.62 -apple-system,"Segoe UI",Roboto,Arial,sans-serif;-webkit-font-smoothing:antialiased}
.mono,code{font-family:ui-monospace,"JetBrains Mono","SF Mono",Menlo,Consolas,monospace}
.wrap{max-width:1080px;margin:0 auto;padding:48px 28px 120px}
header{border-bottom:3px solid var(--ink);padding-bottom:28px;margin-bottom:40px;position:relative}
.kicker{font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.22em;color:var(--signal);font-weight:700;text-transform:uppercase}
h1{font-size:clamp(34px,6vw,58px);line-height:1.02;font-weight:800;letter-spacing:-.02em;margin:10px 0 14px;text-transform:uppercase}
.sub{font-size:17px;color:var(--dim);max-width:720px}
.badges{display:flex;flex-wrap:wrap;gap:8px;margin-top:18px}
.badge{border:1px solid var(--ink);padding:5px 12px;font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.08em;text-transform:uppercase;background:var(--paper)}
.badge.s{background:var(--ink);color:var(--paper)}
section{margin:64px 0 0}
.snum{font-family:ui-monospace,Menlo,monospace;font-size:12px;color:var(--signal);letter-spacing:.2em;font-weight:700}
h2{font-size:26px;font-weight:800;letter-spacing:-.01em;margin:4px 0 6px;text-transform:uppercase}
.lead{color:var(--dim);max-width:760px;margin-bottom:22px}
h3{font-size:15px;text-transform:uppercase;letter-spacing:.06em;margin:26px 0 10px;border-left:3px solid var(--signal);padding-left:10px}
p{margin:10px 0;max-width:820px}
.cardgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px;margin:18px 0}
.card{border:1px solid var(--ink);background:var(--card);padding:16px 16px 14px}
.card .cn{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--signal);font-weight:700;letter-spacing:.15em}
.card h4{font-size:15px;margin:6px 0 6px;text-transform:uppercase;letter-spacing:.02em}
.card p{font-size:13.5px;color:var(--dim);margin:0;line-height:1.55}
table{width:100%;border-collapse:collapse;font-size:13.5px;margin:14px 0}
th{background:var(--ink);color:var(--paper);text-align:left;padding:9px 10px;font-family:ui-monospace,Menlo,monospace;font-size:11px;letter-spacing:.1em;text-transform:uppercase}
td{border-bottom:1px solid var(--line);padding:9px 10px;vertical-align:top}
tr.hl td{background:#fff3ea}
td .m{font-family:ui-monospace,Menlo,monospace;font-size:12px}
.pill{display:inline-block;border:1px solid var(--ink);padding:1px 7px;font-family:ui-monospace,Menlo,monospace;font-size:11px;white-space:nowrap}
.pill.s{border-color:var(--signal);color:var(--signal)}
figure{margin:26px 0;border:1px solid var(--ink);background:#fff}
figcaption{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--dim);padding:8px 12px;border-top:1px solid var(--line);letter-spacing:.05em}
.steps{display:flex;flex-wrap:wrap;gap:0;margin:20px 0;counter-reset:st}
.step{flex:1 1 150px;min-width:150px;border:1px solid var(--ink);border-left:none;padding:14px 14px 12px;background:var(--card);position:relative;counter-increment:st}
.step:first-child{border-left:1px solid var(--ink)}
.step::before{content:counter(st,decimal-leading-zero);font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--signal);font-weight:700;letter-spacing:.15em}
.step b{display:block;font-size:13.5px;margin:5px 0 4px;text-transform:uppercase;letter-spacing:.02em}
.step span{font-size:12.5px;color:var(--dim);line-height:1.5;display:block}
.note{border:1px solid var(--ink);border-left:4px solid var(--signal);padding:14px 18px;margin:18px 0;background:var(--card);font-size:14.5px}
.note b{text-transform:uppercase;letter-spacing:.04em;font-size:13px}
.pyr{border:1px solid var(--ink);margin:18px 0;background:#fff}
.pyr .lvl{display:flex;align-items:center;gap:14px;padding:13px 16px;border-bottom:1px solid var(--line)}
.pyr .lvl:last-child{border-bottom:none}
.pyr .w{flex:0 0 130px;font-family:ui-monospace,Menlo,monospace;font-size:12px;font-weight:700;letter-spacing:.05em}
.pyr .bar{height:10px;background:var(--signal);opacity:.9}
.pyr .d{flex:1;font-size:13px;color:var(--dim)}
.two{display:grid;grid-template-columns:1fr 1fr;gap:18px}
@media(max-width:840px){.two{grid-template-columns:1fr}}
.box{border:1px solid var(--ink);padding:16px;background:var(--card)}
.box h5{font-family:ui-monospace,Menlo,monospace;font-size:11px;letter-spacing:.15em;text-transform:uppercase;color:var(--signal);margin-bottom:10px}
ul{margin:8px 0 8px 18px;font-size:14px}
li{margin:5px 0}
li::marker{color:var(--signal)}
.q{border:1px solid var(--ink);padding:14px 16px;margin:10px 0;display:flex;gap:12px;background:#fff}
.q .cb{flex:0 0 16px;height:16px;border:2px solid var(--ink);margin-top:2px}
.q b{font-size:14px}
.q p{font-size:13px;color:var(--dim);margin:3px 0 0;max-width:none}
footer{margin-top:80px;border-top:3px solid var(--ink);padding-top:18px;font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--dim);display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px;letter-spacing:.05em}
svg text{font-family:ui-monospace,Menlo,monospace}
@media print{body{background:#fff}.wrap{padding:10px}section{page-break-inside:avoid}}
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="kicker">Архитектура · для ревью</div>
<h1>Simbiot&nbsp;Harness</h1>
<div class="sub">Принципиально новый агентный харнес на CQRS + Event Sourcing: агент живёт в тактах разного масштаба — секунда, минута, час, день, неделя, месяц, квартал, год — как человек. Стартовая точка — <b>strands-agents/harness-sdk</b> (Apache-2.0): его исполнительное ядро, поверх — наша временная ось и событийная спина.</div>
<div class="badges">
<span class="badge s">CQRS + Event Sourcing</span>
<span class="badge">Strands Harness SDK</span>
<span class="badge">9router — LLM-шлюз</span>
<span class="badge">Python 3.12 · SQLite WAL</span>
<span class="badge">v0.1 · ревью пройдено 29.09</span>
</div>
</header>
<!-- ================= 01 ================= -->
<section id="s01">
<div class="snum">01 / СУТЬ</div>
<h2>Что мы строим</h2>
<p class="lead">Все существующие харнесы (Strands, Hermes, OpenClaw) — пассивны: они просыпаются, когда пришёл внешний запрос, и не имеют собственной временной оси. Simbiot Harness — экзистенциальный: он тикает сам.</p>
<p>Харнес — это не «функция, которую вызывают», а <b>живой процесс с ритмами</b>. Восемь вложенных циклов бьют одновременно: секундный — как сердцебиение, минутный — как реакция, часовой — как рабочий поток, дневной — как план дня, недельный — как ретро, месячный — как цели, квартальный — как стратегия, годовой — как идентичность. Чем крупнее цикл, тем больше контекста он может себе позволить и тем крупнее задачу он вправе взять.</p>
<p>Всё, что происходит — тики, решения, наблюдения, память, трата бюджета — это <b>события в append-only сторе</b>. Состояние харнеса не хранится, а <b>выводится replay'ем событий</b>. Выключили процесс — включили — он продолжил с того же места, потому что его «личность» — это события, а не RAM.</p>
<div class="note"><b>Одной фразой:</b> Strands даёт нам тело (агентный цикл, инструменты, хуки). Simbiot Harness даёт ему метаболизм — время, память и ритм — на событийной спине.</div>
</section>
<!-- ================= 02 ================= -->
<section id="s02">
<div class="snum">02 / ПРИНЦИПЫ</div>
<h2>Шесть принципов</h2>
<div class="cardgrid">
<div class="card"><div class="cn">P-1</div><h4>Всё есть событие</h4><p>Тик, взятая задача, дайджест, трасса каждого tool-call агента — события. Event store — единственный источник правды. Никакого «состояния в RAM».</p></div>
<div class="card"><div class="cn">P-2</div><h4>Тактовая лестница</h4><p>8 уровней: 1с → 1м → 1ч → 1д → 1н → 1мес → 1кв → 1г. У каждого — свой мандат, бюджет и модель. Мелкое решает мелкое, крупное — крупное.</p></div>
<div class="card"><div class="cn">P-3</div><h4>Контекст ∝ масштабу</h4><p>Секундный цикл не зовёт LLM вовсе, минутный получает 4K токенов, дневной — 128K, годовой — до 1M. Задача S-класса не занимает слот годового цикла.</p></div>
<div class="card"><div class="cn">P-4</div><h4>Непрерываемость</h4><p>Запущенная задача не прерывается тиками. Если исполнитель уровня занят — тик поглощается (skip), а не копится в очередь. Работа важнее метронома.</p></div>
<div class="card"><div class="cn">P-5</div><h4>Переваривание</h4><p>Верхние циклы едят выжимку нижних: дневной читает не сырые минуты, а часовые дайджесты. Пирамида консолидации памяти — как сон у человека.</p></div>
<div class="card"><div class="cn">P-6</div><h4>Воспроизводимость</h4><p>Стор можно переиграть с нуля — и харнес, и память, и решения соберутся заново. Отладка агента = чтение потока событий, а не логов.</p></div>
</div>
</section>
<!-- ================= 03 ================= -->
<section id="s03">
<div class="snum">03 / ЛЕСТНИЦА</div>
<h2>Тактовая лестница</h2>
<p class="lead">Каждый уровень — отдельная «личность»: свой мандат (что ему можно), свой бюджет контекста, своя модель и свой класс задач. Цикл может взять задачу своего класса и ниже; задача выше класса — ожидает уровень повыше.</p>
<table>
<tr><th>Цикл</th><th>Мандат</th><th>Мозг (9router)</th><th>Контекст</th><th>Задачи</th><th>Аналогия у человека</th></tr>
<tr><td class="m"><b>1 сек</b></td><td>Рефлекс: телеметрия, watchdog, heartbeat, алерты</td><td>без LLM — детерминированный код</td><td class="m">0</td><td><span class="pill">микро</span> замер, триггер</td><td>Сердцебиение</td></tr>
<tr><td class="m"><b>1 мин</b></td><td>Пульс: триаж входящих, быстрые ответы, мелкие фиксы</td><td><span class="m">glm-5.3-flash</span></td><td class="m">≤ 4K</td><td><span class="pill s">S</span> ≤ 5 мин работы</td><td>Реакция в разговоре</td></tr>
<tr><td class="m"><b>1 час</b></td><td>Дайджест: сборка часа, батч мелочей, проверка прогресса</td><td><span class="m">glm-5.3-flash</span></td><td class="m">≤ 32K</td><td><span class="pill s">M</span> ≤ 1 ч</td><td>Рабочий поток</td></tr>
<tr class="hl"><td class="m"><b>1 день</b></td><td>Манифест: план дня, выбор задач, дневной отчёт</td><td><span class="m">glm-5.3 (full)</span></td><td class="m">≤ 128K</td><td><span class="pill s">L</span> ≤ 1 день</td><td>План на день</td></tr>
<tr><td class="m"><b>1 неделя</b></td><td>Ретро: разбор недели, обучение, обновление скиллов</td><td><span class="m">glm-5.3 · effort: high</span></td><td class="m">≤ 200K</td><td><span class="pill s">XL</span> неделя</td><td>Недельное ретро</td></tr>
<tr><td class="m"><b>1 месяц</b></td><td>Цели: бюджет, OKR, ранги, накопления</td><td><span class="m">full</span></td><td class="m">≤ 256K</td><td><span class="pill s">XXL</span> месяц</td><td>Месячная цель</td></tr>
<tr><td class="m"><b>1 квартал</b></td><td>Стратегия: пересмотр направления, ставки</td><td><span class="m">pro-слот*</span></td><td class="m">≤ 512K</td><td><span class="pill s">XXL</span> квартал</td><td>Квартальная стратегия</td></tr>
<tr><td class="m"><b>1 год</b></td><td>Идентичность: миссия, «зачем существую», самоопределение</td><td><span class="m">pro-слот*</span></td><td class="m">≤ 1M</td><td><span class="pill s">XXL</span> год</td><td>Жизненный выбор</td></tr>
</table>
<p style="font-size:13px;color:var(--dim)">* pro-слот — зарезервировано под сильную модель (Claude Opus как личный брейн / Gemini 3 Pro через KIE): годовой цикл тикает 1 раз, дорогой запуск раз в год — приемлемо.</p>
</section>
<!-- ================= 04 ================= -->
<section id="s04">
<div class="snum">04 / ДИАГРАММА</div>
<h2>Архитектура</h2>
<figure>
<svg viewBox="0 0 1140 620" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;display:block">
<!-- defs -->
<defs>
<marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" fill="#15150f"/></marker>
<marker id="arrO" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" fill="#FF4F00"/></marker>
</defs>
<style>
.bx{fill:#f3f0e8;stroke:#15150f;stroke-width:1.5}
.bxW{fill:#fff;stroke:#15150f;stroke-width:1.5}
.bxS{fill:#15150f;stroke:#15150f;stroke-width:1.5}
.lab{fill:#15150f;font-size:13px;font-weight:700;letter-spacing:.08em}
.labS{fill:#faf8f3;font-size:13px;font-weight:700;letter-spacing:.08em}
.sub{fill:#6b675e;font-size:11px;letter-spacing:.02em}
.sig{fill:#FF4F00;font-size:10px;font-weight:700;letter-spacing:.2em}
.wire{stroke:#15150f;stroke-width:1.5;fill:none}
.wireO{stroke:#FF4F00;stroke-width:1.6;fill:none}
.wireD{stroke:#6b675e;stroke-width:1.2;fill:none;stroke-dasharray:5 4}
</style>
<!-- ====== ROW 1: CQRS plane ====== -->
<text x="20" y="34" class="sig">CQRS — ПЛОСКОСТЬ КОМАНД И ЗАПРОСОВ</text>
<!-- interfaces -->
<rect x="20" y="48" width="180" height="92" class="bx"/>
<text x="34" y="74" class="lab">ИНТЕРФЕЙСЫ</text>
<text x="34" y="94" class="sub">Telegram · Matrix · Web</text>
<text x="34" y="110" class="sub">cron · webhook · человек</text>
<!-- command side -->
<rect x="250" y="48" width="190" height="92" class="bx"/>
<text x="264" y="74" class="lab">COMMAND SIDE</text>
<text x="264" y="94" class="sub">валидация · идемпотентность</text>
<text x="264" y="110" class="sub">submit_task · set_policy</text>
<!-- event store -->
<rect x="490" y="48" width="220" height="230" class="bxS"/>
<text x="508" y="76" class="labS">EVENT STORE</text>
<text x="508" y="96" fill="#FF4F00" font-size="10" font-weight="700" letter-spacing=".15em">APPEND-ONLY · СПИНА</text>
<text x="508" y="122" fill="#9a958a" font-size="11px">events(aggregate, id,</text>
<text x="508" y="138" fill="#9a958a" font-size="11px"> type, data, version)</text>
<text x="508" y="170" fill="#9a958a" font-size="11px">CycleTicked · TaskClaimed</text>
<text x="508" y="186" fill="#9a958a" font-size="11px">DigestWritten · BudgetSpent</text>
<text x="508" y="218" fill="#9a958a" font-size="11px">replay → вся «личность»</text>
<text x="508" y="234" fill="#9a958a" font-size="11px">харнеса восстанавливается</text>
<text x="508" y="262" fill="#FF4F00" font-size="10" font-weight="700">SQLite · WAL</text>
<!-- projections -->
<rect x="760" y="48" width="200" height="110" class="bx"/>
<text x="774" y="74" class="lab">ПРОЕКЦИИ</text>
<text x="774" y="94" class="sub">task_board · digest_*</text>
<text x="774" y="110" class="sub">metrics · agent_state</text>
<text x="774" y="126" class="sub">(перестраиваются replay'ем)</text>
<!-- query side -->
<rect x="1000" y="48" width="120" height="110" class="bx"/>
<text x="1014" y="74" class="lab">QUERY</text>
<text x="1014" y="94" class="sub">API · дашборд</text>
<text x="1014" y="110" class="sub">человек смотрит</text>
<!-- arrows row1 -->
<line x1="200" y1="94" x2="245" y2="94" class="wire" marker-end="url(#arr)"/>
<line x1="440" y1="94" x2="485" y2="94" class="wireO" marker-end="url(#arrO)"/>
<text x="446" y="84" class="sig">APPEND</text>
<line x1="710" y1="94" x2="755" y2="94" class="wire" marker-end="url(#arr)"/>
<text x="716" y="84" class="sig">STREAM→</text>
<line x1="960" y1="103" x2="995" y2="103" class="wire" marker-end="url(#arr)"/>
<!-- ====== ROW 2: temporal plane ====== -->
<text x="20" y="322" class="sig">ВРЕМЕННАЯ ПЛОСКОСТЬ — ТАКТОВАЯ ЛЕСТНИЦА</text>
<!-- scheduler -->
<rect x="20" y="336" width="180" height="180" class="bx"/>
<text x="34" y="362" class="lab">SCHEDULER</text>
<text x="34" y="382" class="sub">8 уровней бьют по границам</text>
<text x="34" y="398" class="sub">тактов: 1с 1м 1ч 1д 1н</text>
<text x="34" y="414" class="sub">1мес 1кв 1г</text>
<text x="34" y="440" class="sub">тик занят → absorbed</text>
<text x="34" y="456" class="sub">(skip, не очередь)</text>
<text x="34" y="482" class="sub">asyncio · один процесс</text>
<!-- cycle runners -->
<rect x="250" y="336" width="190" height="180" class="bx"/>
<text x="264" y="362" class="lab">CYCLE RUNNERS</text>
<text x="264" y="382" class="sub">по одному исполнителю</text>
<text x="264" y="398" class="sub">на уровень → сериализация,</text>
<text x="264" y="414" class="sub">задача не прерывается</text>
<text x="264" y="444" class="sub">мандат + бюджет +</text>
<text x="264" y="460" class="sub">класс задач — из конфига</text>
<text x="264" y="486" class="sub">уровня (P-2, P-3)</text>
<!-- strands runtime -->
<rect x="490" y="336" width="220" height="180" class="bxW"/>
<text x="508" y="362" class="lab">STRANDS RUNTIME</text>
<text x="508" y="386" class="sub">create_harness() на каждый тик:</text>
<text x="508" y="406" class="sub">agent loop · tools · MCP</text>
<text x="508" y="422" class="sub">session (задача) · effort</text>
<text x="508" y="438" class="sub">context_manager · guardrails</text>
<text x="508" y="462" class="sub">HOOKS → каждая стадия цикла</text>
<text x="508" y="478" class="sub">агента пишет трассу событием</text>
<!-- llm gateway -->
<rect x="760" y="336" width="200" height="92" class="bx"/>
<text x="774" y="362" class="lab">LLM ШЛЮЗ</text>
<text x="774" y="382" class="sub">9router: flash / full / pro</text>
<text x="774" y="398" class="sub">модель задаётся уровнем</text>
<text x="774" y="414" class="sub">цикла — не задачей</text>
<!-- tools -->
<rect x="760" y="452" width="200" height="64" class="bx"/>
<text x="774" y="476" class="lab">ИНСТРУМЕНТЫ</text>
<text x="774" y="496" class="sub">shell · files · web · MCP · скиллы</text>
<!-- arrows row2 -->
<line x1="200" y1="415" x2="245" y2="415" class="wireO" marker-end="url(#arrO)"/>
<text x="204" y="404" class="sig">TICK</text>
<line x1="440" y1="415" x2="485" y2="415" class="wire" marker-end="url(#arr)"/>
<line x1="710" y1="382" x2="755" y2="382" class="wire" marker-end="url(#arr)"/>
<text x="712" y="372" class="sig">LLM</text>
<line x1="860" y1="452" x2="860" y2="427" class="wire" marker-end="url(#arr)"/>
<!-- vertical links between planes -->
<!-- scheduler → event store (CycleTicked) -->
<path d="M110 336 L110 200 Q110 185 125 185 L485 185" class="wireO" marker-end="url(#arrO)"/>
<text x="150" y="177" class="sig">CycleTicked → APPEND</text>
<!-- runners → projections (read context) -->
<path d="M345 336 L345 260 Q345 245 360 245 L755 245 L755 160" class="wireD" marker-end="url(#arr)"/>
<text x="420" y="238" class="sub">читает контекст: дайджесты нижних уровней + задачи класса</text>
<!-- strands hooks → event store -->
<path d="M600 336 L600 282" class="wireO" marker-end="url(#arrO)"/>
<text x="612" y="318" class="sig">HOOKS: трасса агента → события</text>
<!-- event store → scheduler catch-up? no. projections → runners already. -->
</svg>
<figcaption>РИС. 1 — Две плоскости. CQRS: интерфейсы → команды → event store → проекции → запросы. Временная: планировщик → исполнители циклов → Strands-агент → LLM. Замкнутый контур: события порождают циклы, циклы порождают события.</figcaption>
</figure>
</section>
<!-- ================= 05 ================= -->
<section id="s05">
<div class="snum">05 / ТИК</div>
<h2>Жизненный цикл тика</h2>
<p class="lead">Один тик уровня — от границы такта до записи результата. Ключевое правило — непрерываемость: занятый исполнитель поглощает тик.</p>
<div class="steps">
<div class="step"><b>Граница такта</b><span>Scheduler ловит момент 1с/1м/…/1г. Эмитит событие CycleTicked{level, seq}.</span></div>
<div class="step"><b>Исполнитель свободен?</b><span>Занят → TickAbsorbed: детерминированная дайджест-заглушка (без LLM), очередь не копим. Дорога (v0.2+): срочная реакция сможет прервать бегущую задачу или поставить её на паузу.</span></div>
<div class="step"><b>Сборка контекста</b><span>Мандат уровня + идентичность + память уровня + выжимка нижних (дайджесты) + задачи доступного класса. В пределах бюджета P-3.</span></div>
<div class="step"><b>Выбор задачи</b><span>Есть подходящая задача → TaskClaimed + сессия. Нет → цикл делает мандатную работу (дайджест/наблюдение) или проходит впустую — это честно.</span></div>
<div class="step"><b>Strands-цикл</b><span>create_harness(model, effort, tools уровня) → invoke. Каждый шаг — hook → событие. Задача не прерывается никакими тиками.</span></div>
<div class="step"><b>Завершение</b><span>TaskCompleted / TaskFailed (+ политика ретраев и промоушена выше). Результат → событие с ссылкой на артефакт.</span></div>
<div class="step"><b>Выжимка</b><span>Runner пишет DigestWritten{level, period} — сырое остаётся в трассе, в пирамиду идёт сжатое.</span></div>
</div>
<div class="note"><b>Non-preemption на практике:</b> у каждого уровня ровно один исполнитель (worker). Это даёт сериализацию бесплатно: день не может «отложить» час. Долгие L-задачи идут в фоне сессии, дневной цикл на следующих тиках лишь проверяет их статус — и пока они живы, новых L не берёт.</div>
</section>
<!-- ================= 06 ================= -->
<section id="s06">
<div class="snum">06 / ПАМЯТЬ</div>
<h2>Контекстная пирамида</h2>
<p class="lead">Контекст цикла — не «всё, что случилось», а многослойная сборка. Сырые данные живут в трассе событий; наверх поднимаются только выжимки. Это главный эконом токенов: наш фактический опыт — 3.1 млрд prompt-токенов за 14 дней в основном из-за перечитывания сырых контекстов.</p>
<div class="pyr">
<div class="lvl"><div class="w">ГОД · 1M</div><div class="bar" style="width:56%"></div><div class="d"><b>Идентичность:</b> миссия, ценности, «кто я». Пишется раз в год, кэшируется во все нижние уровни.</div></div>
<div class="lvl"><div class="w">КВАРТАЛ · 512K</div><div class="bar" style="width:48%"></div><div class="d"><b>Ставки:</b> 3–5 стратегических направлений + критерии их проверки.</div></div>
<div class="lvl"><div class="w">МЕСЯЦ · 256K</div><div class="bar" style="width:40%"></div><div class="d"><b>Цели:</b> OKR, бюджет, лимиты расходов на уровень.</div></div>
<div class="lvl"><div class="w">НЕДЕЛЯ · 200K</div><div class="bar" style="width:34%"></div><div class="d"><b>Уроки:</b> что сработало/нет, обновления скиллов и политики.</div></div>
<div class="lvl"><div class="w">ДЕНЬ · 128K</div><div class="bar" style="width:28%"></div><div class="d"><b>Манифест дня:</b> план + часовые дайджесты (не сырые минуты!) + доска задач L-класса.</div></div>
<div class="lvl"><div class="w">ЧАС · 32K</div><div class="bar" style="width:20%"></div><div class="d"><b>Дайджест часа:</b> сжатые минутные логи, статус фоновых задач, триаж входящих.</div></div>
<div class="lvl"><div class="w">МИНУТА · 4K</div><div class="bar" style="width:12%"></div><div class="d"><b>Пульс:</b> последние входящие, алерты секундного уровня, S-задачи.</div></div>
<div class="lvl"><div class="w">СЕК · 0</div><div class="bar" style="width:5%;background:var(--ink)"></div><div class="d"><b>Рефлекс:</b> без LLM. Телеметрия и правила. Дёшево тикать каждую секунду.</div></div>
</div>
<p style="font-size:13.5px;color:var(--dim)">Сборка контекста дневного цикла: идентичность (кэш) + квартальные ставки + месячные цели + недельные уроки + дайджесты 24 часов + доска задач. Сырые трассы не поднимаются выше своего уровня никогда — вверх идёт только сжатое.</p>
</section>
<!-- ================= 07 ================= -->
<section id="s07">
<div class="snum">07 / СОБЫТИЯ</div>
<h2>Событийная модель</h2>
<p class="lead">Стор — по образцу кооп-портала (aggregate, aggregate_id, event_type, data, timestamp, version). События иммутабельны; тестовые — единственное исключение при чистке.</p>
<div class="two">
<div class="box">
<h5>Aggregate: cycle</h5>
<ul>
<li><span class="m">CycleTicked</span> {level, seq} — граница такта</li>
<li><span class="m">TickAbsorbed</span> {reason: busy|skipped}</li>
<li><span class="m">DigestWritten</span> {level, period, summary_ref}</li>
</ul>
</div>
<div class="box">
<h5>Aggregate: task</h5>
<ul>
<li><span class="m">TaskSubmitted</span> {title, size S/M/L/XL, source}</li>
<li><span class="m">TaskClaimed</span> {level, session_id}</li>
<li><span class="m">TaskCompleted</span> {result_ref} / <span class="m">TaskFailed</span> {error}</li>
<li><span class="m">TaskPromoted</span> {from, to} — не по зубам уровню</li>
</ul>
</div>
<div class="box">
<h5>Aggregate: harness</h5>
<ul>
<li><span class="m">PolicyChanged</span> {budgets, mandates}</li>
<li><span class="m">BudgetSpent</span> {level, tokens, $}</li>
<li><span class="m">LevelPaused</span> / <span class="m">LevelResumed</span></li>
</ul>
</div>
<div class="box">
<h5>Aggregate: trace</h5>
<ul>
<li><span class="m">InvocationStarted</span> / <span class="m">ToolCalled</span> / <span class="m">ToolResult</span> — из hooks Strands: BeforeInvocationEvent, Before/AfterToolCallEvent</li>
<li>Полная трасса агентного цикла = поток событий. Дебаг = читать стор.</li>
</ul>
</div>
</div>
<h3>Проекции (query side)</h3>
<ul>
<li><b>task_board</b> — готовые/взятые/закрытые задачи по классам; who-can-claim-what</li>
<li><b>digest_{level}</b> — последние выжимки уровня (источник для контекстов уровней выше)</li>
<li><b>metrics</b> — тики/поглощения/расходы по уровням (видно стоимость ритма)</li>
<li><b>agent_state</b> — чем занят каждый уровень сейчас</li>
</ul>
</section>
<!-- ================= 08 ================= -->
<section id="s08">
<div class="snum">08 / STRANDS</div>
<h2>Маппинг на Strands Harness SDK</h2>
<p class="lead">Берём SDK как исполнительное ядро — не форкаем, а собираем поверх. Всё, чего нет в Strands (время, событийная спина, консолидация) — наш слой.</p>
<table>
<tr><th>Наш концепт</th><th>Механизм Strands</th><th>Комментарий</th></tr>
<tr><td>Cycle Runner уровня</td><td class="m">create_harness() на тик</td><td>Фабрика с конфигом уровня: model, effort, tools, instructions</td></tr>
<tr><td>Мандат уровня</td><td class="m">instructions / system prompt</td><td>Собственный слой поверх HARNESS_CONTRACT</td></tr>
<tr><td>Бюджет контекста</td><td class="m">context_manager + session</td><td>agentic-режим для день+, sliding window для минут</td></tr>
<tr><td>Непрерываемость</td><td class="m">background tasks + session id</td><td>Задача = сессия; тики не рвут её, цикл опрашивает</td></tr>
<tr><td>Трасса в стор</td><td class="m">hooks: add_hook(BeforeInvocationEvent, BeforeToolCallEvent…)</td><td>Наш плагин: каждый хук → append_event. Трасса = события</td></tr>
<tr><td>Инструменты</td><td class="m">builtin_tools + mcp_servers + skills dir</td><td>shell/read/write/edit/web — по уровню: минуте не даём shell, дню — даём</td></tr>
<tr><td>Модель по уровню</td><td class="m">model: provider/name, effort: auto…high</td><td>Через наш 9router-провайдер: flash/full/pro</td></tr>
<tr><td>Осторожность</td><td class="m">interventions (guardrails)</td><td>Необратимые действия — через подтверждение или только на высоких уровнях</td></tr>
<tr><td>Долгая память</td><td class="m">memory stores</td><td>Бэкенд — наши проекции digest_*, не файлы</td></tr>
</table>
<div class="two" style="margin-top:20px">
<div class="box"><h5>Берём из Strands как есть</h5>
<ul>
<li>Agent loop: лимиты ходов, токен-бюджеты, stop reasons</li>
<li>Hooks — типизированные события жизненного цикла</li>
<li>Сессии, structured output, MCP, мультитул-оркестрация</li>
<li>Guardrails/interventions, context offloader</li>
</ul>
</div>
<div class="box"><h5>Принципиально своё (новое)</h5>
<ul>
<li><b>Временная ось:</b> тактовая лестница — в Strands нет никакого «времени»</li>
<li><b>Событийная спина:</b> у Strands сессии = файлы; у нас — append-only стор + replay</li>
<li><b>Каскад консолидации</b> памяти (digestion) — у Strands нет иерархии памяти</li>
<li><b>Классы задач ↔ уровни цикла</b> — планировщик как у человека</li>
</ul>
</div>
</div>
</section>
<!-- ================= 09 ================= -->
<section id="s09">
<div class="snum">09 / СТЕК</div>
<h2>Стек и размещение</h2>
<ul>
<li><b>Python 3.12</b>, один процесс-демон (systemd), asyncio-планировщик — как simbiotd, но на событийной спине</li>
<li><span class="m">pip install strands-harness</span> (Apache-2.0) + наш пакет <span class="m">simbiot-harness</span>: scheduler, runners, es-спина, проекции, hooks-плагин</li>
<li><b>Event store: SQLite WAL</b> — проверенный образец кооп-портала (events, projections, projection_state.json); идемпотентные команды; чистые тестовые события после E2E</li>
<li><b>LLM:</b> 9router (glm-5.3-flash / glm-5.3 / pro-слот) — своя модель на уровень, бюджет через BudgetSpent</li>
<li><b>Интерфейсы v0.1:</b> CLI + Telegram (таски от человека); Matrix/Web — после ревью</li>
<li><b>Деплой:</b> портал-бокс рядом с simbiotd; порт не нужен — только systemd-юнит и локальный CLI</li>
</ul>
<h3>Объём v0.1 (что строим первым)</h3>
<ul>
<li>Уровни <b>1с + 1м + 1ч + 1д</b> (рефлекс без LLM уже дёшев); неделя/месяц — v0.2, квартал/год — v0.3</li>
<li>Задачи S/M/L + промоушен классов, непрерываемость, дайджесты часа</li>
<li>E2E: человек кидает задачу в TG → минутный взял S → дневной собрал манифест → отчёт → метрики стоимости ритма в проекции</li>
</ul>
</section>
<!-- ================= 10 ================= -->
<section id="s10">
<div class="snum">10 / РЕШЕНИЯ</div>
<h2>Ревью пройдено — решения</h2>
<p class="lead">Прошли с владельцем 29.09.2026. Все шесть вопросов закрыты.</p>
<table>
<tr><th>Вопрос</th><th>Решение</th></tr>
<tr><td>Стоимость ритма (~$1–3/день за «жизнь» без задач)</td><td>✅ Принято — норма</td></tr>
<tr><td>Пропущенные такты</td><td>✅ Поглощённый тик пишет детерминированную дайджест-заглушку (без LLM). <b>Дальше (v0.2+):</b> срочная реакция сможет прервать бегущую задачу или поставить её на паузу</td></tr>
<tr><td>Один исполнитель на уровень</td><td>✅ Принято; N воркеров минутного уровня — v0.2 при необходимости</td></tr>
<tr><td>Идемпотентность тиков</td><td>✅ UNIQUE(level, seq); пропущенное время не навёрстывается</td></tr>
<tr><td>Pro-слот для квартала/года</td><td>✅ Принято</td></tr>
<tr><td>Имя и место</td><td>✅ Simbiot Harness — <b>отдельный репо-продукт</b> (отдельный продукт = отдельный репо)</td></tr>
</table>
<div class="note"><b>Следующий шаг:</b> сборка v0.1 — уровни 1с + 1м + 1ч + 1д, задачи S/M/L, ES-спина, дайджесты, hooks-плагин Strands, E2E: таска из Telegram → минутный цикл → дневной манифест → отчёт.</div>
</section>
<footer>
<span>SIMBIOT HARNESS · ARCHITECTURE v0.1</span>
<span>РЕВЬЮ ПРОЙДЕНО · 29.09.2026</span>
<span>STRANDS HARNESS SDK · APACHE-2.0</span>
</footer>
</div>
</body>
</html>

71
cli.py Normal file
View file

@ -0,0 +1,71 @@
"""Simbiot Harness — CLI: таски, статус, ручные тики."""
import argparse
import json
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from harness import commands, es
def main():
ap = argparse.ArgumentParser(prog="simbiot", description="Simbiot Harness CLI")
sub = ap.add_subparsers(dest="cmd", required=True)
p_task = sub.add_parser("task", help="подать задачу")
p_task.add_argument("title")
p_task.add_argument("-d", "--details", default="")
p_task.add_argument("-m", "--minutes", type=int, default=5,
help="оценка длительности (класс S≤5м M≤60м L≤8ч XL)")
p_pol = sub.add_parser("policy", help="задать политику")
p_pol.add_argument("--identity", default=None)
p_pol.add_argument("--cpu-threshold", type=int, default=None)
p_rq = sub.add_parser("requeue", help="вернуть failed-задачу в очередь")
p_rq.add_argument("task_id")
p_rq.add_argument("-r", "--reason", default="manual requeue")
sub.add_parser("status", help="состояние харнеса")
sub.add_parser("tasks", help="доска задач")
p_ev = sub.add_parser("events", help="хвост стора")
p_ev.add_argument("-n", type=int, default=15)
p_ev.add_argument("--after", type=int, default=0)
p_init = sub.add_parser("init", help="инициализация стора")
p_init.add_argument("--identity", default="Simbiot Harness — ритмичный агент кооператива")
args = ap.parse_args()
if args.cmd == "init":
es.init_db()
commands.set_policy(identity=args.identity)
print("OK: стор инициализирован, политика записана")
elif args.cmd == "task":
tid, size = commands.submit_task(args.title, args.details, args.minutes)
print(f"OK: задача {tid} [{size}] подана")
elif args.cmd == "policy":
pol = commands.set_policy(identity=args.identity, cpu_alert_threshold=args.cpu_threshold)
print("OK:", json.dumps(pol, ensure_ascii=False))
elif args.cmd == "requeue":
es.init_db()
tid = commands.requeue_task(args.task_id, args.reason)
print(f"OK: задача {tid} возвращена в очередь")
elif args.cmd == "status":
es.init_db()
print(json.dumps(commands.status(), ensure_ascii=False, indent=1))
elif args.cmd == "tasks":
es.init_db()
for t in commands.list_tasks():
print(f"{t['id']} [{t['size']:2}] {t['status']:7} {t['title'][:70]}")
elif args.cmd == "events":
es.init_db()
for ev in es.get_events(args.after, args.n):
d = ev["data"][:110]
print(f"{ev['id']:4} {ev['timestamp'][11:19]} {ev['aggregate']:8} {ev['event_type']:16} {d}")
if __name__ == "__main__":
main()

5
harness/__init__.py Normal file
View file

@ -0,0 +1,5 @@
"""Simbiot Harness — тактовая лестница на CQRS + Event Sourcing.
Уровни 1с/1м/1ч/1д (v0.1). Стартовая точка — strands-agents/harness-sdk.
"""
__version__ = "0.1.0"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

68
harness/_llm_worker.py Normal file
View file

@ -0,0 +1,68 @@
"""Воркер одного LLM-тика (запускается сабпроцессом из isolated_runner).
Читает payload из stdin, гоняет create_harness() один раз и умирает —
каждый тик получает чистый asyncio-рантайм Strands.
Трасса пишется напрямую в event store (es доступен).
Результат — в JSON-файл (path в payload['result_path']); stdout — только лог.
"""
import json
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
def _emit(payload, obj):
rp = payload.get("result_path")
if rp:
try:
with open(rp, "w") as f:
json.dump(obj, f, ensure_ascii=False)
except OSError:
pass
def main():
payload = json.loads(sys.stdin.read())
from harness import es
from harness.runtime import setup_provider, _wire_trace_hooks
setup_provider()
from strands_harness import create_harness
trace_id = payload.get("trace_prefix") or "tick"
try:
agent = create_harness(
model=f"openai/{payload['model']}",
effort=payload.get("effort", "auto"),
instructions=payload.get("instructions", ""),
session=None,
builtin_tools=("read", "write", "edit", "shell") if payload.get("can_shell") else [],
memory=False,
skills=False,
)
agent = _wire_trace_hooks(agent, es, trace_id)
result = agent(payload.get("prompt", ""))
usage = {}
try:
msgs = getattr(agent, "messages", None) or []
for m in reversed(msgs):
u = (m or {}).get("message", {}).get("usage")
if u:
usage = {
"prompt_tokens": u.get("inputTokens") or u.get("prompt_tokens"),
"completion_tokens": u.get("outputTokens") or u.get("completion_tokens"),
}
break
except Exception:
pass
_emit(payload, {"ok": True, "text": str(result), "usage": usage, "trace_id": trace_id})
except Exception as e:
import traceback
_emit(payload, {"ok": False, "error": f"{e}\n{traceback.format_exc()[-800:]}"})
# орфан-воркер после убийства родителя может умереть на печати — не критично
if __name__ == "__main__":
main()

67
harness/commands.py Normal file
View file

@ -0,0 +1,67 @@
"""Simbiot Harness — command side (CQRS): валидация + события.
Задачи подаются через CLI/Telegram/файл-инбокс; каждая команда
валидируется и только потом пишет событие.
"""
import os
import secrets
from datetime import datetime, timezone
from . import es
from .levels import size_class
def submit_task(title: str, details: str = "", minutes: int = 5, source: str = "cli"):
"""SubmitTask: класс по оценке длительности. Идемпотентно по id."""
task_id = f"t-{datetime.now(timezone.utc).strftime('%y%m%d%H%M%S')}-{secrets.token_hex(3)}"
size = size_class(minutes)
es.append_event("task", task_id, "TaskSubmitted", {
"id": task_id, "title": title, "details": details,
"size": size, "est_minutes": minutes, "source": source,
})
return task_id, size
def requeue_task(task_id: str, reason: str = "manual requeue"):
"""Вернуть failed/прерванную задачу в очередь (TaskPromoted → ready)."""
model = es.run_projections()
t = model["tasks"].get(task_id)
if t is None:
raise ValueError(f"задача {task_id} не найдена")
if t["status"] not in ("failed", "done"):
raise ValueError(f"задача {task_id} в статусе {t['status']} — requeue не нужен")
es.append_event("task", task_id, "TaskPromoted", {"to": t["size"], "reason": reason})
return task_id
def set_policy(identity: str = None, cpu_alert_threshold: int = None):
"""PolicyChanged: идентичность + пороги. Мержим поверх текущей политики."""
model = es.run_projections()
current = dict(model.get("policy") or {})
patch = {}
if identity is not None:
patch["identity"] = identity
if cpu_alert_threshold is not None:
patch["cpu_alert_threshold"] = cpu_alert_threshold
current.update(patch)
es.append_event("harness", "policy", "PolicyChanged", current)
return current
def list_tasks():
model = es.run_projections()
out = []
for t in model["tasks"].values():
out.append(t)
return out
def status():
model = es.run_projections()
return {
"cycle_state": model["cycle_state"],
"tasks_ready": sum(1 for t in model["tasks"].values() if t["status"] == "ready"),
"tasks_done": sum(1 for t in model["tasks"].values() if t["status"] == "done"),
"digests": len(model["digests"]),
"spend": model.get("spend", [])[-8:],
}

251
harness/es.py Normal file
View file

@ -0,0 +1,251 @@
"""Simbiot Harness — ES-спина: append-only event store + проекции.
Образец — кооп-портал (aggregate, aggregate_id, event_type, data, version).
Идемпотентные команды; тики защищены UNIQUE(level, seq).
"""
import json
import os
import sqlite3
import threading
import time
from datetime import datetime, timezone
DB_PATH = os.environ.get("SIMBIOT_DB", os.path.join(os.path.dirname(__file__), "harness.db"))
# RLock обязателен: rebuild_projections держит лок и зовёт
# run_projections, который берёт его же — обычный Lock давил сам себя.
_LOCK = threading.RLock()
_read_model = None # инкрементальный read model (глобал для инкрементальных проекций)
def _conn():
db = sqlite3.connect(DB_PATH, timeout=30)
db.execute("PRAGMA journal_mode=WAL")
db.execute("PRAGMA busy_timeout=30000")
db.row_factory = sqlite3.Row
return db
def init_db():
with _LOCK:
db = _conn()
db.executescript(
"""
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
aggregate TEXT NOT NULL,
aggregate_id TEXT NOT NULL,
event_type TEXT NOT NULL,
data TEXT NOT NULL,
timestamp TEXT NOT NULL,
version INTEGER NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_events_agg
ON events(aggregate, aggregate_id, version);
CREATE TABLE IF NOT EXISTS projection_meta (
name TEXT PRIMARY KEY,
last_event INTEGER NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS read_model (
name TEXT PRIMARY KEY,
model TEXT NOT NULL,
updated_at TEXT NOT NULL
);
"""
)
# идемпотентность тиков: один уровень — один seq
db.executescript(
"""
CREATE UNIQUE INDEX IF NOT EXISTS idx_cycle_tick
ON events(aggregate, aggregate_id) WHERE aggregate='cycle';
"""
)
# aggregate_id для cycle = "level:seq", поэтому общий уникальный индекс
# (aggregate, aggregate_id) уже даёт UNIQUE(level, seq).
db.commit()
db.close()
def append_event(aggregate: str, aggregate_id: str, event_type: str, data: dict):
"""Append-only запись события. Возвращает (event_id, version) или None при дубликате."""
with _LOCK:
db = _conn()
row = db.execute(
"SELECT MAX(version) AS v FROM events WHERE aggregate=? AND aggregate_id=?",
(aggregate, aggregate_id),
).fetchone()
version = (row["v"] or 0) + 1
ts = datetime.now(timezone.utc).isoformat()
try:
cur = db.execute(
"INSERT INTO events (aggregate,aggregate_id,event_type,data,timestamp,version) VALUES (?,?,?,?,?,?)",
(aggregate, aggregate_id, event_type,
json.dumps(data, ensure_ascii=False), ts, version),
)
db.commit()
eid = cur.lastrowid
except sqlite3.IntegrityError:
db.close()
return None # дубль (тик с тем же level:seq) — идемпотентно пропускаем
db.close()
return (eid, version)
def append_tick(level: str, seq: int):
"""CycleTicked идемпотентно: aggregate_id = 'level:seq' → UNIQUE даёт защиту."""
return append_event("cycle", f"{level}:{seq}", "CycleTicked", {"level": level, "seq": seq})
def get_events(after_id: int = 0, limit: int = 1000):
db = _conn()
rows = db.execute(
"SELECT * FROM events WHERE id > ? ORDER BY id LIMIT ?", (after_id, limit)
).fetchall()
db.close()
return [dict(r) for r in rows]
# ────────────────────────── ПРОЕКЦИИ ──────────────────────────
def _default_model():
return {
"tasks": {}, # task_id → {title, size, status, level, session_id, result_ref, ...}
"digests": {}, # (level, period) → {summary_ref, text}
"cycle_state": {}, # level → {busy, current_task, last_tick}
"metrics": [], # [{level, ticks, absorbed, llm_ticks, tokens, cost}]
"agents": {}, # level → {model, calls}
}
def _load_meta(db, name):
row = db.execute("SELECT last_event FROM projection_meta WHERE name=?", (name,)).fetchone()
return row["last_event"] if row else 0
def _save_meta(db, name, last_event):
db.execute(
"INSERT INTO projection_meta (name, last_event, updated_at) VALUES (?,?,?) "
"ON CONFLICT(name) DO UPDATE SET last_event=excluded.last_event, updated_at=excluded.updated_at",
(name, last_event, datetime.now(timezone.utc).isoformat()),
)
def _apply_event(model: dict, ev: dict):
etype, data = ev["event_type"], json.loads(ev["data"])
aid = ev["aggregate_id"]
if etype == "TaskSubmitted":
model["tasks"][aid] = {
"id": aid, "title": data.get("title", ""), "size": data.get("size", "M"),
"status": "ready", "level": None, "session_id": None,
"result_ref": None, "created": ev["timestamp"],
}
elif etype == "TaskClaimed":
t = model["tasks"].get(aid)
if t:
t.update(status="running", level=data.get("level"), session_id=data.get("session_id"))
elif etype == "TaskCompleted":
t = model["tasks"].get(aid)
if t:
t.update(status="done", result_ref=data.get("result_ref"), finished=ev["timestamp"])
elif etype == "TaskFailed":
t = model["tasks"].get(aid)
if t:
t.update(status="failed", error=data.get("error"), finished=ev["timestamp"])
elif etype == "TaskPromoted":
t = model["tasks"].get(aid)
if t:
t.update(status="ready", level=None, session_id=None,
promoted_to=data.get("to"), promotions=(t.get("promotions") or 0) + 1)
elif etype == "DigestWritten":
key = f"{data['level']}:{data['period']}"
model["digests"][key] = {
"level": data["level"], "period": data["period"],
"summary_ref": data.get("summary_ref"),
"text": data.get("text", ""),
"ts": ev["timestamp"],
}
elif etype == "CycleTicked":
level = data.get("level", "?")
cs = model["cycle_state"].setdefault(level, {"busy": False, "current_task": None, "ticks": 0})
cs["ticks"] = cs.get("ticks", 0) + 1
cs["last_tick"] = ev["timestamp"]
elif etype == "TickAbsorbed":
level = data.get("level", "?")
cs = model["cycle_state"].setdefault(level, {"busy": False, "current_task": None, "ticks": 0})
cs["absorbed"] = cs.get("absorbed", 0) + 1
elif etype == "LevelPaused":
model["cycle_state"].setdefault(data["level"], {})["paused"] = True
elif etype == "LevelResumed":
model["cycle_state"].setdefault(data["level"], {})["paused"] = False
elif etype == "PolicyChanged":
model.setdefault("policy", {}).update(data)
elif etype == "BudgetSpent":
model.setdefault("spend", []).append(data)
# trace-события (InvocationStarted/ToolCalled/ToolResult) не материализуем — они для дебага
def _load_model(db):
"""Персистентный read model: без него каждый новый процесс
стартовал с пустой моделью при уже продвинутом last_event —
и терял TaskSubmitted/PolicyChanged навсегда."""
row = db.execute("SELECT model FROM read_model WHERE name='main'").fetchone()
if row:
try:
return json.loads(row["model"])
except (ValueError, TypeError):
pass
return None
def _save_model(db, model):
db.execute(
"INSERT INTO read_model (name, model, updated_at) VALUES ('main', ?, ?) "
"ON CONFLICT(name) DO UPDATE SET model=excluded.model, updated_at=excluded.updated_at",
(json.dumps(model, ensure_ascii=False, default=str), datetime.now(timezone.utc).isoformat()),
)
def run_projections():
"""Инкрементальная проекция с персистентным read model."""
global _read_model
with _LOCK:
db = _conn()
last = _load_meta(db, "main")
rows = db.execute("SELECT * FROM events WHERE id > ? ORDER BY id", (last,)).fetchall()
if _read_model is None:
_read_model = _load_model(db) or _default_model()
if not rows:
db.close()
return _read_model
for r in rows:
_apply_event(_read_model, dict(r))
_save_meta(db, "main", rows[-1]["id"])
_save_model(db, _read_model)
db.commit()
db.close()
return _read_model
def rebuild_projections():
"""Полный replay с нуля (проекции одноразовые, стор — истина)."""
global _read_model
with _LOCK:
db = _conn()
db.executescript(
"""
CREATE TABLE IF NOT EXISTS projection_meta (
name TEXT PRIMARY KEY, last_event INTEGER NOT NULL, updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS read_model (
name TEXT PRIMARY KEY, model TEXT NOT NULL, updated_at TEXT NOT NULL
);
DELETE FROM projection_meta;
DELETE FROM read_model;
"""
)
db.commit()
db.close()
_read_model = None
return run_projections()

View file

@ -0,0 +1,70 @@
"""Simbiot Harness — изолированный раннер LLM-тика.
Strands держит глобальный asyncio-рантайм: несколько create_harness()
в одном процессе валит его («cannot schedule new futures after shutdown»).
Решение — каждый LLM-тик в ОТДЕЛЬНОМ сабпроцессе: чистый asyncio-рантайм
на тик, полная изоляция, ноль утечек. Демон живёт вечно.
"""
import json
import os
import subprocess
import sys
_HARNESS_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_VENV_PY = os.path.join(_HARNESS_DIR, "venv", "bin", "python")
_WORKER = os.path.join(_HARNESS_DIR, "harness", "_llm_worker.py")
MARKER = "@@SIMBIOT_RESULT@@"
def run_llm_tick(level_cfg, prompt: str, session_id=None, trace_prefix=None, timeout=None):
"""Запускает LLM-тик в сабпроцессе. Возвращает (text, usage, trace_id).
События трассы воркер пишет в стор сам (у него есть es).
Протокол: воркер пишет результат в JSON-ФАЙЛ (путь передан в env);
stdout/stderr только для лога — библиотечный шум strands не мешает.
"""
import tempfile
result_path = tempfile.mktemp(prefix="simbiot_tick_", suffix=".json", dir="/tmp")
payload = {
"model": level_cfg.model,
"effort": level_cfg.effort,
"instructions": level_cfg.mandate,
"prompt": prompt,
"session_id": None, # сессии пока выключены: .agent/sessions растёт без очистки
"trace_prefix": trace_prefix,
"can_shell": level_cfg.can_shell,
"ctx_tokens": level_cfg.ctx_tokens,
"result_path": result_path,
}
env = dict(os.environ)
env["SIMBIOT_DB"] = env.get("SIMBIOT_DB", os.path.join(_HARNESS_DIR, "harness", "harness.db"))
if timeout is None:
# таймаут чуть меньше интервала уровня, чтобы тик не наезжал на границу
timeout = max(120, min(int(level_cfg.every * 0.8), 900))
try:
proc = subprocess.run(
[_VENV_PY, _WORKER],
input=json.dumps(payload).encode(),
capture_output=True,
timeout=timeout,
cwd=_HARNESS_DIR,
env=env,
)
finally:
pass
out = None
if os.path.exists(result_path):
try:
with open(result_path) as f:
out = json.load(f)
except ValueError:
out = None
try:
os.unlink(result_path)
except OSError:
pass
if out is None:
return None, {"error": f"worker result file missing: stdout={(proc.stdout or b'')[:200]!r} stderr={(proc.stderr or b'').decode(errors='replace')[-300:]}"}, None
if not out.get("ok"):
return None, {"error": (out.get("error") or "unknown")[:500]}, None
return out.get("text", ""), out.get("usage", {}), out.get("trace_id")

80
harness/levels.py Normal file
View file

@ -0,0 +1,80 @@
"""Simbiot Harness — уровни и конфигурация тактовой лестницы.
Контекст ∝ масштабу: чем крупнее цикл, тем больше контекст и модель.
"""
from dataclasses import dataclass, field
SEC = 1
MIN = 60
HOUR = 3600
DAY = 86400
WEEK = 7 * DAY
MONTH = 30 * DAY
QUARTER = 91 * DAY
YEAR = 366 * DAY # округляем год как 366 дней для простоты выравнивания
@dataclass
class CycleLevel:
name: str
label: str
every: int # секунд между тиками
uses_llm: bool
model: str # модель 9router (или "none")
effort: str # effort Strands: auto/low/medium/high/off
ctx_tokens: int # бюджет контекста уровня
task_classes: tuple # какие классы задач может брать
mandate: str # мандат уровня (в промпт)
can_shell: bool # инструменты уровня
def seq(self, ts: int) -> int:
"""Номер такта уровня для unix-ts (для идемпотентности тиков)."""
return ts // self.every
LEVELS = {
"sec": CycleLevel(
name="sec", label="Секунда · рефлекс", every=SEC, uses_llm=False,
model="none", effort="off", ctx_tokens=0, task_classes=(),
mandate=("Ты — рефлекторный уровень харнеса. Работает без LLM: телеметрия, "
"heartbeat, алерты. Никаких решений — только наблюдение."),
can_shell=False,
),
"min": CycleLevel(
name="min", label="Минута · пульс", every=MIN, uses_llm=True,
model="auto", effort="low", ctx_tokens=4_000, task_classes=("S",),
mandate=("Ты — минутный пульс харнеса Simbiot. Триаж входящих, быстрые ответы, "
"мелкие фиксы (S-задачи до 5 минут). Отвечай кратко, не углубляйся. "
"Если задача больше твоего класса — откажись и предложи промоушен."),
can_shell=True,
),
"hour": CycleLevel(
name="hour", label="Час · поток", every=HOUR, uses_llm=True,
model="auto", effort="medium", ctx_tokens=32_000, task_classes=("S", "M"),
mandate=("Ты — часовой поток харнеса Simbiot. Дайджест часа: что произошло, "
"статусы фоновых задач, батч мелочей (S/M). Консолидируй минутные "
"события в выжимку — сырое наверх не поднимай."),
can_shell=True,
),
"day": CycleLevel(
name="day", label="День · манифест", every=DAY, uses_llm=True,
model="auto", effort="high", ctx_tokens=128_000, task_classes=("S", "M", "L"),
mandate=("Ты — дневной цикл харнеса Simbiot. Манифест дня: план, выбор задач "
"L-класса, дневной отчёт. Думай глубоко, планируй на день вперёд."),
can_shell=True,
),
}
# v0.2+: week/month/quarter/year — включаются по мере готовности
FUTURE_LEVELS = ("week", "month", "quarter", "year")
def size_class(minutes: int) -> str:
"""Класс задачи по оценке длительности."""
if minutes <= 5:
return "S"
if minutes <= 60:
return "M"
if minutes <= 8 * 60:
return "L"
return "XL"

130
harness/runtime.py Normal file
View file

@ -0,0 +1,130 @@
"""Simbiot Harness — Strands runtime-адаптер.
create_harness() на тик: модель/effort из конфига уровня.
Hooks → трасса в event store (агрегат trace).
"""
import json
import os
import uuid
from datetime import datetime, timezone
_HARNESS_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def _router_key():
env = os.environ.get("ROUTER_KEY", "")
if not env:
path = os.path.join(_HARNESS_DIR, ".env")
if os.path.exists(path):
for line in open(path):
if line.startswith("ROUTER_KEY="):
env = line.split("=", 1)[1].strip()
break
return env
def setup_provider():
"""9router как openai-совместимый провайдер strands."""
key = _router_key()
if not key:
raise RuntimeError("ROUTER_KEY не найден (.env)")
os.environ["OPENAI_API_KEY"] = key
os.environ["OPENAI_BASE_URL"] = os.environ.get("ROUTER_BASE_URL", "http://127.0.0.1:20128/v1")
def _wire_trace_hooks(agent, es, trace_id: str):
"""Каждая стадия агентного цикла → событие агрегата trace."""
from strands import Agent
from strands.hooks import BeforeInvocationEvent, BeforeToolCallEvent, AfterToolCallEvent
seq = {"n": 0}
def _trace(kind: str, payload: dict):
seq["n"] += 1
es.append_event("trace", f"{trace_id}:{seq['n']}", kind, payload)
def on_before_invocation(event):
_trace("InvocationStarted", {"trace_id": trace_id, "ts": _now()})
def on_before_tool(event):
tool = getattr(event, "tool_use", None) or {}
_trace("ToolCalled", {
"tool": tool.get("name", "?"),
"input": json.dumps(tool.get("input", {}), ensure_ascii=False, default=str)[:4000],
})
def on_after_tool(event):
try:
res = str(getattr(event, "result", ""))[:4000]
except Exception:
res = "<unreadable>"
_trace("ToolResult", {"tool": "?", "result": res})
agent.add_hook(on_before_invocation, BeforeInvocationEvent)
agent.add_hook(on_before_tool, BeforeToolCallEvent)
agent.add_hook(on_after_tool, AfterToolCallEvent)
return agent
def _now():
return datetime.now(timezone.utc).isoformat()
def build_agent(level_cfg, session_id=None, extra_instructions=""):
"""Собирает Strands-агента для тика уровня. None если уровень без LLM."""
if not level_cfg.uses_llm:
return None
setup_provider()
from strands_harness import create_harness
instructions = level_cfg.mandate + ("\n\n" + extra_instructions if extra_instructions else "")
agent = create_harness(
model=f"openai/{level_cfg.model}",
effort=level_cfg.effort,
instructions=instructions,
session={"id": session_id} if session_id else None,
builtin_tools=("read", "write", "edit", "shell") if level_cfg.can_shell else [],
memory=False,
skills=False,
)
return agent
def run_cycle(level_cfg, es, prompt: str, session_id=None, trace_prefix=None):
"""Прогон тика уровня с LLM и трассой. Возвращает (text, usage_dict, trace_id)."""
setup_provider()
from strands_harness import create_harness
trace_id = trace_prefix or uuid.uuid4().hex[:12]
instructions = level_cfg.mandate
agent = create_harness(
model=f"openai/{level_cfg.model}",
effort=level_cfg.effort,
instructions=instructions,
session={"id": session_id} if session_id else None,
builtin_tools=("read", "write", "edit", "shell") if level_cfg.can_shell else [],
memory=False,
skills=False,
)
agent = _wire_trace_hooks(agent, es, trace_id)
result = agent(prompt)
# AgentResult: текст из message, usage из metrics.accumulated_usage
# (старый разбор agent.messages молчал → BudgetSpent всегда был пуст).
text = str(result)
usage = _extract_usage(result)
return text, usage, trace_id
def _extract_usage(result):
"""Usage из AgentResult.metrics.accumulated_usage (input/output/total)."""
try:
u = getattr(getattr(result, "metrics", None), "accumulated_usage", None)
if u:
return {
"prompt_tokens": u.get("inputTokens"),
"completion_tokens": u.get("outputTokens"),
"total_tokens": u.get("totalTokens"),
}
except Exception:
pass
return {}

236
harness/scheduler.py Normal file
View file

@ -0,0 +1,236 @@
"""Simbiot Harness — scheduler: тактовая лестница + cycle runners.
Правила:
- тик уровня идемпотентен (UNIQUE level:seq);
- один исполнитель на уровень → сериализация;
- занятый уровень поглощает тик (TickAbsorbed) и пишет
детерминированную дайджест-заглушку — без LLM;
- запущенная задача не прерывается.
"""
import json
import os
import time
import traceback
from datetime import datetime, timezone, timedelta
from . import es
from .levels import LEVELS
from . import runtime
# интервал демона
POLL = 1.0
# уровни, включённые в v0.1 (порядок для лога)
ACTIVE = ["sec", "min", "hour", "day"]
# состояние исполнителей уровня: level → {"busy": bool, "task": id|None}
_runners = {}
def _now():
return datetime.now(timezone.utc)
def _log(msg: str):
print(f"[{datetime.now().strftime('%H:%M:%S')}] {msg}", flush=True)
def tick_number(level_cfg, ts: int):
return ts // level_cfg.every
def emit_tick(level_cfg, seq: int):
"""CycleTicked идемпотентно. Возвращает True если это новый тик."""
r = es.append_tick(level_cfg.name, seq)
return r is not None
def deterministic_digest_stub(level_cfg, seq: int, reason: str):
"""Заглушка дайджеста для поглощённого тика — детерминированно, без LLM."""
period = datetime.fromtimestamp(seq * level_cfg.every, tz=timezone.utc)
es.append_event("cycle", f"stub:{level_cfg.name}:{seq}", "DigestWritten", {
"level": level_cfg.name,
"period": period.isoformat(),
"summary_ref": None,
"text": f"[absorbed:{reason}] уровень {level_cfg.name} был занят/пропущен; такт {seq}",
"stub": True,
})
def find_task(level_cfg, model: dict):
"""Ищет ready-задачу доступного классу уровня. Возвращает task_id или None."""
classes = level_cfg.task_classes
for tid, t in model["tasks"].items():
if t["status"] == "ready" and t.get("size") in classes:
return tid
return None
def build_context(level_cfg, model: dict, task=None):
"""Сборка контекста тика: мандат + память уровня + выжимки нижних + задача."""
parts = [level_cfg.mandate]
# идентичность/цели v0.1 — статично из policy (если есть)
policy = model.get("policy")
if policy and policy.get("identity"):
parts.append("ИДЕНТИЧНОСТЬ: " + policy["identity"])
# выжимки нижних уровней: последние дайджесты
order = ["min", "hour", "day"]
idx = order.index(level_cfg.name) if level_cfg.name in order else 0
lower = order[:idx]
digests = []
for lvl in lower:
for key, d in model["digests"].items():
if d["level"] == lvl and not d.get("stub"):
digests.append(d)
digests.sort(key=lambda d: d["ts"], reverse=True)
if digests:
parts.append("ВЫЖИМКИ НИЖНИХ УРОВНЕЙ (последние, сжатые):")
for d in digests[:6]:
parts.append(f"- [{d['level']}] {d['text'][:400]}")
# доска задач
tasks = [t for t in model["tasks"].values() if t["status"] in ("ready", "running")]
if tasks:
lines = [f"- {t['id']}: {t['title'][:80]} [{t['size']}] {t['status']}" for t in tasks[:15]]
parts.append("ДОСКА ЗАДАЧ:\n" + "\n".join(lines))
if task:
parts.append(f"ТЕКУЩАЯ ЗАДАЧА: {task['id']}: {task['title']}\nВыполни её и выдай результат.")
return "\n\n".join(parts)
def run_level_tick(level_cfg, seq: int, model: dict):
"""Один тик уровня с LLM: дайджест + (опц.) задача. Возвращает текст.
LLM-тик гоняется в изолированном сабпроцессе (isolated_runner):
у Strands глобальный asyncio-рантайм, второй create_harness()
в том же процессе валит его («cannot schedule new futures»).
"""
from . import isolated_runner
task_id = find_task(level_cfg, model)
task = model["tasks"].get(task_id) if task_id else None
if task:
es.append_event("task", task_id, "TaskClaimed", {
"level": level_cfg.name, "session_id": f"s-{level_cfg.name}-{seq}",
})
prompt = build_context(level_cfg, model, task)
text, usage, trace_id = isolated_runner.run_llm_tick(
level_cfg, prompt,
session_id=f"s-{level_cfg.name}-{seq}",
trace_prefix=f"{level_cfg.name}-{seq}",
)
if text is None:
# воркер упал — задача не выполнена, откатываем claim
if task:
es.append_event("task", task_id, "TaskFailed", {
"error": str(usage.get("error", "llm worker failed"))[:500],
})
raise RuntimeError(str(usage.get("error", "llm worker failed")))
# дайджест тика
period = datetime.fromtimestamp(seq * level_cfg.every, tz=timezone.utc)
es.append_event("cycle", f"digest:{level_cfg.name}:{seq}", "DigestWritten", {
"level": level_cfg.name, "period": period.isoformat(),
"summary_ref": trace_id, "text": (text or "")[:2000],
})
if usage:
es.append_event("harness", "budget", "BudgetSpent", {
"level": level_cfg.name, **{k: v for k, v in usage.items() if k != "error"},
})
if task:
es.append_event("task", task_id, "TaskCompleted", {
"result_ref": trace_id, "answer": (text or "")[:2000],
})
return text, task_id
def process_level(level_cfg, model: dict):
"""Тик одного уровня: занят? → absorb; иначе работа."""
st = _runners.setdefault(level_cfg.name, {"busy": False, "task": None})
if st["busy"]:
seq = tick_number(level_cfg, int(time.time()))
deterministic_digest_stub(level_cfg, seq, "busy")
es.append_event("cycle", f"absorbed:{level_cfg.name}:{seq}", "TickAbsorbed",
{"level": level_cfg.name, "reason": "busy"})
return "absorbed"
seq = tick_number(level_cfg, int(time.time()))
if not emit_tick(level_cfg, seq):
return "already" # тик этого такта уже был (идемпотентность)
st["busy"] = True
try:
if not level_cfg.uses_llm:
# секундный уровень: детерминированный рефлекс без LLM
text, task_id = _reflex_tick(level_cfg, seq, model)
return f"reflex ok {task_id or ''}".strip()
text, task_id = run_level_tick(level_cfg, seq, model)
return f"ran (task={task_id or '-'})"
except Exception as e:
es.append_event("harness", "errors", "CycleError", {
"level": level_cfg.name, "error": str(e)[:1000],
"trace": traceback.format_exc()[-2000:],
})
return "error: " + str(e)[:120]
finally:
st["busy"] = False
def _reflex_tick(level_cfg, seq: int, model: dict):
"""Секундный рефлекс: телеметрия + алерты, без LLM."""
import os
load = None
try:
load = os.getloadavg()[0]
except Exception:
pass
period = datetime.fromtimestamp(seq * level_cfg.every, tz=timezone.utc)
payload = {"level": "sec", "seq": seq, "load1": load,
"ts": period.isoformat()}
# алерт-порог из policy (по умолчанию 8 — как simbiotd)
thr = (model.get("policy") or {}).get("cpu_alert_threshold", 8)
if load is not None and load >= thr:
payload["alert"] = f"load {load:.2f} >= {thr}"
es.append_event("cycle", f"reflex:{seq}", "ReflexObserved", payload)
return payload, None
def main_loop():
"""Демон: уровни тикают независимо, каждый в своём потоке.
LLM-тик одного уровня больше не блокирует остальные
(раньше минутный тик замораживал и сек-рефлекс, и час)."""
import threading
es.init_db()
_log("Simbiot Harness запущен; уровни: " + ", ".join(ACTIVE))
last_seq = {}
threads = {}
while True:
for name in ACTIVE:
cfg = LEVELS[name]
seq = tick_number(cfg, int(time.time()))
if last_seq.get(name) == seq:
continue # этого такта ещё не было — ждём границу
last_seq[name] = seq
if cfg.uses_llm and _runners.get(name, {}).get("busy"):
# уровень ещё работает в предыдущем такте — тик поглощается,
# но остальные уровни продолжают жить
continue
t = threads.get(name)
if t is not None and t.is_alive():
continue
def _run(cfg=cfg, seq=seq):
model = es.run_projections()
state = process_level(cfg, model)
if state not in ("already",):
_log(f"{cfg.name} #{seq}: {state}")
t = threading.Thread(target=_run, daemon=True, name=f"tick-{name}")
threads[name] = t
t.start()
time.sleep(POLL)
if __name__ == "__main__":
main_loop()

247
venv/bin/Activate.ps1 Normal file
View file

@ -0,0 +1,247 @@
<#
.Synopsis
Activate a Python virtual environment for the current PowerShell session.
.Description
Pushes the python executable for a virtual environment to the front of the
$Env:PATH environment variable and sets the prompt to signify that you are
in a Python virtual environment. Makes use of the command line switches as
well as the `pyvenv.cfg` file values present in the virtual environment.
.Parameter VenvDir
Path to the directory that contains the virtual environment to activate. The
default value for this is the parent of the directory that the Activate.ps1
script is located within.
.Parameter Prompt
The prompt prefix to display when this virtual environment is activated. By
default, this prompt is the name of the virtual environment folder (VenvDir)
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
.Example
Activate.ps1
Activates the Python virtual environment that contains the Activate.ps1 script.
.Example
Activate.ps1 -Verbose
Activates the Python virtual environment that contains the Activate.ps1 script,
and shows extra information about the activation as it executes.
.Example
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
Activates the Python virtual environment located in the specified location.
.Example
Activate.ps1 -Prompt "MyPython"
Activates the Python virtual environment that contains the Activate.ps1 script,
and prefixes the current prompt with the specified string (surrounded in
parentheses) while the virtual environment is active.
.Notes
On Windows, it may be required to enable this Activate.ps1 script by setting the
execution policy for the user. You can do this by issuing the following PowerShell
command:
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
For more information on Execution Policies:
https://go.microsoft.com/fwlink/?LinkID=135170
#>
Param(
[Parameter(Mandatory = $false)]
[String]
$VenvDir,
[Parameter(Mandatory = $false)]
[String]
$Prompt
)
<# Function declarations --------------------------------------------------- #>
<#
.Synopsis
Remove all shell session elements added by the Activate script, including the
addition of the virtual environment's Python executable from the beginning of
the PATH variable.
.Parameter NonDestructive
If present, do not remove this function from the global namespace for the
session.
#>
function global:deactivate ([switch]$NonDestructive) {
# Revert to original values
# The prior prompt:
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
}
# The prior PYTHONHOME:
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
}
# The prior PATH:
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
}
# Just remove the VIRTUAL_ENV altogether:
if (Test-Path -Path Env:VIRTUAL_ENV) {
Remove-Item -Path env:VIRTUAL_ENV
}
# Just remove VIRTUAL_ENV_PROMPT altogether.
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
}
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
}
# Leave deactivate function in the global namespace if requested:
if (-not $NonDestructive) {
Remove-Item -Path function:deactivate
}
}
<#
.Description
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
given folder, and returns them in a map.
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
two strings separated by `=` (with any amount of whitespace surrounding the =)
then it is considered a `key = value` line. The left hand string is the key,
the right hand is the value.
If the value starts with a `'` or a `"` then the first and last character is
stripped from the value before being captured.
.Parameter ConfigDir
Path to the directory that contains the `pyvenv.cfg` file.
#>
function Get-PyVenvConfig(
[String]
$ConfigDir
) {
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
# An empty map will be returned if no config file is found.
$pyvenvConfig = @{ }
if ($pyvenvConfigPath) {
Write-Verbose "File exists, parse `key = value` lines"
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
$pyvenvConfigContent | ForEach-Object {
$keyval = $PSItem -split "\s*=\s*", 2
if ($keyval[0] -and $keyval[1]) {
$val = $keyval[1]
# Remove extraneous quotations around a string value.
if ("'""".Contains($val.Substring(0, 1))) {
$val = $val.Substring(1, $val.Length - 2)
}
$pyvenvConfig[$keyval[0]] = $val
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
}
}
}
return $pyvenvConfig
}
<# Begin Activate script --------------------------------------------------- #>
# Determine the containing directory of this script
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
$VenvExecDir = Get-Item -Path $VenvExecPath
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
# Set values required in priority: CmdLine, ConfigFile, Default
# First, get the location of the virtual environment, it might not be
# VenvExecDir if specified on the command line.
if ($VenvDir) {
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
}
else {
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
Write-Verbose "VenvDir=$VenvDir"
}
# Next, read the `pyvenv.cfg` file to determine any required value such
# as `prompt`.
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
# Next, set the prompt from the command line, or the config file, or
# just use the name of the virtual environment folder.
if ($Prompt) {
Write-Verbose "Prompt specified as argument, using '$Prompt'"
}
else {
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
$Prompt = $pyvenvCfg['prompt'];
}
else {
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
$Prompt = Split-Path -Path $venvDir -Leaf
}
}
Write-Verbose "Prompt = '$Prompt'"
Write-Verbose "VenvDir='$VenvDir'"
# Deactivate any currently active virtual environment, but leave the
# deactivate function in place.
deactivate -nondestructive
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
# that there is an activated venv.
$env:VIRTUAL_ENV = $VenvDir
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
Write-Verbose "Setting prompt to '$Prompt'"
# Set the prompt to include the env name
# Make sure _OLD_VIRTUAL_PROMPT is global
function global:_OLD_VIRTUAL_PROMPT { "" }
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
function global:prompt {
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
_OLD_VIRTUAL_PROMPT
}
$env:VIRTUAL_ENV_PROMPT = $Prompt
}
# Clear PYTHONHOME
if (Test-Path -Path Env:PYTHONHOME) {
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
Remove-Item -Path Env:PYTHONHOME
}
# Add the venv to the PATH
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"

Binary file not shown.

70
venv/bin/activate Normal file
View file

@ -0,0 +1,70 @@
# This file must be used with "source bin/activate" *from bash*
# You cannot run it directly
deactivate () {
# reset old environment variables
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
PATH="${_OLD_VIRTUAL_PATH:-}"
export PATH
unset _OLD_VIRTUAL_PATH
fi
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
export PYTHONHOME
unset _OLD_VIRTUAL_PYTHONHOME
fi
# Call hash to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
hash -r 2> /dev/null
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
PS1="${_OLD_VIRTUAL_PS1:-}"
export PS1
unset _OLD_VIRTUAL_PS1
fi
unset VIRTUAL_ENV
unset VIRTUAL_ENV_PROMPT
if [ ! "${1:-}" = "nondestructive" ] ; then
# Self destruct!
unset -f deactivate
fi
}
# unset irrelevant variables
deactivate nondestructive
# on Windows, a path can contain colons and backslashes and has to be converted:
if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
# transform D:\path\to\venv to /d/path/to/venv on MSYS
# and to /cygdrive/d/path/to/venv on Cygwin
export VIRTUAL_ENV=$(cygpath /root/simbiot-harness/venv)
else
# use the path as-is
export VIRTUAL_ENV=/root/simbiot-harness/venv
fi
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/"bin":$PATH"
export PATH
# unset PYTHONHOME if set
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
# could use `if (set -u; : $PYTHONHOME) ;` in bash
if [ -n "${PYTHONHOME:-}" ] ; then
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
unset PYTHONHOME
fi
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
_OLD_VIRTUAL_PS1="${PS1:-}"
PS1='(venv) '"${PS1:-}"
export PS1
VIRTUAL_ENV_PROMPT='(venv) '
export VIRTUAL_ENV_PROMPT
fi
# Call hash to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
hash -r 2> /dev/null

27
venv/bin/activate.csh Normal file
View file

@ -0,0 +1,27 @@
# This file must be used with "source bin/activate.csh" *from csh*.
# You cannot run it directly.
# Created by Davide Di Blasi <davidedb@gmail.com>.
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
# Unset irrelevant variables.
deactivate nondestructive
setenv VIRTUAL_ENV /root/simbiot-harness/venv
set _OLD_VIRTUAL_PATH="$PATH"
setenv PATH "$VIRTUAL_ENV/"bin":$PATH"
set _OLD_VIRTUAL_PROMPT="$prompt"
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
set prompt = '(venv) '"$prompt"
setenv VIRTUAL_ENV_PROMPT '(venv) '
endif
alias pydoc python -m pydoc
rehash

69
venv/bin/activate.fish Normal file
View file

@ -0,0 +1,69 @@
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
# (https://fishshell.com/). You cannot run it directly.
function deactivate -d "Exit virtual environment and return to normal shell environment"
# reset old environment variables
if test -n "$_OLD_VIRTUAL_PATH"
set -gx PATH $_OLD_VIRTUAL_PATH
set -e _OLD_VIRTUAL_PATH
end
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
set -e _OLD_VIRTUAL_PYTHONHOME
end
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
set -e _OLD_FISH_PROMPT_OVERRIDE
# prevents error when using nested fish instances (Issue #93858)
if functions -q _old_fish_prompt
functions -e fish_prompt
functions -c _old_fish_prompt fish_prompt
functions -e _old_fish_prompt
end
end
set -e VIRTUAL_ENV
set -e VIRTUAL_ENV_PROMPT
if test "$argv[1]" != "nondestructive"
# Self-destruct!
functions -e deactivate
end
end
# Unset irrelevant variables.
deactivate nondestructive
set -gx VIRTUAL_ENV /root/simbiot-harness/venv
set -gx _OLD_VIRTUAL_PATH $PATH
set -gx PATH "$VIRTUAL_ENV/"bin $PATH
# Unset PYTHONHOME if set.
if set -q PYTHONHOME
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
set -e PYTHONHOME
end
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
# fish uses a function instead of an env var to generate the prompt.
# Save the current fish_prompt function as the function _old_fish_prompt.
functions -c fish_prompt _old_fish_prompt
# With the original prompt function renamed, we can override with our own.
function fish_prompt
# Save the return status of the last command.
set -l old_status $status
# Output the venv prompt; color taken from the blue of the Python logo.
printf "%s%s%s" (set_color 4B8BBE) '(venv) ' (set_color normal)
# Restore the return status of the previous command.
echo "exit $old_status" | .
# Output the original/"old" prompt.
_old_fish_prompt
end
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
set -gx VIRTUAL_ENV_PROMPT '(venv) '
end

6
venv/bin/cffi-gen-src Executable file
View file

@ -0,0 +1,6 @@
#!/root/simbiot-harness/venv/bin/python3.12
import sys
from cffi._cffi_gen_src import run
if __name__ == '__main__':
sys.argv[0] = sys.argv[0].removesuffix('.exe')
sys.exit(run())

6
venv/bin/httpx Executable file
View file

@ -0,0 +1,6 @@
#!/root/simbiot-harness/venv/bin/python3.12
import sys
from httpx import main
if __name__ == '__main__':
sys.argv[0] = sys.argv[0].removesuffix('.exe')
sys.exit(main())

6
venv/bin/httpx2 Executable file
View file

@ -0,0 +1,6 @@
#!/root/simbiot-harness/venv/bin/python3.12
import sys
from httpx2._main import main
if __name__ == '__main__':
sys.argv[0] = sys.argv[0].removesuffix('.exe')
sys.exit(main())

6
venv/bin/idna Executable file
View file

@ -0,0 +1,6 @@
#!/root/simbiot-harness/venv/bin/python3.12
import sys
from idna.cli import main
if __name__ == '__main__':
sys.argv[0] = sys.argv[0].removesuffix('.exe')
sys.exit(main())

54
venv/bin/jp.py Executable file
View file

@ -0,0 +1,54 @@
#!/root/simbiot-harness/venv/bin/python3.12
import sys
import json
import argparse
from pprint import pformat
import jmespath
from jmespath import exceptions
def main():
parser = argparse.ArgumentParser()
parser.add_argument('expression')
parser.add_argument('-f', '--filename',
help=('The filename containing the input data. '
'If a filename is not given then data is '
'read from stdin.'))
parser.add_argument('--ast', action='store_true',
help=('Pretty print the AST, do not search the data.'))
args = parser.parse_args()
expression = args.expression
if args.ast:
# Only print the AST
expression = jmespath.compile(args.expression)
sys.stdout.write(pformat(expression.parsed))
sys.stdout.write('\n')
return 0
if args.filename:
with open(args.filename, 'r') as f:
data = json.load(f)
else:
data = sys.stdin.read()
data = json.loads(data)
try:
sys.stdout.write(json.dumps(
jmespath.search(expression, data), indent=4, ensure_ascii=False))
sys.stdout.write('\n')
except exceptions.ArityError as e:
sys.stderr.write("invalid-arity: %s\n" % e)
return 1
except exceptions.JMESPathTypeError as e:
sys.stderr.write("invalid-type: %s\n" % e)
return 1
except exceptions.UnknownFunctionError as e:
sys.stderr.write("unknown-function: %s\n" % e)
return 1
except exceptions.ParseError as e:
sys.stderr.write("syntax-error: %s\n" % e)
return 1
if __name__ == '__main__':
sys.exit(main())

6
venv/bin/jsonschema Executable file
View file

@ -0,0 +1,6 @@
#!/root/simbiot-harness/venv/bin/python3.12
import sys
from jsonschema.cli import main
if __name__ == '__main__':
sys.argv[0] = sys.argv[0].removesuffix('.exe')
sys.exit(main())

6
venv/bin/mcp Executable file
View file

@ -0,0 +1,6 @@
#!/root/simbiot-harness/venv/bin/python3.12
import sys
from mcp.cli import app
if __name__ == '__main__':
sys.argv[0] = sys.argv[0].removesuffix('.exe')
sys.exit(app())

BIN
venv/bin/monty Executable file

Binary file not shown.

View file

@ -0,0 +1,6 @@
#!/root/simbiot-harness/venv/bin/python3.12
import sys
from opentelemetry.instrumentation.bootstrap import run
if __name__ == '__main__':
sys.argv[0] = sys.argv[0].removesuffix('.exe')
sys.exit(run())

View file

@ -0,0 +1,6 @@
#!/root/simbiot-harness/venv/bin/python3.12
import sys
from opentelemetry.instrumentation.auto_instrumentation import run
if __name__ == '__main__':
sys.argv[0] = sys.argv[0].removesuffix('.exe')
sys.exit(run())

8
venv/bin/pip Executable file
View file

@ -0,0 +1,8 @@
#!/root/simbiot-harness/venv/bin/python3.12
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
venv/bin/pip3 Executable file
View file

@ -0,0 +1,8 @@
#!/root/simbiot-harness/venv/bin/python3.12
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
venv/bin/pip3.12 Executable file
View file

@ -0,0 +1,8 @@
#!/root/simbiot-harness/venv/bin/python3.12
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

BIN
venv/bin/py-spy Executable file

Binary file not shown.

6
venv/bin/pydantic-monty Executable file
View file

@ -0,0 +1,6 @@
#!/root/simbiot-harness/venv/bin/python3.12
import sys
from pydantic_monty._cli import main
if __name__ == '__main__':
sys.argv[0] = sys.argv[0].removesuffix('.exe')
sys.exit(main())

1
venv/bin/python Symbolic link
View file

@ -0,0 +1 @@
python3.12

1
venv/bin/python3 Symbolic link
View file

@ -0,0 +1 @@
python3.12

1
venv/bin/python3.12 Symbolic link
View file

@ -0,0 +1 @@
/usr/bin/python3.12

6
venv/bin/uvicorn Executable file
View file

@ -0,0 +1,6 @@
#!/root/simbiot-harness/venv/bin/python3.12
import sys
from uvicorn.main import main
if __name__ == '__main__':
sys.argv[0] = sys.argv[0].removesuffix('.exe')
sys.exit(main())

6
venv/bin/watchmedo Executable file
View file

@ -0,0 +1,6 @@
#!/root/simbiot-harness/venv/bin/python3.12
import sys
from watchdog.watchmedo import main
if __name__ == '__main__':
sys.argv[0] = sys.argv[0].removesuffix('.exe')
sys.exit(main())

View file

@ -0,0 +1,33 @@
# This is a stub package designed to roughly emulate the _yaml
# extension module, which previously existed as a standalone module
# and has been moved into the `yaml` package namespace.
# It does not perfectly mimic its old counterpart, but should get
# close enough for anyone who's relying on it even when they shouldn't.
import yaml
# in some circumstances, the yaml module we imoprted may be from a different version, so we need
# to tread carefully when poking at it here (it may not have the attributes we expect)
if not getattr(yaml, '__with_libyaml__', False):
from sys import version_info
exc = ModuleNotFoundError if version_info >= (3, 6) else ImportError
raise exc("No module named '_yaml'")
else:
from yaml._yaml import *
import warnings
warnings.warn(
'The _yaml extension module is now located at yaml._yaml'
' and its location is subject to change. To use the'
' LibYAML-based parser and emitter, import from `yaml`:'
' `from yaml import CLoader as Loader, CDumper as Dumper`.',
DeprecationWarning
)
del warnings
# Don't `del yaml` here because yaml is actually an existing
# namespace member of _yaml.
__name__ = '_yaml'
# If the module is top-level (i.e. not a part of any specific package)
# then the attribute should be set to ''.
# https://docs.python.org/3.8/library/types.html
__package__ = ''

View file

@ -0,0 +1,295 @@
Metadata-Version: 2.4
Name: annotated-types
Version: 0.8.0
Summary: Reusable constraint types to use with typing.Annotated
Project-URL: Homepage, https://github.com/annotated-types/annotated-types
Project-URL: Source, https://github.com/annotated-types/annotated-types
Project-URL: Changelog, https://github.com/annotated-types/annotated-types/releases
Author-email: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com>, Samuel Colvin <s@muelcolvin.com>, Zac Hatfield-Dodds <zac@zhd.dev>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Environment :: MacOS X
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: Unix
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
# annotated-types
[![CI](https://github.com/annotated-types/annotated-types/workflows/CI/badge.svg?event=push)](https://github.com/annotated-types/annotated-types/actions?query=event%3Apush+branch%3Amain+workflow%3ACI)
[![pypi](https://img.shields.io/pypi/v/annotated-types.svg)](https://pypi.python.org/pypi/annotated-types)
[![versions](https://img.shields.io/pypi/pyversions/annotated-types.svg)](https://github.com/annotated-types/annotated-types)
[![license](https://img.shields.io/github/license/annotated-types/annotated-types.svg)](https://github.com/annotated-types/annotated-types/blob/main/LICENSE)
[PEP-593](https://peps.python.org/pep-0593/) added `typing.Annotated` as a way of
adding context-specific metadata to existing types, and specifies that
`Annotated[T, x]` _should_ be treated as `T` by any tool or library without special
logic for `x`.
This package provides metadata objects which can be used to represent common
constraints such as upper and lower bounds on scalar values and collection sizes,
a `Predicate` marker for runtime checks, and
descriptions of how we intend these metadata to be interpreted. In some cases,
we also note alternative representations which do not require this package.
## Install
```bash
pip install annotated-types
```
## Examples
```python
from typing import Annotated
from annotated_types import Gt, Len, Predicate
class MyClass:
age: Annotated[int, Gt(18)] # Valid: 19, 20, ...
# Invalid: 17, 18, "19", 19.0, ...
factors: list[Annotated[int, Predicate(is_prime)]] # Valid: 2, 3, 5, 7, 11, ...
# Invalid: 4, 8, -2, 5.0, "prime", ...
my_list: Annotated[list[int], Len(0, 10)] # Valid: [], [10, 20, 30, 40, 50]
# Invalid: (1, 2), ["abc"], [0] * 20
```
## Documentation
_While `annotated-types` avoids runtime checks for performance, users should not
construct invalid combinations such as `MultipleOf("non-numeric")` or `Annotated[int, Len(3)]`.
Downstream implementors may choose to raise an error, emit a warning, silently ignore
a metadata item, etc., if the metadata objects described below are used with an
incompatible type - or for any other reason!_
### Gt, Ge, Lt, Le
Express inclusive and/or exclusive bounds on orderable values - which may be numbers,
dates, times, strings, sets, etc. Note that the boundary value need not be of the
same type that was annotated, so long as they can be compared: `Annotated[int, Gt(1.5)]`
is fine, for example, and implies that the value is an integer x such that `x > 1.5`.
We suggest that implementors may also interpret `functools.partial(operator.le, 1.5)`
as being equivalent to `Gt(1.5)`, for users who wish to avoid a runtime dependency on
the `annotated-types` package.
To be explicit, these types have the following meanings:
* `Gt(x)` - value must be "Greater Than" `x` - equivalent to exclusive minimum
* `Ge(x)` - value must be "Greater than or Equal" to `x` - equivalent to inclusive minimum
* `Lt(x)` - value must be "Less Than" `x` - equivalent to exclusive maximum
* `Le(x)` - value must be "Less than or Equal" to `x` - equivalent to inclusive maximum
### Interval
`Interval(gt, ge, lt, le)` allows you to specify an upper and lower bound with a single
metadata object. `None` attributes should be ignored, and non-`None` attributes
treated as per the single bounds above.
### MultipleOf
`MultipleOf(multiple_of=x)` might be interpreted in two ways:
1. Python semantics, implying `value % multiple_of == 0`, or
2. [JSONschema semantics](https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.6.2.1),
where `int(value / multiple_of) == value / multiple_of`.
We encourage users to be aware of these two common interpretations and their
distinct behaviours, especially since very large or non-integer numbers make
it easy to cause silent data corruption due to floating-point imprecision.
We encourage libraries to carefully document which interpretation they implement.
### MinLen, MaxLen, Len
`Len()` implies that `min_length <= len(value) <= max_length` - lower and upper bounds are inclusive.
As well as `Len()` which can optionally include upper and lower bounds, we also
provide `MinLen(x)` and `MaxLen(y)` which are equivalent to `Len(min_length=x)`
and `Len(max_length=y)` respectively.
`Len`, `MinLen`, and `MaxLen` may be used with any type which supports `len(value)`.
Examples of usage:
* `Annotated[list, MaxLen(10)]` (or `Annotated[list, Len(max_length=10)]`) - list must have a length of 10 or less
* `Annotated[str, MaxLen(10)]` - string must have a length of 10 or less
* `Annotated[list, MinLen(3)]` (or `Annotated[list, Len(min_length=3)]`) - list must have a length of 3 or more
* `Annotated[list, Len(4, 6)]` - list must have a length of 4, 5, or 6
* `Annotated[list, Len(8, 8)]` - list must have a length of exactly 8
#### Changed in v0.4.0
* `min_inclusive` has been renamed to `min_length`, no change in meaning
* `max_exclusive` has been renamed to `max_length`, upper bound is now **inclusive** instead of **exclusive**
* The recommendation that slices are interpreted as `Len` has been removed due to ambiguity and different semantic
meaning of the upper bound in slices vs. `Len`
See [issue #23](https://github.com/annotated-types/annotated-types/issues/23) for discussion.
### Timezone
`Timezone` can be used with a `datetime` or a `time` to express which timezones
are allowed. `Annotated[datetime, Timezone(None)]` must be a naive datetime.
`Timezone[...]` ([literal ellipsis](https://docs.python.org/3/library/constants.html#Ellipsis))
expresses that any timezone-aware datetime is allowed. You may also pass a specific
timezone string or [`tzinfo`](https://docs.python.org/3/library/datetime.html#tzinfo-objects)
object such as `Timezone(timezone.utc)` or `Timezone("Africa/Abidjan")` to express that you only
allow a specific timezone, though we note that this is often a symptom of fragile design.
#### Changed in v0.x.x
* `Timezone` accepts [`tzinfo`](https://docs.python.org/3/library/datetime.html#tzinfo-objects) objects instead of
`timezone`, extending compatibility to [`zoneinfo`](https://docs.python.org/3/library/zoneinfo.html) and third party libraries.
### Unit
`Unit(unit: str)` expresses that the annotated numeric value is the magnitude of
a quantity with the specified unit. For example, `Annotated[float, Unit("m/s")]`
would be a float representing a velocity in meters per second.
Please note that `annotated_types` itself makes no attempt to parse or validate
the unit string in any way. That is left entirely to downstream libraries,
such as [`pint`](https://pint.readthedocs.io) or
[`astropy.units`](https://docs.astropy.org/en/stable/units/).
An example of how a library might use this metadata:
```python
from annotated_types import Unit
from typing import Annotated, TypeVar, Callable, Any, get_origin, get_args
# given a type annotated with a unit:
Meters = Annotated[float, Unit("m")]
# you can cast the annotation to a specific unit type with any
# callable that accepts a string and returns the desired type
T = TypeVar("T")
def cast_unit(tp: Any, unit_cls: Callable[[str], T]) -> T | None:
if get_origin(tp) is Annotated:
for arg in get_args(tp):
if isinstance(arg, Unit):
return unit_cls(arg.unit)
return None
# using `pint`
import pint
pint_unit = cast_unit(Meters, pint.Unit)
# using `astropy.units`
import astropy.units as u
astropy_unit = cast_unit(Meters, u.Unit)
```
### Predicate
`Predicate(func: Callable)` expresses that `func(value)` is truthy for valid values.
Users should prefer the statically inspectable metadata above, but if you need
the full power and flexibility of arbitrary runtime predicates... here it is.
For some common constraints, we provide generic types:
* `LowerCase = Annotated[T, Predicate(str.islower)]`
* `UpperCase = Annotated[T, Predicate(str.isupper)]`
* `IsDigit = Annotated[T, Predicate(str.isdigit)]`
* `IsFinite = Annotated[T, Predicate(math.isfinite)]`
* `IsNotFinite = Annotated[T, Predicate(Not(math.isfinite))]`
* `IsNan = Annotated[T, Predicate(math.isnan)]`
* `IsNotNan = Annotated[T, Predicate(Not(math.isnan))]`
* `IsInfinite = Annotated[T, Predicate(math.isinf)]`
* `IsNotInfinite = Annotated[T, Predicate(Not(math.isinf))]`
so that you can write e.g. `x: IsFinite[float] = 2.0` instead of the longer
(but exactly equivalent) `x: Annotated[float, Predicate(math.isfinite)] = 2.0`.
Some libraries might have special logic to handle known or understandable predicates,
for example by checking for `str.isdigit` and using its presence to both call custom
logic to enforce digit-only strings, and customise some generated external schema.
Users are therefore encouraged to avoid indirection like `lambda s: s.lower()`, in
favor of introspectable methods such as `str.lower` or `re.compile("pattern").search`.
To enable basic negation of commonly used predicates like `math.isnan` without introducing introspection that makes it impossible for implementers to introspect the predicate we provide a `Not` wrapper that simply negates the predicate in an introspectable manner. Several of the predicates listed above are created in this manner.
We do not specify what behaviour should be expected for predicates that raise
an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently
skip invalid constraints, or statically raise an error; or it might try calling it
and then propagate or discard the resulting
`TypeError: descriptor 'isdigit' for 'str' objects doesn't apply to a 'int' object`
exception. We encourage libraries to document the behaviour they choose.
### Doc
`doc()` can be used to add documentation information in `Annotated`, for function and method parameters, variables, class attributes, return types, and any place where `Annotated` can be used.
It expects a value that can be statically analyzed, as the main use case is for static analysis, editors, documentation generators, and similar tools.
It returns a `DocInfo` class with a single attribute `documentation` containing the value passed to `doc()`.
This is the early adopter's alternative form of the [`typing-doc` proposal](https://github.com/tiangolo/fastapi/blob/typing-doc/typing_doc.md).
### Integrating downstream types with `GroupedMetadata`
Implementers may choose to provide a convenience wrapper that groups multiple pieces of metadata.
This can help reduce verbosity and cognitive overhead for users.
For example, an implementer like Pydantic might provide a `Field` or `Meta` type that accepts keyword arguments and transforms these into low-level metadata:
```python
from dataclasses import dataclass
from typing import Iterator
from annotated_types import GroupedMetadata, Ge
@dataclass
class Field(GroupedMetadata):
ge: int | None = None
description: str | None = None
def __iter__(self) -> Iterator[object]:
# Iterating over a GroupedMetadata object should yield annotated-types
# constraint metadata objects which describe it as fully as possible,
# and may include other unknown objects too.
if self.ge is not None:
yield Ge(self.ge)
if self.description is not None:
yield Description(self.description)
```
Libraries consuming annotated-types constraints should check for `GroupedMetadata` and unpack it by iterating over the object and treating the results as if they had been "unpacked" in the `Annotated` type. The same logic should be applied to the [PEP 646 `Unpack` type](https://peps.python.org/pep-0646/), so that `Annotated[T, Field(...)]`, `Annotated[T, Unpack[Field(...)]]` and `Annotated[T, *Field(...)]` are all treated consistently.
Libraries consuming annotated-types should also ignore any metadata they do not recongize that came from unpacking a `GroupedMetadata`, just like they ignore unrecognized metadata in `Annotated` itself.
Our own `annotated_types.Interval` class is a `GroupedMetadata` which unpacks itself into `Gt`, `Lt`, etc., so this is not an abstract concern. Similarly, `annotated_types.Len` is a `GroupedMetadata` which unpacks itself into `MinLen` (optionally) and `MaxLen`.
### Consuming metadata
We intend to not be prescriptive as to _how_ the metadata and constraints are used, but as an example of how one might parse constraints from types annotations see our [implementation in `test_main.py`](https://github.com/annotated-types/annotated-types/blob/f59cf6d1b5255a0fe359b93896759a180bec30ae/tests/test_main.py#L94-L103).
It is up to the implementer to determine how this metadata is used.
You could use the metadata for runtime type checking, for generating schemas or to generate example data, amongst other use cases.
## Design & History
This package was designed at the PyCon 2022 sprints by the maintainers of Pydantic
and Hypothesis, with the goal of making it as easy as possible for end-users to
provide more informative annotations for use by runtime libraries.
It is deliberately minimal, and following PEP-593 allows considerable downstream
discretion in what (if anything!) they choose to support. Nonetheless, we expect
that staying simple and covering _only_ the most common use-cases will give users
and maintainers the best experience we can. If you'd like more constraints for your
types - follow our lead, by defining them and documenting them downstream!

View file

@ -0,0 +1,10 @@
annotated_types-0.8.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
annotated_types-0.8.0.dist-info/METADATA,sha256=YUmFsnj2Abjvhj-CeDuZHMOtpLifWS63HHxIxfLvIpg,15009
annotated_types-0.8.0.dist-info/RECORD,,
annotated_types-0.8.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
annotated_types-0.8.0.dist-info/licenses/LICENSE,sha256=_hBJiEsaDZNCkB6I4H8ykl0ksxIdmXK2poBfuYJLCV0,1083
annotated_types/__init__.py,sha256=pxBKTUObJ6n3T8C-I2ubobeDHmBEAmgCogWrwSmKm8g,13273
annotated_types/__pycache__/__init__.cpython-312.pyc,,
annotated_types/__pycache__/test_cases.cpython-312.pyc,,
annotated_types/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
annotated_types/test_cases.py,sha256=2GdHKstXuBzpf3iXKCjmvFZpyk2zP9Hm9kXGGDhGBo4,6310

View file

@ -0,0 +1,4 @@
Wheel-Version: 1.0
Generator: hatchling 1.31.0
Root-Is-Purelib: true
Tag: py3-none-any

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2022 the contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,416 @@
import math
import types
from collections.abc import Callable, Iterator
from dataclasses import dataclass
from datetime import tzinfo
from types import EllipsisType
from typing import (
TYPE_CHECKING,
Annotated,
Any,
Literal,
Protocol,
SupportsFloat,
SupportsIndex,
TypeVar,
Union,
runtime_checkable,
)
__all__ = (
'BaseMetadata',
'GroupedMetadata',
'Gt',
'Ge',
'Lt',
'Le',
'Interval',
'MultipleOf',
'MinLen',
'MaxLen',
'Len',
'Timezone',
'Predicate',
'LowerCase',
'UpperCase',
'IsDigits',
'IsFinite',
'IsNotFinite',
'IsNan',
'IsNotNan',
'IsInfinite',
'IsNotInfinite',
'doc',
'DocInfo',
'__version__',
)
__version__ = '0.8.0'
T = TypeVar('T')
# arguments that start with __ are considered
# positional only
# see https://peps.python.org/pep-0484/#positional-only-arguments
class SupportsGt(Protocol):
def __gt__(self: T, __other: T) -> bool:
...
class SupportsGe(Protocol):
def __ge__(self: T, __other: T) -> bool:
...
class SupportsLt(Protocol):
def __lt__(self: T, __other: T) -> bool:
...
class SupportsLe(Protocol):
def __le__(self: T, __other: T) -> bool:
...
class SupportsMod(Protocol):
def __mod__(self: T, __other: T) -> T:
...
class SupportsDiv(Protocol):
def __div__(self: T, __other: T) -> T:
...
class BaseMetadata:
"""Base class for all metadata.
This exists mainly so that implementers
can do `isinstance(..., BaseMetadata)` while traversing field annotations.
"""
__slots__ = ()
@dataclass(frozen=True, slots=True)
class Gt(BaseMetadata):
"""Gt(gt=x) implies that the value must be greater than x.
It can be used with any type that supports the ``>`` operator,
including numbers, dates and times, strings, sets, and so on.
"""
gt: SupportsGt
@dataclass(frozen=True, slots=True)
class Ge(BaseMetadata):
"""Ge(ge=x) implies that the value must be greater than or equal to x.
It can be used with any type that supports the ``>=`` operator,
including numbers, dates and times, strings, sets, and so on.
"""
ge: SupportsGe
@dataclass(frozen=True, slots=True)
class Lt(BaseMetadata):
"""Lt(lt=x) implies that the value must be less than x.
It can be used with any type that supports the ``<`` operator,
including numbers, dates and times, strings, sets, and so on.
"""
lt: SupportsLt
@dataclass(frozen=True, slots=True)
class Le(BaseMetadata):
"""Le(le=x) implies that the value must be less than or equal to x.
It can be used with any type that supports the ``<=`` operator,
including numbers, dates and times, strings, sets, and so on.
"""
le: SupportsLe
@runtime_checkable
class GroupedMetadata(Protocol):
"""A grouping of multiple objects, like typing.Unpack.
`GroupedMetadata` on its own is not metadata and has no meaning.
All of the constraints and metadata should be fully expressable
in terms of the `BaseMetadata`'s returned by `GroupedMetadata.__iter__()`.
Concrete implementations should override `GroupedMetadata.__iter__()`
to add their own metadata.
For example:
>>> @dataclass
>>> class Field(GroupedMetadata):
>>> gt: float | None = None
>>> description: str | None = None
...
>>> def __iter__(self) -> Iterable[object]:
>>> if self.gt is not None:
>>> yield Gt(self.gt)
>>> if self.description is not None:
>>> yield Description(self.gt)
Also see the implementation of `Interval` below for an example.
Parsers should recognize this and unpack it so that it can be used
both with and without unpacking:
- `Annotated[int, Field(...)]` (parser must unpack Field)
- `Annotated[int, *Field(...)]` (PEP-646)
""" # noqa: trailing-whitespace
@property
def __is_annotated_types_grouped_metadata__(self) -> Literal[True]:
return True
def __iter__(self) -> Iterator[object]:
...
if not TYPE_CHECKING:
__slots__ = () # allow subclasses to use slots
def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None:
# Basic ABC like functionality without the complexity of an ABC
super().__init_subclass__(*args, **kwargs)
if cls.__iter__ is GroupedMetadata.__iter__:
raise TypeError("Can't subclass GroupedMetadata without implementing __iter__")
def __iter__(self) -> Iterator[object]: # noqa: F811
raise NotImplementedError # more helpful than "None has no attribute..." type errors
@dataclass(frozen=True, kw_only=True, slots=True)
class Interval(GroupedMetadata):
"""Interval can express inclusive or exclusive bounds with a single object.
It accepts keyword arguments ``gt``, ``ge``, ``lt``, and/or ``le``, which
are interpreted the same way as the single-bound constraints.
"""
gt: SupportsGt | None = None
ge: SupportsGe | None = None
lt: SupportsLt | None = None
le: SupportsLe | None = None
def __iter__(self) -> Iterator[BaseMetadata]:
"""Unpack an Interval into zero or more single-bounds."""
if self.gt is not None:
yield Gt(self.gt)
if self.ge is not None:
yield Ge(self.ge)
if self.lt is not None:
yield Lt(self.lt)
if self.le is not None:
yield Le(self.le)
@dataclass(frozen=True, slots=True)
class MultipleOf(BaseMetadata):
"""MultipleOf(multiple_of=x) might be interpreted in two ways:
1. Python semantics, implying ``value % multiple_of == 0``, or
2. JSONschema semantics, where ``int(value / multiple_of) == value / multiple_of``
We encourage users to be aware of these two common interpretations,
and libraries to carefully document which they implement.
"""
multiple_of: SupportsDiv | SupportsMod
@dataclass(frozen=True, slots=True)
class MinLen(BaseMetadata):
"""
MinLen() implies minimum inclusive length,
e.g. ``len(value) >= min_length``.
"""
min_length: Annotated[int, Ge(0)]
@dataclass(frozen=True, slots=True)
class MaxLen(BaseMetadata):
"""
MaxLen() implies maximum inclusive length,
e.g. ``len(value) <= max_length``.
"""
max_length: Annotated[int, Ge(0)]
@dataclass(frozen=True, slots=True)
class Len(GroupedMetadata):
"""
Len() implies that ``min_length <= len(value) <= max_length``.
Upper bound may be omitted or ``None`` to indicate no upper length bound.
"""
min_length: Annotated[int, Ge(0)] = 0
max_length: Annotated[int, Ge(0)] | None = None
def __iter__(self) -> Iterator[BaseMetadata]:
"""Unpack a Len into zero or more single-bounds."""
if self.min_length > 0:
yield MinLen(self.min_length)
if self.max_length is not None:
yield MaxLen(self.max_length)
@dataclass(frozen=True, slots=True)
class Timezone(BaseMetadata):
"""Timezone(tz=...) requires a datetime to be aware (or ``tz=None``, naive).
``Annotated[datetime, Timezone(None)]`` must be a naive datetime.
``Timezone(...)`` (the ellipsis literal) expresses that the datetime must be
tz-aware but any timezone is allowed.
You may also pass a specific timezone string or tzinfo object such as
``Timezone(timezone.utc)`` or ``Timezone("Africa/Abidjan")`` to express that
you only allow a specific timezone, though we note that this is often
a symptom of poor design.
"""
tz: str | tzinfo | EllipsisType | None
@dataclass(frozen=True, slots=True)
class Unit(BaseMetadata):
"""Indicates that the value is a physical quantity with the specified unit.
It is intended for usage with numeric types, where the value represents the
magnitude of the quantity. For example, ``distance: Annotated[float, Unit('m')]``
or ``speed: Annotated[float, Unit('m/s')]``.
Interpretation of the unit string is left to the discretion of the consumer.
It is suggested to follow conventions established by python libraries that work
with physical quantities, such as
- ``pint`` : <https://pint.readthedocs.io/en/stable/>
- ``astropy.units``: <https://docs.astropy.org/en/stable/units/>
For indicating a quantity with a certain dimensionality but without a specific unit
it is recommended to use square brackets, e.g. `Annotated[float, Unit('[time]')]`.
Note, however, ``annotated_types`` itself makes no use of the unit string.
"""
unit: str
@dataclass(frozen=True, slots=True)
class Predicate(BaseMetadata):
"""``Predicate(func: Callable)`` implies `func(value)` is truthy for valid values.
Users should prefer statically inspectable metadata, but if you need the full
power and flexibility of arbitrary runtime predicates... here it is.
We provide a few predefined predicates for common string constraints:
``LowerCase = Predicate(str.islower)``, ``UpperCase = Predicate(str.isupper)``, and
``IsDigits = Predicate(str.isdigit)``. Users are encouraged to use methods which
can be given special handling, and avoid indirection like ``lambda s: s.lower()``.
Some libraries might have special logic to handle certain predicates, e.g. by
checking for `str.isdigit` and using its presence to both call custom logic to
enforce digit-only strings, and customise some generated external schema.
We do not specify what behaviour should be expected for predicates that raise
an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently
skip invalid constraints, or statically raise an error; or it might try calling it
and then propagate or discard the resulting exception.
"""
func: Callable[[Any], bool]
def __repr__(self) -> str:
if getattr(self.func, "__name__", "<lambda>") == "<lambda>":
return f"{self.__class__.__name__}({self.func!r})"
if isinstance(self.func, (types.MethodType, types.BuiltinMethodType)) and (
namespace := getattr(self.func.__self__, "__name__", None)
):
return f"{self.__class__.__name__}({namespace}.{self.func.__name__})"
if isinstance(self.func, type(str.isascii)): # method descriptor
return f"{self.__class__.__name__}({self.func.__qualname__})"
return f"{self.__class__.__name__}({self.func.__name__})"
@dataclass
class Not:
func: Callable[[Any], bool]
def __call__(self, __v: Any) -> bool:
return not self.func(__v)
_StrType = TypeVar("_StrType", bound=str)
LowerCase = Annotated[_StrType, Predicate(str.islower)]
"""
Return True if the string is a lowercase string, False otherwise.
A string is lowercase if all cased characters in the string are lowercase and there is at least one cased character in the string.
""" # noqa: E501
UpperCase = Annotated[_StrType, Predicate(str.isupper)]
"""
Return True if the string is an uppercase string, False otherwise.
A string is uppercase if all cased characters in the string are uppercase and there is at least one cased character in the string.
""" # noqa: E501
IsDigit = Annotated[_StrType, Predicate(str.isdigit)]
IsDigits = IsDigit # type: ignore # plural for backwards compatibility, see #63
"""
Return True if the string is a digit string, False otherwise.
A string is a digit string if all characters in the string are digits and there is at least one character in the string.
""" # noqa: E501
IsAscii = Annotated[_StrType, Predicate(str.isascii)]
"""
Return True if all characters in the string are ASCII, False otherwise.
ASCII characters have code points in the range U+0000-U+007F. Empty string is ASCII too.
"""
_NumericType = TypeVar('_NumericType', bound=Union[SupportsFloat, SupportsIndex])
IsFinite = Annotated[_NumericType, Predicate(math.isfinite)]
"""Return True if x is neither an infinity nor a NaN, and False otherwise."""
IsNotFinite = Annotated[_NumericType, Predicate(Not(math.isfinite))]
"""Return True if x is one of infinity or NaN, and False otherwise"""
IsNan = Annotated[_NumericType, Predicate(math.isnan)]
"""Return True if x is a NaN (not a number), and False otherwise."""
IsNotNan = Annotated[_NumericType, Predicate(Not(math.isnan))]
"""Return True if x is anything but NaN (not a number), and False otherwise."""
IsInfinite = Annotated[_NumericType, Predicate(math.isinf)]
"""Return True if x is a positive or negative infinity, and False otherwise."""
IsNotInfinite = Annotated[_NumericType, Predicate(Not(math.isinf))]
"""Return True if x is neither a positive or negative infinity, and False otherwise."""
try:
# PEP 727 – Documentation in Annotated Metadata
from typing_extensions import Doc # type: ignore[attr-defined]
except ImportError:
@dataclass(frozen=True, slots=True)
class Doc: # type: ignore [no-redef]
""" "
The return value of doc(), mainly to be used by tools that want to extract the
Annotated documentation at runtime.
"""
documentation: str
"""The documentation string passed to doc()."""
DocInfo = Doc # backwards compatibility
doc = Doc

View file

@ -0,0 +1,146 @@
import math
from collections.abc import Iterable, Iterator
from datetime import date, datetime, timedelta, timezone
from decimal import Decimal
from typing import Annotated, Any, NamedTuple
import annotated_types as at
class Case(NamedTuple):
"""
A test case for `annotated_types`.
"""
annotation: Any
valid_cases: Iterable[Any]
invalid_cases: Iterable[Any]
def cases() -> Iterable[Case]:
# Gt, Ge, Lt, Le
yield Case(Annotated[int, at.Gt(4)], (5, 6, 1000), (4, 0, -1))
yield Case(Annotated[float, at.Gt(0.5)], (0.6, 0.7, 0.8, 0.9), (0.5, 0.0, -0.1))
yield Case(
Annotated[datetime, at.Gt(datetime(2000, 1, 1))],
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
[datetime(2000, 1, 1), datetime(1999, 12, 31)],
)
yield Case(
Annotated[datetime, at.Gt(date(2000, 1, 1))],
[date(2000, 1, 2), date(2000, 1, 3)],
[date(2000, 1, 1), date(1999, 12, 31)],
)
yield Case(
Annotated[datetime, at.Gt(Decimal('1.123'))],
[Decimal('1.1231'), Decimal('123')],
[Decimal('1.123'), Decimal('0')],
)
yield Case(Annotated[int, at.Ge(4)], (4, 5, 6, 1000, 4), (0, -1))
yield Case(Annotated[float, at.Ge(0.5)], (0.5, 0.6, 0.7, 0.8, 0.9), (0.4, 0.0, -0.1))
yield Case(
Annotated[datetime, at.Ge(datetime(2000, 1, 1))],
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
[datetime(1998, 1, 1), datetime(1999, 12, 31)],
)
yield Case(Annotated[int, at.Lt(4)], (0, -1), (4, 5, 6, 1000, 4))
yield Case(Annotated[float, at.Lt(0.5)], (0.4, 0.0, -0.1), (0.5, 0.6, 0.7, 0.8, 0.9))
yield Case(
Annotated[datetime, at.Lt(datetime(2000, 1, 1))],
[datetime(1999, 12, 31), datetime(1999, 12, 31)],
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
)
yield Case(Annotated[int, at.Le(4)], (4, 0, -1), (5, 6, 1000))
yield Case(Annotated[float, at.Le(0.5)], (0.5, 0.0, -0.1), (0.6, 0.7, 0.8, 0.9))
yield Case(
Annotated[datetime, at.Le(datetime(2000, 1, 1))],
[datetime(2000, 1, 1), datetime(1999, 12, 31)],
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
)
# Interval
yield Case(Annotated[int, at.Interval(gt=4)], (5, 6, 1000), (4, 0, -1))
yield Case(Annotated[int, at.Interval(gt=4, lt=10)], (5, 6), (4, 10, 1000, 0, -1))
yield Case(Annotated[float, at.Interval(ge=0.5, le=1)], (0.5, 0.9, 1), (0.49, 1.1))
yield Case(
Annotated[datetime, at.Interval(gt=datetime(2000, 1, 1), le=datetime(2000, 1, 3))],
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
[datetime(2000, 1, 1), datetime(2000, 1, 4)],
)
yield Case(Annotated[int, at.MultipleOf(multiple_of=3)], (0, 3, 9), (1, 2, 4))
yield Case(Annotated[float, at.MultipleOf(multiple_of=0.5)], (0, 0.5, 1, 1.5), (0.4, 1.1))
# lengths
yield Case(Annotated[str, at.MinLen(3)], ('123', '1234', 'x' * 10), ('', '1', '12'))
yield Case(Annotated[str, at.Len(3)], ('123', '1234', 'x' * 10), ('', '1', '12'))
yield Case(Annotated[list[int], at.MinLen(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2]))
yield Case(Annotated[list[int], at.Len(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2]))
yield Case(Annotated[str, at.MaxLen(4)], ('', '1234'), ('12345', 'x' * 10))
yield Case(Annotated[str, at.Len(0, 4)], ('', '1234'), ('12345', 'x' * 10))
yield Case(Annotated[list[str], at.MaxLen(4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10))
yield Case(Annotated[list[str], at.Len(0, 4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10))
yield Case(Annotated[str, at.Len(3, 5)], ('123', '12345'), ('', '1', '12', '123456', 'x' * 10))
yield Case(Annotated[str, at.Len(3, 3)], ('123',), ('12', '1234'))
yield Case(Annotated[dict[int, int], at.Len(2, 3)], [{1: 1, 2: 2}], [{}, {1: 1}, {1: 1, 2: 2, 3: 3, 4: 4}])
yield Case(Annotated[set[int], at.Len(2, 3)], ({1, 2}, {1, 2, 3}), (set(), {1}, {1, 2, 3, 4}))
yield Case(Annotated[tuple[int, ...], at.Len(2, 3)], ((1, 2), (1, 2, 3)), ((), (1,), (1, 2, 3, 4)))
# Timezone
yield Case(
Annotated[datetime, at.Timezone(None)], [datetime(2000, 1, 1)], [datetime(2000, 1, 1, tzinfo=timezone.utc)]
)
yield Case(
Annotated[datetime, at.Timezone(...)], [datetime(2000, 1, 1, tzinfo=timezone.utc)], [datetime(2000, 1, 1)]
)
yield Case(
Annotated[datetime, at.Timezone(timezone.utc)],
[datetime(2000, 1, 1, tzinfo=timezone.utc)],
[datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))],
)
yield Case(
Annotated[datetime, at.Timezone('Europe/London')],
[datetime(2000, 1, 1, tzinfo=timezone(timedelta(0), name='Europe/London'))],
[datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))],
)
# Quantity
yield Case(Annotated[float, at.Unit(unit='m')], (5, 4.2), ('5m', '4.2m'))
# predicate types
yield Case(at.LowerCase[str], ['abc', 'foobar'], ['', 'A', 'Boom'])
yield Case(at.UpperCase[str], ['ABC', 'DEFO'], ['', 'a', 'abc', 'AbC'])
yield Case(at.IsDigit[str], ['123'], ['', 'ab', 'a1b2'])
yield Case(at.IsAscii[str], ['123', 'foo bar'], ['£100', '😊', 'whatever 👀'])
yield Case(Annotated[int, at.Predicate(lambda x: x % 2 == 0)], [0, 2, 4], [1, 3, 5])
yield Case(at.IsFinite[float], [1.23], [math.nan, math.inf, -math.inf])
yield Case(at.IsNotFinite[float], [math.nan, math.inf], [1.23])
yield Case(at.IsNan[float], [math.nan], [1.23, math.inf])
yield Case(at.IsNotNan[float], [1.23, math.inf], [math.nan])
yield Case(at.IsInfinite[float], [math.inf], [math.nan, 1.23])
yield Case(at.IsNotInfinite[float], [math.nan, 1.23], [math.inf])
# check stacked predicates
yield Case(at.IsInfinite[Annotated[float, at.Predicate(lambda x: x > 0)]], [math.inf], [-math.inf, 1.23, math.nan])
# doc
yield Case(Annotated[int, at.doc("A number")], [1, 2], [])
# custom GroupedMetadata
class MyCustomGroupedMetadata(at.GroupedMetadata):
def __iter__(self) -> Iterator[at.Predicate]:
yield at.Predicate(lambda x: float(x).is_integer())
yield Case(Annotated[float, MyCustomGroupedMetadata()], [0, 2.0], [0.01, 1.5])

View file

@ -0,0 +1 @@
pip

View file

@ -0,0 +1,108 @@
Metadata-Version: 2.4
Name: anyio
Version: 4.15.1
Summary: High-level concurrency and networking framework on top of asyncio or Trio
Author-email: Alex Grönholm <alex.gronholm@nextday.fi>
License-Expression: MIT
Project-URL: Documentation, https://anyio.readthedocs.io/en/latest/
Project-URL: Changelog, https://anyio.readthedocs.io/en/stable/versionhistory.html
Project-URL: Source code, https://github.com/agronholm/anyio
Project-URL: Issue tracker, https://github.com/agronholm/anyio/issues
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Framework :: AnyIO
Classifier: Typing :: Typed
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: 3.15
Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
Requires-Python: >=3.10
Description-Content-Type: text/x-rst
License-File: LICENSE
Requires-Dist: exceptiongroup>=1.0.2; python_version < "3.11"
Requires-Dist: idna>=2.8
Requires-Dist: typing_extensions>=4.16.0; python_version < "3.15"
Provides-Extra: trio
Requires-Dist: trio>=0.32.0; extra == "trio"
Dynamic: license-file
.. image:: https://github.com/agronholm/anyio/actions/workflows/test.yml/badge.svg
:target: https://github.com/agronholm/anyio/actions/workflows/test.yml
:alt: Build Status
.. image:: https://coveralls.io/repos/github/agronholm/anyio/badge.svg?branch=master
:target: https://coveralls.io/github/agronholm/anyio?branch=master
:alt: Code Coverage
.. image:: https://readthedocs.org/projects/anyio/badge/?version=latest
:target: https://anyio.readthedocs.io/en/latest/?badge=latest
:alt: Documentation
.. image:: https://badges.gitter.im/gitterHQ/gitter.svg
:target: https://gitter.im/python-trio/AnyIO
:alt: Gitter chat
.. image:: https://tidelift.com/badges/package/pypi/anyio
:target: https://tidelift.com/subscription/pkg/pypi-anyio
:alt: Tidelift
AnyIO is an asynchronous networking and concurrency library that works on top of either asyncio_ or
Trio_. It implements Trio-like `structured concurrency`_ (SC) on top of asyncio and works in harmony
with the native SC of Trio itself.
Applications and libraries written against AnyIO's API will run unmodified on either asyncio_ or
Trio_. AnyIO can also be adopted into a library or application incrementally – bit by bit, no full
refactoring necessary. It will blend in with the native libraries of your chosen backend.
To find out why you might want to use AnyIO's APIs instead of asyncio's, you can read about it
`here <https://anyio.readthedocs.io/en/stable/why.html>`_.
Documentation
-------------
View full documentation at: https://anyio.readthedocs.io/
Features
--------
AnyIO offers the following functionality:
* Task groups (nurseries_ in trio terminology)
* High-level networking (TCP, UDP and UNIX sockets)
* `Happy eyeballs`_ algorithm for TCP connections (more robust than that of asyncio on Python
3.8)
* async/await style UDP sockets (unlike asyncio where you still have to use Transports and
Protocols)
* A versatile API for byte streams and object streams
* Inter-task synchronization and communication (locks, conditions, events, semaphores, object
streams and futures)
* Worker threads
* Subprocesses
* Subinterpreter support for code parallelization (on Python 3.13 and later)
* Asynchronous file I/O (using worker threads)
* Signal handling
* Asynchronous versions of the functools_ and itertools_ modules
AnyIO also comes with its own pytest_ plugin which also supports asynchronous fixtures.
It even works with the popular Hypothesis_ library.
.. _asyncio: https://docs.python.org/3/library/asyncio.html
.. _Trio: https://github.com/python-trio/trio
.. _structured concurrency: https://en.wikipedia.org/wiki/Structured_concurrency
.. _nurseries: https://trio.readthedocs.io/en/stable/reference-core.html#nurseries-and-spawning
.. _Happy eyeballs: https://en.wikipedia.org/wiki/Happy_Eyeballs
.. _pytest: https://docs.pytest.org/en/latest/
.. _functools: https://docs.python.org/3/library/functools.html
.. _itertools: https://docs.python.org/3/library/itertools.html
.. _Hypothesis: https://hypothesis.works/
Security contact information
----------------------------
To report a security vulnerability, please use the `Tidelift security contact`_.
Tidelift will coordinate the fix and disclosure.
.. _Tidelift security contact: https://tidelift.com/security

View file

@ -0,0 +1,100 @@
anyio-4.15.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
anyio-4.15.1.dist-info/METADATA,sha256=BDrstXGvcYnopsUo9djjJUXxss36r3YdUebVtn6jyoE,4733
anyio-4.15.1.dist-info/RECORD,,
anyio-4.15.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
anyio-4.15.1.dist-info/entry_points.txt,sha256=_d6Yu6uiaZmNe0CydowirE9Cmg7zUL2g08tQpoS3Qvc,39
anyio-4.15.1.dist-info/licenses/LICENSE,sha256=U2GsncWPLvX9LpsJxoKXwX8ElQkJu8gCO9uC6s8iwrA,1081
anyio-4.15.1.dist-info/top_level.txt,sha256=QglSMiWX8_5dpoVAEIHdEYzvqFMdSYWmCj6tYw2ITkQ,6
anyio/__init__.py,sha256=OfroYRjIUw_JFj0Zwh1JAaajMQg4VqtoAt0XoOHDRwE,7602
anyio/__pycache__/__init__.cpython-312.pyc,,
anyio/__pycache__/_lazyimport.cpython-312.pyc,,
anyio/__pycache__/from_thread.cpython-312.pyc,,
anyio/__pycache__/functools.cpython-312.pyc,,
anyio/__pycache__/itertools.cpython-312.pyc,,
anyio/__pycache__/lowlevel.cpython-312.pyc,,
anyio/__pycache__/pytest_plugin.cpython-312.pyc,,
anyio/__pycache__/to_interpreter.cpython-312.pyc,,
anyio/__pycache__/to_process.cpython-312.pyc,,
anyio/__pycache__/to_thread.cpython-312.pyc,,
anyio/_backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
anyio/_backends/__pycache__/__init__.cpython-312.pyc,,
anyio/_backends/__pycache__/_asyncio.cpython-312.pyc,,
anyio/_backends/__pycache__/_trio.cpython-312.pyc,,
anyio/_backends/_asyncio.py,sha256=Vqy7O1ymmEpxTiPzehSuBCVTziGALNV-BbsIRaKXsg4,107767
anyio/_backends/_trio.py,sha256=Gs8XzX0IeBvje8IEZtJmpNylKCUAChKv7I_YZi3wlmg,45715
anyio/_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
anyio/_core/__pycache__/__init__.cpython-312.pyc,,
anyio/_core/__pycache__/_asyncio_selector_thread.cpython-312.pyc,,
anyio/_core/__pycache__/_concurrency_utils.cpython-312.pyc,,
anyio/_core/__pycache__/_contextmanagers.cpython-312.pyc,,
anyio/_core/__pycache__/_eventloop.cpython-312.pyc,,
anyio/_core/__pycache__/_exceptions.cpython-312.pyc,,
anyio/_core/__pycache__/_fileio.cpython-312.pyc,,
anyio/_core/__pycache__/_futures.cpython-312.pyc,,
anyio/_core/__pycache__/_resources.cpython-312.pyc,,
anyio/_core/__pycache__/_signals.cpython-312.pyc,,
anyio/_core/__pycache__/_sockets.cpython-312.pyc,,
anyio/_core/__pycache__/_streams.cpython-312.pyc,,
anyio/_core/__pycache__/_subprocesses.cpython-312.pyc,,
anyio/_core/__pycache__/_synchronization.cpython-312.pyc,,
anyio/_core/__pycache__/_tasks.cpython-312.pyc,,
anyio/_core/__pycache__/_tempfile.cpython-312.pyc,,
anyio/_core/__pycache__/_testing.cpython-312.pyc,,
anyio/_core/__pycache__/_typedattr.cpython-312.pyc,,
anyio/_core/_asyncio_selector_thread.py,sha256=2PdxFM3cs02Kp6BSppbvmRT7q7asreTW5FgBxEsflBo,5626
anyio/_core/_concurrency_utils.py,sha256=kvRXMrnDqgQTBkZc-f279-lBnZM4YbSToXqhnAUCfSc,4522
anyio/_core/_contextmanagers.py,sha256=YInBCabiEeS-UaP_Jdxa1CaFC71ETPW8HZTHIM8Rsc8,7215
anyio/_core/_eventloop.py,sha256=ByZUeJD9alMfcyTseRo5IzTO0IltEul_Gyq9iqSjqDk,6658
anyio/_core/_exceptions.py,sha256=B4ZhLkBQT4tI6vShv4-GJui492IJsoIDTw1FQJEkmAI,5618
anyio/_core/_fileio.py,sha256=q--ft9h5Y_FRLzhAnoYrXf3tOuZ8ZyIN2A9mCfMbZRc,34163
anyio/_core/_futures.py,sha256=YQq5uhnCnKQMQD62tOLfzEm7dUXY3GQorJdH6ghfgIQ,6177
anyio/_core/_resources.py,sha256=Ld5duCe9NwgPfI7EIOh3CMIBYQ7rq6ZQEa7CFS0u9Ec,450
anyio/_core/_signals.py,sha256=mjTBB2hTKNPRlU0IhnijeQedpWOGERDiMjSlJQsFrug,1016
anyio/_core/_sockets.py,sha256=C8b-3w5H9n0Fo4g7X8WJvk0m6CGh6NVAAFtieBGHK3g,35289
anyio/_core/_streams.py,sha256=FczFwIgDpnkK0bODWJXMpsUJYdvAD04kaUaGzJU8DK0,1806
anyio/_core/_subprocesses.py,sha256=TMBuGNILz-47w5t9vayJ1GpCpDT6LxsMohJDFCewIRc,7938
anyio/_core/_synchronization.py,sha256=ggsd74QPd846faA2HQeT07ONQ5HQ1S78gy8mvB3qCrw,21658
anyio/_core/_tasks.py,sha256=J2dhM_Jq3Grsfg_enNWpRz2SxfM15uLt-pEfvoQq3uA,15391
anyio/_core/_tempfile.py,sha256=IgoBCFaPhrFvsF9KHcCcrnCRn-UqMfw0WRdSDWgxAiI,19726
anyio/_core/_testing.py,sha256=ZRucLh5kqhiFWAJMMbd8yZ6uIvz0rq3WwL72ldosxAU,2340
anyio/_core/_typedattr.py,sha256=gt9Dzn-IL0QW14aHPDD8PAtq95FYKNNupHyf37xZGPQ,2607
anyio/_lazyimport.py,sha256=LCjnYanIfYE9G-Dv_EsqNLDuVRppNicWZjCTfS9-sJU,6082
anyio/abc/__init__.py,sha256=XIVvCI2c6oyaOcqXe94k2rzU5_s7-ZNnlxGTk34H4T0,3050
anyio/abc/__pycache__/__init__.cpython-312.pyc,,
anyio/abc/__pycache__/_eventloop.cpython-312.pyc,,
anyio/abc/__pycache__/_resources.cpython-312.pyc,,
anyio/abc/__pycache__/_sockets.cpython-312.pyc,,
anyio/abc/__pycache__/_streams.cpython-312.pyc,,
anyio/abc/__pycache__/_subprocesses.cpython-312.pyc,,
anyio/abc/__pycache__/_tasks.cpython-312.pyc,,
anyio/abc/__pycache__/_testing.cpython-312.pyc,,
anyio/abc/_eventloop.py,sha256=0-UwgB351rkocCt8m3fjTmcNx2Fxn52nUoujG5DzMvU,11127
anyio/abc/_resources.py,sha256=hrb0LXUgHSp3i8G6W8JJchdzKXgg1m0T-L7udXmv2y0,900
anyio/abc/_sockets.py,sha256=jQWnPX7iDIDyvyvVkbuFhjezw4oI2WggzbW6Vl2-eqs,13162
anyio/abc/_streams.py,sha256=IMeOPPutFXRbyeAutxV1HJe7FUQZYp2qKrJJpP1DCv0,7620
anyio/abc/_subprocesses.py,sha256=CDqU1p0HuBS0pXUzBW_hbfhihTJRM67n__7qZmoPU84,2123
anyio/abc/_tasks.py,sha256=HBPpT72aOy0bwMAxBCKmnXsR328wCYRjYm3iaVw2fQo,7070
anyio/abc/_testing.py,sha256=A9Y8RAClIPn1A0aNHBN8YoaRy_MKOpbFGEbTHAXgB6U,2145
anyio/from_thread.py,sha256=iZZbfcNySi4wAcx1_cxAQE4J0YZC88qRMIYqQ6SF9sc,19415
anyio/functools.py,sha256=T4JS8IXq-x1S0Lbo2owF8l9fza2KypO147QLeyz4cjs,11797
anyio/itertools.py,sha256=TS7_UCa2Y3cggtaLlWHbjOyf6QhPfQUdPduNW-ddXTE,16255
anyio/lowlevel.py,sha256=QyWkvfetbCH0mHkqv1yxGwOaiZ7fyxhpZkHWOXPV5JU,6280
anyio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
anyio/pytest_plugin.py,sha256=oLXyOstvRZW3VVbqIODhZsPt4jSz2UGh44YeG2nJXx0,14516
anyio/streams/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
anyio/streams/__pycache__/__init__.cpython-312.pyc,,
anyio/streams/__pycache__/buffered.cpython-312.pyc,,
anyio/streams/__pycache__/file.cpython-312.pyc,,
anyio/streams/__pycache__/memory.cpython-312.pyc,,
anyio/streams/__pycache__/stapled.cpython-312.pyc,,
anyio/streams/__pycache__/text.cpython-312.pyc,,
anyio/streams/__pycache__/tls.cpython-312.pyc,,
anyio/streams/buffered.py,sha256=u7hCD8SNrYHcutG6K5wEiy1F88-pizgjvEFM22Kq2Cw,6746
anyio/streams/file.py,sha256=6jujI2m-QJITqqKFamrupX_DNsU7y2Fz3omLZxOLuY0,4524
anyio/streams/memory.py,sha256=rpxNzDMag9PCOynBqN64KyuAfAPB41vFyRP-Npo-lE0,10793
anyio/streams/stapled.py,sha256=UJ2Rgt0NUmM1VovRNOoZoic960lcbDjb3vhfZg3b_Z0,4845
anyio/streams/text.py,sha256=BcVAGJw1VRvtIqnv-o0Rb0pwH7p8vwlvl21xHq522ag,5765
anyio/streams/tls.py,sha256=fqLmxvULFO9rvxGyJ33VPrOIN7E3eiU1q90BkmKigQ8,15818
anyio/to_interpreter.py,sha256=ATnoT6kei9wDmQZtXTA3SsY4CK7XKr8Ta4_x5i6pbTw,7100
anyio/to_process.py,sha256=p6n8j0nreJUl1DEkR_9PL6KRoKtNqFxORTn8qZBvn94,9850
anyio/to_thread.py,sha256=8MPVrrQq8ttGvV21cSd68rMto5bhxJ0-i9CupXag5Gs,2750

View file

@ -0,0 +1,5 @@
Wheel-Version: 1.0
Generator: setuptools (84.0.0)
Root-Is-Purelib: true
Tag: py3-none-any

View file

@ -0,0 +1,2 @@
[pytest11]
anyio = anyio.pytest_plugin

View file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2018 Alex Grönholm
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,154 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from ._lazyimport import (
fix_package_names,
install_lazy_importer,
set_deprecated_aliases,
)
if TYPE_CHECKING or not install_lazy_importer():
from ._core._concurrency_utils import amap as amap
from ._core._concurrency_utils import as_completed as as_completed
from ._core._concurrency_utils import gather as gather
from ._core._contextmanagers import (
AsyncContextManagerMixin as AsyncContextManagerMixin,
)
from ._core._contextmanagers import ContextManagerMixin as ContextManagerMixin
from ._core._eventloop import current_time as current_time
from ._core._eventloop import get_all_backends as get_all_backends
from ._core._eventloop import get_available_backends as get_available_backends
from ._core._eventloop import get_cancelled_exc_class as get_cancelled_exc_class
from ._core._eventloop import run as run
from ._core._eventloop import sleep as sleep
from ._core._eventloop import sleep_forever as sleep_forever
from ._core._eventloop import sleep_until as sleep_until
from ._core._exceptions import BrokenResourceError as BrokenResourceError
from ._core._exceptions import BrokenWorkerInterpreter as BrokenWorkerInterpreter
from ._core._exceptions import BrokenWorkerProcess as BrokenWorkerProcess
from ._core._exceptions import BusyResourceError as BusyResourceError
from ._core._exceptions import ClosedResourceError as ClosedResourceError
from ._core._exceptions import ConnectionFailed as ConnectionFailed
from ._core._exceptions import DelimiterNotFound as DelimiterNotFound
from ._core._exceptions import EndOfStream as EndOfStream
from ._core._exceptions import FutureAlreadyFinished as FutureAlreadyFinished
from ._core._exceptions import FutureCancelled as FutureCancelled
from ._core._exceptions import FutureFailed as FutureFailed
from ._core._exceptions import FutureNotFinished as FutureNotFinished
from ._core._exceptions import IncompleteRead as IncompleteRead
from ._core._exceptions import NoEventLoopError as NoEventLoopError
from ._core._exceptions import RunFinishedError as RunFinishedError
from ._core._exceptions import TaskCancelled as TaskCancelled
from ._core._exceptions import TaskFailed as TaskFailed
from ._core._exceptions import TaskNotFinished as TaskNotFinished
from ._core._exceptions import (
TypedAttributeLookupError as TypedAttributeLookupError,
)
from ._core._exceptions import WouldBlock as WouldBlock
from ._core._fileio import AsyncFile as AsyncFile
from ._core._fileio import Path as Path
from ._core._fileio import open_file as open_file
from ._core._fileio import wrap_file as wrap_file
from ._core._futures import Future as Future
from ._core._resources import aclose_forcefully as aclose_forcefully
from ._core._signals import open_signal_receiver as open_signal_receiver
from ._core._sockets import TCPConnectable as TCPConnectable
from ._core._sockets import UNIXConnectable as UNIXConnectable
from ._core._sockets import as_connectable as as_connectable
from ._core._sockets import connect_tcp as connect_tcp
from ._core._sockets import connect_unix as connect_unix
from ._core._sockets import (
create_connected_udp_socket as create_connected_udp_socket,
)
from ._core._sockets import (
create_connected_unix_datagram_socket as create_connected_unix_datagram_socket,
)
from ._core._sockets import create_tcp_listener as create_tcp_listener
from ._core._sockets import create_udp_socket as create_udp_socket
from ._core._sockets import (
create_unix_datagram_socket as create_unix_datagram_socket,
)
from ._core._sockets import create_unix_listener as create_unix_listener
from ._core._sockets import getaddrinfo as getaddrinfo
from ._core._sockets import getnameinfo as getnameinfo
from ._core._sockets import notify_closing as notify_closing
from ._core._sockets import wait_readable as wait_readable
from ._core._sockets import wait_socket_readable as wait_socket_readable
from ._core._sockets import wait_socket_writable as wait_socket_writable
from ._core._sockets import wait_writable as wait_writable
from ._core._streams import (
create_memory_object_stream as create_memory_object_stream,
)
from ._core._subprocesses import open_process as open_process
from ._core._subprocesses import run_process as run_process
from ._core._synchronization import CapacityLimiter as CapacityLimiter
from ._core._synchronization import (
CapacityLimiterStatistics as CapacityLimiterStatistics,
)
from ._core._synchronization import Condition as Condition
from ._core._synchronization import ConditionStatistics as ConditionStatistics
from ._core._synchronization import Event as Event
from ._core._synchronization import EventStatistics as EventStatistics
from ._core._synchronization import Lock as Lock
from ._core._synchronization import LockStatistics as LockStatistics
from ._core._synchronization import ResourceGuard as ResourceGuard
from ._core._synchronization import Semaphore as Semaphore
from ._core._synchronization import SemaphoreStatistics as SemaphoreStatistics
from ._core._tasks import TASK_STATUS_IGNORED as TASK_STATUS_IGNORED
from ._core._tasks import CancelScope as CancelScope
from ._core._tasks import TaskHandle as TaskHandle
from ._core._tasks import create_task_group as create_task_group
from ._core._tasks import current_effective_deadline as current_effective_deadline
from ._core._tasks import fail_after as fail_after
from ._core._tasks import fail_at as fail_at
from ._core._tasks import move_on_after as move_on_after
from ._core._tasks import move_on_at as move_on_at
from ._core._tempfile import NamedTemporaryFile as NamedTemporaryFile
from ._core._tempfile import SpooledTemporaryFile as SpooledTemporaryFile
from ._core._tempfile import TemporaryDirectory as TemporaryDirectory
from ._core._tempfile import TemporaryFile as TemporaryFile
from ._core._tempfile import gettempdir as gettempdir
from ._core._tempfile import gettempdirb as gettempdirb
from ._core._tempfile import mkdtemp as mkdtemp
from ._core._tempfile import mkstemp as mkstemp
from ._core._testing import TaskInfo as TaskInfo
from ._core._testing import get_current_task as get_current_task
from ._core._testing import get_running_tasks as get_running_tasks
from ._core._testing import wait_all_tasks_blocked as wait_all_tasks_blocked
from ._core._typedattr import TypedAttributeProvider as TypedAttributeProvider
from ._core._typedattr import TypedAttributeSet as TypedAttributeSet
from ._core._typedattr import typed_attribute as typed_attribute
# ruff: isort: off
from . import (
abc as abc,
)
from . import (
from_thread as from_thread,
)
from . import (
functools as functools,
)
from . import (
itertools as itertools,
)
from . import (
lowlevel as lowlevel,
)
from . import (
to_interpreter as to_interpreter,
)
from . import (
to_process as to_process,
)
from . import (
to_thread as to_thread,
)
fix_package_names()
set_deprecated_aliases(
{
"BrokenWorkerIntepreter": "anyio.BrokenWorkerInterpreter",
}
)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more