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:
commit
b60439152b
10884 changed files with 1125375 additions and 0 deletions
BIN
__pycache__/cli.cpython-312.pyc
Normal file
BIN
__pycache__/cli.cpython-312.pyc
Normal file
Binary file not shown.
426
architecture.html
Normal file
426
architecture.html
Normal 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 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
71
cli.py
Normal 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
5
harness/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
"""Simbiot Harness — тактовая лестница на CQRS + Event Sourcing.
|
||||
|
||||
Уровни 1с/1м/1ч/1д (v0.1). Стартовая точка — strands-agents/harness-sdk.
|
||||
"""
|
||||
__version__ = "0.1.0"
|
||||
BIN
harness/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
harness/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
harness/__pycache__/_llm_worker.cpython-312.pyc
Normal file
BIN
harness/__pycache__/_llm_worker.cpython-312.pyc
Normal file
Binary file not shown.
BIN
harness/__pycache__/commands.cpython-312.pyc
Normal file
BIN
harness/__pycache__/commands.cpython-312.pyc
Normal file
Binary file not shown.
BIN
harness/__pycache__/es.cpython-312.pyc
Normal file
BIN
harness/__pycache__/es.cpython-312.pyc
Normal file
Binary file not shown.
BIN
harness/__pycache__/isolated_runner.cpython-312.pyc
Normal file
BIN
harness/__pycache__/isolated_runner.cpython-312.pyc
Normal file
Binary file not shown.
BIN
harness/__pycache__/levels.cpython-312.pyc
Normal file
BIN
harness/__pycache__/levels.cpython-312.pyc
Normal file
Binary file not shown.
BIN
harness/__pycache__/runtime.cpython-312.pyc
Normal file
BIN
harness/__pycache__/runtime.cpython-312.pyc
Normal file
Binary file not shown.
BIN
harness/__pycache__/scheduler.cpython-312.pyc
Normal file
BIN
harness/__pycache__/scheduler.cpython-312.pyc
Normal file
Binary file not shown.
68
harness/_llm_worker.py
Normal file
68
harness/_llm_worker.py
Normal 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
67
harness/commands.py
Normal 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
251
harness/es.py
Normal 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()
|
||||
70
harness/isolated_runner.py
Normal file
70
harness/isolated_runner.py
Normal 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
80
harness/levels.py
Normal 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
130
harness/runtime.py
Normal 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
236
harness/scheduler.py
Normal 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
247
venv/bin/Activate.ps1
Normal 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"
|
||||
BIN
venv/bin/__pycache__/jp.cpython-312.pyc
Normal file
BIN
venv/bin/__pycache__/jp.cpython-312.pyc
Normal file
Binary file not shown.
70
venv/bin/activate
Normal file
70
venv/bin/activate
Normal 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
27
venv/bin/activate.csh
Normal 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
69
venv/bin/activate.fish
Normal 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
6
venv/bin/cffi-gen-src
Executable 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
6
venv/bin/httpx
Executable 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
6
venv/bin/httpx2
Executable 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
6
venv/bin/idna
Executable 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
54
venv/bin/jp.py
Executable 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
6
venv/bin/jsonschema
Executable 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
6
venv/bin/mcp
Executable 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
BIN
venv/bin/monty
Executable file
Binary file not shown.
6
venv/bin/opentelemetry-bootstrap
Executable file
6
venv/bin/opentelemetry-bootstrap
Executable 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())
|
||||
6
venv/bin/opentelemetry-instrument
Executable file
6
venv/bin/opentelemetry-instrument
Executable 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
8
venv/bin/pip
Executable 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
8
venv/bin/pip3
Executable 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
8
venv/bin/pip3.12
Executable 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
BIN
venv/bin/py-spy
Executable file
Binary file not shown.
6
venv/bin/pydantic-monty
Executable file
6
venv/bin/pydantic-monty
Executable 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
1
venv/bin/python
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
python3.12
|
||||
1
venv/bin/python3
Symbolic link
1
venv/bin/python3
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
python3.12
|
||||
1
venv/bin/python3.12
Symbolic link
1
venv/bin/python3.12
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
/usr/bin/python3.12
|
||||
6
venv/bin/uvicorn
Executable file
6
venv/bin/uvicorn
Executable 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
6
venv/bin/watchmedo
Executable 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())
|
||||
Binary file not shown.
Binary file not shown.
BIN
venv/lib/python3.12/site-packages/_cffi_backend.cpython-312-x86_64-linux-gnu.so
Executable file
BIN
venv/lib/python3.12/site-packages/_cffi_backend.cpython-312-x86_64-linux-gnu.so
Executable file
Binary file not shown.
33
venv/lib/python3.12/site-packages/_yaml/__init__.py
Normal file
33
venv/lib/python3.12/site-packages/_yaml/__init__.py
Normal 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__ = ''
|
||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
pip
|
||||
|
|
@ -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
|
||||
|
||||
[](https://github.com/annotated-types/annotated-types/actions?query=event%3Apush+branch%3Amain+workflow%3ACI)
|
||||
[](https://pypi.python.org/pypi/annotated-types)
|
||||
[](https://github.com/annotated-types/annotated-types)
|
||||
[](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!
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
Wheel-Version: 1.0
|
||||
Generator: hatchling 1.31.0
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
|
@ -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.
|
||||
416
venv/lib/python3.12/site-packages/annotated_types/__init__.py
Normal file
416
venv/lib/python3.12/site-packages/annotated_types/__init__.py
Normal 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
|
||||
Binary file not shown.
Binary file not shown.
146
venv/lib/python3.12/site-packages/annotated_types/test_cases.py
Normal file
146
venv/lib/python3.12/site-packages/annotated_types/test_cases.py
Normal 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])
|
||||
|
|
@ -0,0 +1 @@
|
|||
pip
|
||||
|
|
@ -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
|
||||
100
venv/lib/python3.12/site-packages/anyio-4.15.1.dist-info/RECORD
Normal file
100
venv/lib/python3.12/site-packages/anyio-4.15.1.dist-info/RECORD
Normal 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
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
Wheel-Version: 1.0
|
||||
Generator: setuptools (84.0.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
[pytest11]
|
||||
anyio = anyio.pytest_plugin
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1 @@
|
|||
anyio
|
||||
154
venv/lib/python3.12/site-packages/anyio/__init__.py
Normal file
154
venv/lib/python3.12/site-packages/anyio/__init__.py
Normal 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",
|
||||
}
|
||||
)
|
||||
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
3201
venv/lib/python3.12/site-packages/anyio/_backends/_asyncio.py
Normal file
3201
venv/lib/python3.12/site-packages/anyio/_backends/_asyncio.py
Normal file
File diff suppressed because it is too large
Load diff
1473
venv/lib/python3.12/site-packages/anyio/_backends/_trio.py
Normal file
1473
venv/lib/python3.12/site-packages/anyio/_backends/_trio.py
Normal file
File diff suppressed because it is too large
Load diff
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.
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.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue