prompt-manager-client-dl 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- prompt_manager_client_dl-0.1.0/.gitignore +11 -0
- prompt_manager_client_dl-0.1.0/PKG-INFO +167 -0
- prompt_manager_client_dl-0.1.0/README.md +157 -0
- prompt_manager_client_dl-0.1.0/pyproject.toml +22 -0
- prompt_manager_client_dl-0.1.0/src/prompt_manager/__init__.py +12 -0
- prompt_manager_client_dl-0.1.0/src/prompt_manager/async_client.py +132 -0
- prompt_manager_client_dl-0.1.0/src/prompt_manager/cli.py +57 -0
- prompt_manager_client_dl-0.1.0/src/prompt_manager/client.py +172 -0
- prompt_manager_client_dl-0.1.0/src/prompt_manager/models.py +69 -0
- prompt_manager_client_dl-0.1.0/uv.lock +230 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: prompt-manager-client-dl
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Live-updating client for Prompt Manager
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: cachetools<7,>=5
|
|
7
|
+
Requires-Dist: httpx<1,>=0.27
|
|
8
|
+
Requires-Dist: requests<3,>=2.31
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# Prompt Manager — Cliente Python
|
|
12
|
+
|
|
13
|
+
`prompt-manager-client` é a biblioteca que as aplicações de IA importam para buscar seus prompts na API do Prompt Manager em tempo de execução. Os prompts se atualizam ao vivo: quando alguém commita e publica uma nova versão na UI, a aplicação passa a usá-la em segundos, sem redeploy.
|
|
14
|
+
|
|
15
|
+
Ela é deliberadamente pequena — três coisas públicas:
|
|
16
|
+
|
|
17
|
+
- `PromptManager` — cliente síncrono (`requests`)
|
|
18
|
+
- `AsyncPromptManager` — cliente assíncrono (`httpx`), mesma interface com `await`
|
|
19
|
+
- `Prompt` — o prompt imutável e totalmente resolvido que uma busca retorna
|
|
20
|
+
|
|
21
|
+
## Instalação
|
|
22
|
+
|
|
23
|
+
O pacote vive neste repositório, em `client/`. Adicione-o como dependência de path/git, por exemplo com uv:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv add "prompt-manager-client @ git+ssh://git@bitbucket.org/avisourgente/prompt-manager.git#subdirectory=client"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Requer Python ≥ 3.10. Dependências: `requests`, `httpx`, `cachetools`.
|
|
30
|
+
|
|
31
|
+
## Começo rápido
|
|
32
|
+
|
|
33
|
+
Crie **um cliente por processo** na inicialização (ele guarda o cache e a sessão HTTP) e reutilize-o:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from prompt_manager import PromptManager
|
|
37
|
+
|
|
38
|
+
pm = PromptManager(
|
|
39
|
+
application="superchat", # o nome da sua aplicação no manager
|
|
40
|
+
environment="prod", # qual deployment seguir
|
|
41
|
+
fallback_dir="prompts_fallback", # opcional: snapshots offline (veja abaixo)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Uma chamada: busca (com cache) + preenche variáveis + relata o uso
|
|
45
|
+
text = pm.render(
|
|
46
|
+
"resposta_juridica",
|
|
47
|
+
variables={"pergunta": pergunta, "contexto": contexto},
|
|
48
|
+
request_id=request_id, # campos opcionais de observabilidade —
|
|
49
|
+
user=user_email, # eles vinculam o caso real ao prompt
|
|
50
|
+
metadata={"tenant": tenant}, # na tela de "Registros" do manager
|
|
51
|
+
)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`render()` retorna a string final pronta para enviar ao modelo. Se o prompt define mensagens de chat, você provavelmente quer `get_prompt()` (abaixo).
|
|
55
|
+
|
|
56
|
+
### Onde o cliente se conecta (`base_url`)
|
|
57
|
+
|
|
58
|
+
O endereço da API é resolvido nesta ordem:
|
|
59
|
+
|
|
60
|
+
1. o argumento `base_url` do construtor, se informado;
|
|
61
|
+
2. a variável de ambiente **`PROMPT_MANAGER_URL`**;
|
|
62
|
+
3. o padrão de produção: **`http://promptmanager-prod.datalawyer.local`**.
|
|
63
|
+
|
|
64
|
+
Ou seja: em produção você não configura nada, e deployments de staging/dev redirecionam todos os clientes com uma única variável de ambiente:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
export PROMPT_MANAGER_URL=http://localhost:8000 # ex.: stack de dev local
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Async
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from prompt_manager import AsyncPromptManager
|
|
74
|
+
|
|
75
|
+
pm = AsyncPromptManager(application="superchat", environment="prod")
|
|
76
|
+
|
|
77
|
+
async def handle(question: str) -> str:
|
|
78
|
+
return await pm.render("resposta_juridica", variables={"pergunta": question})
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Use-o como async context manager (`async with AsyncPromptManager(...) as pm:`) ou mantenha-o pela vida do processo; ao sair, ele descarrega os relatos de uso pendentes e fecha seu cliente `httpx` (a menos que você tenha passado o seu próprio).
|
|
82
|
+
|
|
83
|
+
## Trabalhando com o objeto `Prompt`
|
|
84
|
+
|
|
85
|
+
`get_prompt()` retorna o prompt resolvido sem renderizá-lo:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
prompt = pm.get_prompt("resposta_juridica")
|
|
89
|
+
|
|
90
|
+
prompt.template # texto resolvido completo (fragmentos, controles e exemplos já expandidos)
|
|
91
|
+
prompt.variables # as variáveis de runtime que o template ainda espera
|
|
92
|
+
prompt.version # número da versão commitada que esta resolução usou
|
|
93
|
+
prompt.hash # hash do snapshot — registre-o para rastrear qualquer chamada de LLM até o prompt exato
|
|
94
|
+
prompt.suggested_model # modelo + parâmetros sugeridos pelo autor do prompt
|
|
95
|
+
prompt.suggested_params
|
|
96
|
+
prompt.fragment_versions # quais versões de fragmentos foram embutidas
|
|
97
|
+
prompt.fewshots # quais bancos de exemplos foram injetados, e quantos exemplos cada um contribuiu
|
|
98
|
+
|
|
99
|
+
rendered = prompt.render(pergunta="...", contexto="...")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`render` (tanto no cliente quanto no `Prompt`) valida os valores: nomes de variáveis desconhecidos, variáveis obrigatórias faltando e `{{placeholders}}` restantes levantam `PromptValidationError`. Valores dict/list são codificados em JSON automaticamente.
|
|
103
|
+
|
|
104
|
+
### Mensagens de chat
|
|
105
|
+
|
|
106
|
+
Um template resolvido pode conter linhas com marcadores de papel `{% system %}` / `{% user %}` / `{% assistant %}`. O cliente mantém o template plano (os marcadores sobrevivem intactos ao `render()`); para prompts com múltiplas mensagens, divida você mesmo o texto renderizado nessas linhas de marcador: uma nova mensagem começa em cada marcador, e o conteúdo antes do primeiro marcador pertence a `system`. A resposta do `/fetch` da API também carrega um array `messages` já dividido, se você preferir consumi-lo diretamente.
|
|
107
|
+
|
|
108
|
+
## Ambientes
|
|
109
|
+
|
|
110
|
+
O `environment` informado na construção é o padrão; qualquer chamada pode sobrescrevê-lo:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
pm.get_prompt("resposta_juridica", environment="staging")
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`"default"` é especial: ele sempre segue a **versão commitada mais recente**, sem precisar de deployment explícito. Ambientes nomeados (`prod`, `staging`, …) servem a versão que foi fixada neles pela UI — e caem para a versão mais recente se o prompt nunca foi fixado ali.
|
|
117
|
+
|
|
118
|
+
## Ablação de few-shots (A/B sem exemplos)
|
|
119
|
+
|
|
120
|
+
`fewshots=False` resolve o mesmo prompt com todos os bancos de exemplos desligados — é assim que uma aplicação mede o que os exemplos realmente valem:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
com_exemplos = pm.render("classificador", variables=v) # normal
|
|
124
|
+
sem_exemplos = pm.render("classificador", variables=v, fewshots=False) # ablação
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
As duas resoluções têm cache e ETag separados.
|
|
128
|
+
|
|
129
|
+
## Cache, resiliência e fallback offline
|
|
130
|
+
|
|
131
|
+
O cliente é construído para que a API de prompts nunca seja um ponto único de falha:
|
|
132
|
+
|
|
133
|
+
1. **Cache com TTL** (padrão 45 s, `ttl=` no construtor): buscas repetidas dentro da janela não custam nada.
|
|
134
|
+
2. **Revalidação por ETag**: expirado o TTL, o cliente revalida com `If-None-Match`; um prompt inalterado custa um 304, não um novo download.
|
|
135
|
+
3. **Último valor conhecido**: se a API estiver inacessível, o cliente registra um warning e continua servindo o último prompt buscado com sucesso (por chave nome/ambiente/fewshots), pela vida do processo.
|
|
136
|
+
4. **Arquivos de fallback locais**: se a API estiver inacessível *e* nada foi buscado ainda (ex.: logo após um cold start), o cliente carrega `<fallback_dir>/<nome>.<ambiente>.json`, depois `<fallback_dir>/<nome>.json`.
|
|
137
|
+
|
|
138
|
+
Só quando os quatro falham ele levanta `PromptUnavailableError`.
|
|
139
|
+
|
|
140
|
+
Gere os snapshots de fallback com a CLI incluída (commite-os no seu repositório ou embuta-os na sua imagem, atualizando a cada deploy):
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
uv run prompt-manager pull --app superchat --env prod --out prompts_fallback/
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
(`--url` sobrescreve o endereço da API; caso contrário valem `PROMPT_MANAGER_URL` / o padrão de produção, igual à biblioteca.)
|
|
147
|
+
|
|
148
|
+
## Relato de uso
|
|
149
|
+
|
|
150
|
+
Cada `render()` dispara (fire-and-forget) um relato para `POST /api/v1/usage` com a identidade do prompt (nome, versão, hash do snapshot) e os valores de variáveis com que foi preenchido. A UI do manager os lê de volta para que os prompts possam ser testados contra **casos reais de produção** em vez de casos inventados.
|
|
151
|
+
|
|
152
|
+
- Nunca bloqueia nem quebra um render — falhas são logadas em nível debug e descartadas.
|
|
153
|
+
- Passe `request_id`, `user` e `metadata` para tornar os casos gravados filtráveis.
|
|
154
|
+
- Desabilite completamente com `report_usage=False` (ex.: em testes), ou no servidor deixando `OPENSEARCH_HOST` vazio.
|
|
155
|
+
|
|
156
|
+
## Referência do construtor
|
|
157
|
+
|
|
158
|
+
| Parâmetro | Padrão | Significado |
|
|
159
|
+
|-----------|--------|-------------|
|
|
160
|
+
| `application` | *(obrigatório)* | Nome da aplicação registrada no manager |
|
|
161
|
+
| `base_url` | `$PROMPT_MANAGER_URL`, senão `http://promptmanager-prod.datalawyer.local` | Raiz da API do Prompt Manager |
|
|
162
|
+
| `environment` | `"default"` | Ambiente padrão de todas as chamadas |
|
|
163
|
+
| `ttl` | `45` | Segundos que um prompt buscado é servido sem revalidação |
|
|
164
|
+
| `fallback_dir` | `None` | Diretório com snapshots offline do `prompt-manager pull` |
|
|
165
|
+
| `timeout` | `10` | Timeout HTTP em segundos |
|
|
166
|
+
| `session` / `client` | `None` | Traga seu próprio `requests.Session` / `httpx.AsyncClient` |
|
|
167
|
+
| `report_usage` | `True` | Relata as variáveis de cada render para a tela de Registros |
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Prompt Manager — Cliente Python
|
|
2
|
+
|
|
3
|
+
`prompt-manager-client` é a biblioteca que as aplicações de IA importam para buscar seus prompts na API do Prompt Manager em tempo de execução. Os prompts se atualizam ao vivo: quando alguém commita e publica uma nova versão na UI, a aplicação passa a usá-la em segundos, sem redeploy.
|
|
4
|
+
|
|
5
|
+
Ela é deliberadamente pequena — três coisas públicas:
|
|
6
|
+
|
|
7
|
+
- `PromptManager` — cliente síncrono (`requests`)
|
|
8
|
+
- `AsyncPromptManager` — cliente assíncrono (`httpx`), mesma interface com `await`
|
|
9
|
+
- `Prompt` — o prompt imutável e totalmente resolvido que uma busca retorna
|
|
10
|
+
|
|
11
|
+
## Instalação
|
|
12
|
+
|
|
13
|
+
O pacote vive neste repositório, em `client/`. Adicione-o como dependência de path/git, por exemplo com uv:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv add "prompt-manager-client @ git+ssh://git@bitbucket.org/avisourgente/prompt-manager.git#subdirectory=client"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Requer Python ≥ 3.10. Dependências: `requests`, `httpx`, `cachetools`.
|
|
20
|
+
|
|
21
|
+
## Começo rápido
|
|
22
|
+
|
|
23
|
+
Crie **um cliente por processo** na inicialização (ele guarda o cache e a sessão HTTP) e reutilize-o:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from prompt_manager import PromptManager
|
|
27
|
+
|
|
28
|
+
pm = PromptManager(
|
|
29
|
+
application="superchat", # o nome da sua aplicação no manager
|
|
30
|
+
environment="prod", # qual deployment seguir
|
|
31
|
+
fallback_dir="prompts_fallback", # opcional: snapshots offline (veja abaixo)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Uma chamada: busca (com cache) + preenche variáveis + relata o uso
|
|
35
|
+
text = pm.render(
|
|
36
|
+
"resposta_juridica",
|
|
37
|
+
variables={"pergunta": pergunta, "contexto": contexto},
|
|
38
|
+
request_id=request_id, # campos opcionais de observabilidade —
|
|
39
|
+
user=user_email, # eles vinculam o caso real ao prompt
|
|
40
|
+
metadata={"tenant": tenant}, # na tela de "Registros" do manager
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`render()` retorna a string final pronta para enviar ao modelo. Se o prompt define mensagens de chat, você provavelmente quer `get_prompt()` (abaixo).
|
|
45
|
+
|
|
46
|
+
### Onde o cliente se conecta (`base_url`)
|
|
47
|
+
|
|
48
|
+
O endereço da API é resolvido nesta ordem:
|
|
49
|
+
|
|
50
|
+
1. o argumento `base_url` do construtor, se informado;
|
|
51
|
+
2. a variável de ambiente **`PROMPT_MANAGER_URL`**;
|
|
52
|
+
3. o padrão de produção: **`http://promptmanager-prod.datalawyer.local`**.
|
|
53
|
+
|
|
54
|
+
Ou seja: em produção você não configura nada, e deployments de staging/dev redirecionam todos os clientes com uma única variável de ambiente:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
export PROMPT_MANAGER_URL=http://localhost:8000 # ex.: stack de dev local
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Async
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from prompt_manager import AsyncPromptManager
|
|
64
|
+
|
|
65
|
+
pm = AsyncPromptManager(application="superchat", environment="prod")
|
|
66
|
+
|
|
67
|
+
async def handle(question: str) -> str:
|
|
68
|
+
return await pm.render("resposta_juridica", variables={"pergunta": question})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Use-o como async context manager (`async with AsyncPromptManager(...) as pm:`) ou mantenha-o pela vida do processo; ao sair, ele descarrega os relatos de uso pendentes e fecha seu cliente `httpx` (a menos que você tenha passado o seu próprio).
|
|
72
|
+
|
|
73
|
+
## Trabalhando com o objeto `Prompt`
|
|
74
|
+
|
|
75
|
+
`get_prompt()` retorna o prompt resolvido sem renderizá-lo:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
prompt = pm.get_prompt("resposta_juridica")
|
|
79
|
+
|
|
80
|
+
prompt.template # texto resolvido completo (fragmentos, controles e exemplos já expandidos)
|
|
81
|
+
prompt.variables # as variáveis de runtime que o template ainda espera
|
|
82
|
+
prompt.version # número da versão commitada que esta resolução usou
|
|
83
|
+
prompt.hash # hash do snapshot — registre-o para rastrear qualquer chamada de LLM até o prompt exato
|
|
84
|
+
prompt.suggested_model # modelo + parâmetros sugeridos pelo autor do prompt
|
|
85
|
+
prompt.suggested_params
|
|
86
|
+
prompt.fragment_versions # quais versões de fragmentos foram embutidas
|
|
87
|
+
prompt.fewshots # quais bancos de exemplos foram injetados, e quantos exemplos cada um contribuiu
|
|
88
|
+
|
|
89
|
+
rendered = prompt.render(pergunta="...", contexto="...")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`render` (tanto no cliente quanto no `Prompt`) valida os valores: nomes de variáveis desconhecidos, variáveis obrigatórias faltando e `{{placeholders}}` restantes levantam `PromptValidationError`. Valores dict/list são codificados em JSON automaticamente.
|
|
93
|
+
|
|
94
|
+
### Mensagens de chat
|
|
95
|
+
|
|
96
|
+
Um template resolvido pode conter linhas com marcadores de papel `{% system %}` / `{% user %}` / `{% assistant %}`. O cliente mantém o template plano (os marcadores sobrevivem intactos ao `render()`); para prompts com múltiplas mensagens, divida você mesmo o texto renderizado nessas linhas de marcador: uma nova mensagem começa em cada marcador, e o conteúdo antes do primeiro marcador pertence a `system`. A resposta do `/fetch` da API também carrega um array `messages` já dividido, se você preferir consumi-lo diretamente.
|
|
97
|
+
|
|
98
|
+
## Ambientes
|
|
99
|
+
|
|
100
|
+
O `environment` informado na construção é o padrão; qualquer chamada pode sobrescrevê-lo:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
pm.get_prompt("resposta_juridica", environment="staging")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`"default"` é especial: ele sempre segue a **versão commitada mais recente**, sem precisar de deployment explícito. Ambientes nomeados (`prod`, `staging`, …) servem a versão que foi fixada neles pela UI — e caem para a versão mais recente se o prompt nunca foi fixado ali.
|
|
107
|
+
|
|
108
|
+
## Ablação de few-shots (A/B sem exemplos)
|
|
109
|
+
|
|
110
|
+
`fewshots=False` resolve o mesmo prompt com todos os bancos de exemplos desligados — é assim que uma aplicação mede o que os exemplos realmente valem:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
com_exemplos = pm.render("classificador", variables=v) # normal
|
|
114
|
+
sem_exemplos = pm.render("classificador", variables=v, fewshots=False) # ablação
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
As duas resoluções têm cache e ETag separados.
|
|
118
|
+
|
|
119
|
+
## Cache, resiliência e fallback offline
|
|
120
|
+
|
|
121
|
+
O cliente é construído para que a API de prompts nunca seja um ponto único de falha:
|
|
122
|
+
|
|
123
|
+
1. **Cache com TTL** (padrão 45 s, `ttl=` no construtor): buscas repetidas dentro da janela não custam nada.
|
|
124
|
+
2. **Revalidação por ETag**: expirado o TTL, o cliente revalida com `If-None-Match`; um prompt inalterado custa um 304, não um novo download.
|
|
125
|
+
3. **Último valor conhecido**: se a API estiver inacessível, o cliente registra um warning e continua servindo o último prompt buscado com sucesso (por chave nome/ambiente/fewshots), pela vida do processo.
|
|
126
|
+
4. **Arquivos de fallback locais**: se a API estiver inacessível *e* nada foi buscado ainda (ex.: logo após um cold start), o cliente carrega `<fallback_dir>/<nome>.<ambiente>.json`, depois `<fallback_dir>/<nome>.json`.
|
|
127
|
+
|
|
128
|
+
Só quando os quatro falham ele levanta `PromptUnavailableError`.
|
|
129
|
+
|
|
130
|
+
Gere os snapshots de fallback com a CLI incluída (commite-os no seu repositório ou embuta-os na sua imagem, atualizando a cada deploy):
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
uv run prompt-manager pull --app superchat --env prod --out prompts_fallback/
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
(`--url` sobrescreve o endereço da API; caso contrário valem `PROMPT_MANAGER_URL` / o padrão de produção, igual à biblioteca.)
|
|
137
|
+
|
|
138
|
+
## Relato de uso
|
|
139
|
+
|
|
140
|
+
Cada `render()` dispara (fire-and-forget) um relato para `POST /api/v1/usage` com a identidade do prompt (nome, versão, hash do snapshot) e os valores de variáveis com que foi preenchido. A UI do manager os lê de volta para que os prompts possam ser testados contra **casos reais de produção** em vez de casos inventados.
|
|
141
|
+
|
|
142
|
+
- Nunca bloqueia nem quebra um render — falhas são logadas em nível debug e descartadas.
|
|
143
|
+
- Passe `request_id`, `user` e `metadata` para tornar os casos gravados filtráveis.
|
|
144
|
+
- Desabilite completamente com `report_usage=False` (ex.: em testes), ou no servidor deixando `OPENSEARCH_HOST` vazio.
|
|
145
|
+
|
|
146
|
+
## Referência do construtor
|
|
147
|
+
|
|
148
|
+
| Parâmetro | Padrão | Significado |
|
|
149
|
+
|-----------|--------|-------------|
|
|
150
|
+
| `application` | *(obrigatório)* | Nome da aplicação registrada no manager |
|
|
151
|
+
| `base_url` | `$PROMPT_MANAGER_URL`, senão `http://promptmanager-prod.datalawyer.local` | Raiz da API do Prompt Manager |
|
|
152
|
+
| `environment` | `"default"` | Ambiente padrão de todas as chamadas |
|
|
153
|
+
| `ttl` | `45` | Segundos que um prompt buscado é servido sem revalidação |
|
|
154
|
+
| `fallback_dir` | `None` | Diretório com snapshots offline do `prompt-manager pull` |
|
|
155
|
+
| `timeout` | `10` | Timeout HTTP em segundos |
|
|
156
|
+
| `session` / `client` | `None` | Traga seu próprio `requests.Session` / `httpx.AsyncClient` |
|
|
157
|
+
| `report_usage` | `True` | Relata as variáveis de cada render para a tela de Registros |
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "prompt-manager-client-dl"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Live-updating client for Prompt Manager"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"cachetools>=5,<7",
|
|
9
|
+
"httpx>=0.27,<1",
|
|
10
|
+
"requests>=2.31,<3",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[project.scripts]
|
|
14
|
+
prompt-manager = "prompt_manager.cli:main"
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["hatchling"]
|
|
18
|
+
build-backend = "hatchling.build"
|
|
19
|
+
|
|
20
|
+
[tool.hatch.build.targets.wheel]
|
|
21
|
+
packages = ["src/prompt_manager"]
|
|
22
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .async_client import AsyncPromptManager
|
|
2
|
+
from .client import PromptManager
|
|
3
|
+
from .models import Prompt, PromptUnavailableError, PromptValidationError
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"AsyncPromptManager",
|
|
7
|
+
"Prompt",
|
|
8
|
+
"PromptManager",
|
|
9
|
+
"PromptUnavailableError",
|
|
10
|
+
"PromptValidationError",
|
|
11
|
+
]
|
|
12
|
+
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .client import resolve_base_url, usage_payload
|
|
11
|
+
from .models import Prompt, PromptUnavailableError
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AsyncPromptManager:
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
*,
|
|
20
|
+
application: str,
|
|
21
|
+
base_url: str | None = None,
|
|
22
|
+
environment: str = "default",
|
|
23
|
+
ttl: int = 45,
|
|
24
|
+
fallback_dir: str | Path | None = None,
|
|
25
|
+
timeout: float = 10,
|
|
26
|
+
client: httpx.AsyncClient | None = None,
|
|
27
|
+
report_usage: bool = True,
|
|
28
|
+
):
|
|
29
|
+
self.base_url = resolve_base_url(base_url)
|
|
30
|
+
self.application = application
|
|
31
|
+
self.environment = environment
|
|
32
|
+
self.ttl = ttl
|
|
33
|
+
self.fallback_dir = Path(fallback_dir) if fallback_dir else None
|
|
34
|
+
self.client = client or httpx.AsyncClient(timeout=timeout)
|
|
35
|
+
self._owns_client = client is None
|
|
36
|
+
# Every render() reports the variables it was filled with, so the UI can
|
|
37
|
+
# replay real cases. Fire-and-forget: never blocks or breaks a render.
|
|
38
|
+
self.report_usage = report_usage
|
|
39
|
+
# Keeps fire-and-forget tasks referenced until they finish.
|
|
40
|
+
self._usage_tasks: set[asyncio.Task] = set()
|
|
41
|
+
# Keyed by (name, environment, with-examples): the two resolutions are
|
|
42
|
+
# different prompts and must not share a cache entry or an ETag.
|
|
43
|
+
self._cache: dict[tuple[str, str, bool], tuple[float, Prompt]] = {}
|
|
44
|
+
self._last_known_good: dict[tuple[str, str, bool], Prompt] = {}
|
|
45
|
+
self._etags: dict[tuple[str, str, bool], str] = {}
|
|
46
|
+
|
|
47
|
+
async def __aenter__(self):
|
|
48
|
+
return self
|
|
49
|
+
|
|
50
|
+
async def __aexit__(self, *_: object):
|
|
51
|
+
if self._usage_tasks:
|
|
52
|
+
await asyncio.gather(*self._usage_tasks, return_exceptions=True)
|
|
53
|
+
if self._owns_client:
|
|
54
|
+
await self.client.aclose()
|
|
55
|
+
|
|
56
|
+
def _fallback(self, name: str, environment: str) -> Prompt | None:
|
|
57
|
+
if not self.fallback_dir:
|
|
58
|
+
return None
|
|
59
|
+
for path in (
|
|
60
|
+
self.fallback_dir / f"{name}.{environment}.json",
|
|
61
|
+
self.fallback_dir / f"{name}.json",
|
|
62
|
+
):
|
|
63
|
+
if path.exists():
|
|
64
|
+
return Prompt.from_dict(json.loads(path.read_text(encoding="utf-8")))
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
async def get_prompt(
|
|
68
|
+
self, name: str, *, environment: str | None = None, fewshots: bool = True
|
|
69
|
+
) -> Prompt:
|
|
70
|
+
"""Fetch a prompt. `fewshots=False` resolves it without its example banks,
|
|
71
|
+
which is how an application measures what those examples are worth."""
|
|
72
|
+
env = environment or self.environment
|
|
73
|
+
key = (name, env, fewshots)
|
|
74
|
+
cached = self._cache.get(key)
|
|
75
|
+
if cached and cached[0] > time.monotonic():
|
|
76
|
+
return cached[1]
|
|
77
|
+
headers = {"If-None-Match": self._etags[key]} if key in self._etags else {}
|
|
78
|
+
params = {"environment": env}
|
|
79
|
+
if not fewshots:
|
|
80
|
+
params["fewshots"] = "off"
|
|
81
|
+
try:
|
|
82
|
+
response = await self.client.get(
|
|
83
|
+
f"{self.base_url}/api/v1/fetch/{self.application}/{name}",
|
|
84
|
+
params=params,
|
|
85
|
+
headers=headers,
|
|
86
|
+
)
|
|
87
|
+
if response.status_code == 304 and key in self._last_known_good:
|
|
88
|
+
prompt = self._last_known_good[key]
|
|
89
|
+
else:
|
|
90
|
+
response.raise_for_status()
|
|
91
|
+
prompt = Prompt.from_dict(response.json())
|
|
92
|
+
self._etags[key] = prompt.hash
|
|
93
|
+
self._cache[key] = (time.monotonic() + self.ttl, prompt)
|
|
94
|
+
self._last_known_good[key] = prompt
|
|
95
|
+
return prompt
|
|
96
|
+
except httpx.HTTPError as exc:
|
|
97
|
+
prompt = self._last_known_good.get(key) or self._fallback(name, env)
|
|
98
|
+
if prompt:
|
|
99
|
+
logger.warning("Prompt API unavailable; using stale/fallback prompt")
|
|
100
|
+
return prompt
|
|
101
|
+
raise PromptUnavailableError(
|
|
102
|
+
f"Unable to fetch {self.application}/{name} ({env})"
|
|
103
|
+
) from exc
|
|
104
|
+
|
|
105
|
+
async def render(
|
|
106
|
+
self,
|
|
107
|
+
name: str,
|
|
108
|
+
*,
|
|
109
|
+
variables: dict[str, Any],
|
|
110
|
+
environment: str | None = None,
|
|
111
|
+
fewshots: bool = True,
|
|
112
|
+
request_id: str | None = None,
|
|
113
|
+
user: str | None = None,
|
|
114
|
+
metadata: dict[str, Any] | None = None,
|
|
115
|
+
) -> str:
|
|
116
|
+
"""`request_id`, `user` and `metadata` describe the original request; they
|
|
117
|
+
travel with the usage report and end up attached to the recorded case."""
|
|
118
|
+
prompt = await self.get_prompt(name, environment=environment, fewshots=fewshots)
|
|
119
|
+
rendered = prompt.render(**variables)
|
|
120
|
+
if self.report_usage:
|
|
121
|
+
payload = usage_payload(prompt, variables, request_id, user, metadata)
|
|
122
|
+
task = asyncio.create_task(self._post_usage(payload))
|
|
123
|
+
self._usage_tasks.add(task)
|
|
124
|
+
task.add_done_callback(self._usage_tasks.discard)
|
|
125
|
+
return rendered
|
|
126
|
+
|
|
127
|
+
async def _post_usage(self, payload: dict[str, Any]) -> None:
|
|
128
|
+
try:
|
|
129
|
+
await self.client.post(f"{self.base_url}/api/v1/usage", json=payload)
|
|
130
|
+
except httpx.HTTPError:
|
|
131
|
+
logger.debug("Usage report failed", exc_info=True)
|
|
132
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from .client import PromptManager, resolve_base_url
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def pull(args: argparse.Namespace) -> None:
|
|
11
|
+
base_url = resolve_base_url(args.url)
|
|
12
|
+
output = Path(args.out)
|
|
13
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
14
|
+
response = requests.get(
|
|
15
|
+
f"{base_url}/api/v1/applications/{args.app}/prompts",
|
|
16
|
+
timeout=30,
|
|
17
|
+
)
|
|
18
|
+
response.raise_for_status()
|
|
19
|
+
manager = PromptManager(
|
|
20
|
+
base_url=base_url,
|
|
21
|
+
application=args.app,
|
|
22
|
+
environment=args.env,
|
|
23
|
+
ttl=1,
|
|
24
|
+
)
|
|
25
|
+
count = 0
|
|
26
|
+
for item in response.json():
|
|
27
|
+
if item["kind"] != "prompt" or item.get("archived_at"):
|
|
28
|
+
continue
|
|
29
|
+
prompt = manager.get_prompt(item["name"])
|
|
30
|
+
path = output / f"{item['name']}.{args.env}.json"
|
|
31
|
+
path.write_text(
|
|
32
|
+
json.dumps(prompt.to_dict(), indent=2, ensure_ascii=False, default=str),
|
|
33
|
+
encoding="utf-8",
|
|
34
|
+
)
|
|
35
|
+
count += 1
|
|
36
|
+
print(f"Pulled {count} prompt(s) to {output}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main() -> None:
|
|
40
|
+
parser = argparse.ArgumentParser(prog="prompt-manager")
|
|
41
|
+
commands = parser.add_subparsers(required=True)
|
|
42
|
+
command = commands.add_parser("pull", help="Download offline prompt snapshots")
|
|
43
|
+
command.add_argument(
|
|
44
|
+
"--url",
|
|
45
|
+
default=None,
|
|
46
|
+
help="API base URL (default: $PROMPT_MANAGER_URL or the production instance)",
|
|
47
|
+
)
|
|
48
|
+
command.add_argument("--app", required=True)
|
|
49
|
+
command.add_argument("--env", default="default")
|
|
50
|
+
command.add_argument("--out", required=True)
|
|
51
|
+
command.set_defaults(handler=pull)
|
|
52
|
+
args = parser.parse_args()
|
|
53
|
+
args.handler(args)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
main()
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import threading
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
from cachetools import TTLCache
|
|
10
|
+
|
|
11
|
+
from .models import Prompt, PromptUnavailableError
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
DEFAULT_BASE_URL = "http://promptmanager-prod.datalawyer.local"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def resolve_base_url(base_url: str | None) -> str:
|
|
19
|
+
"""Explicit argument first, then $PROMPT_MANAGER_URL, then the production
|
|
20
|
+
default — so deployments can repoint every client without a code change."""
|
|
21
|
+
return (
|
|
22
|
+
base_url or os.environ.get("PROMPT_MANAGER_URL") or DEFAULT_BASE_URL
|
|
23
|
+
).rstrip("/")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def usage_payload(
|
|
27
|
+
prompt: Prompt,
|
|
28
|
+
variables: dict[str, Any],
|
|
29
|
+
request_id: str | None,
|
|
30
|
+
user: str | None,
|
|
31
|
+
metadata: dict[str, Any] | None,
|
|
32
|
+
) -> dict[str, Any]:
|
|
33
|
+
"""The document POSTed to /api/v1/usage. Values are coerced to JSON-safe
|
|
34
|
+
(str fallback) so an odd variable type never breaks the report."""
|
|
35
|
+
return json.loads(
|
|
36
|
+
json.dumps(
|
|
37
|
+
{
|
|
38
|
+
"application": prompt.application,
|
|
39
|
+
"prompt": prompt.name,
|
|
40
|
+
"environment": prompt.environment,
|
|
41
|
+
"prompt_version": prompt.version,
|
|
42
|
+
"snapshot_hash": prompt.hash,
|
|
43
|
+
"variables": variables,
|
|
44
|
+
"request_id": request_id,
|
|
45
|
+
"user": user,
|
|
46
|
+
"metadata": metadata or {},
|
|
47
|
+
},
|
|
48
|
+
ensure_ascii=False,
|
|
49
|
+
default=str,
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class PromptManager:
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
*,
|
|
58
|
+
application: str,
|
|
59
|
+
base_url: str | None = None,
|
|
60
|
+
environment: str = "default",
|
|
61
|
+
ttl: int = 45,
|
|
62
|
+
fallback_dir: str | Path | None = None,
|
|
63
|
+
timeout: float = 10,
|
|
64
|
+
session: requests.Session | None = None,
|
|
65
|
+
report_usage: bool = True,
|
|
66
|
+
):
|
|
67
|
+
self.base_url = resolve_base_url(base_url)
|
|
68
|
+
self.application = application
|
|
69
|
+
self.environment = environment
|
|
70
|
+
self.fallback_dir = Path(fallback_dir) if fallback_dir else None
|
|
71
|
+
self.timeout = timeout
|
|
72
|
+
self.session = session or requests.Session()
|
|
73
|
+
# Every render() reports the variables it was filled with, so the UI can
|
|
74
|
+
# replay real cases. Fire-and-forget: never blocks or breaks a render.
|
|
75
|
+
self.report_usage = report_usage
|
|
76
|
+
self._cache: TTLCache = TTLCache(maxsize=512, ttl=ttl)
|
|
77
|
+
# Keyed by (name, environment, with-examples): the two resolutions are
|
|
78
|
+
# different prompts and must not share a cache entry or an ETag.
|
|
79
|
+
self._last_known_good: dict[tuple[str, str, bool], Prompt] = {}
|
|
80
|
+
self._etags: dict[tuple[str, str, bool], str] = {}
|
|
81
|
+
|
|
82
|
+
def _fallback(self, name: str, environment: str) -> Prompt | None:
|
|
83
|
+
if not self.fallback_dir:
|
|
84
|
+
return None
|
|
85
|
+
candidates = [
|
|
86
|
+
self.fallback_dir / f"{name}.{environment}.json",
|
|
87
|
+
self.fallback_dir / f"{name}.json",
|
|
88
|
+
]
|
|
89
|
+
for path in candidates:
|
|
90
|
+
if path.exists():
|
|
91
|
+
return Prompt.from_dict(json.loads(path.read_text(encoding="utf-8")))
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
def get_prompt(
|
|
95
|
+
self, name: str, *, environment: str | None = None, fewshots: bool = True
|
|
96
|
+
) -> Prompt:
|
|
97
|
+
"""Fetch a prompt. `fewshots=False` resolves it without its example banks,
|
|
98
|
+
which is how an application measures what those examples are worth."""
|
|
99
|
+
env = environment or self.environment
|
|
100
|
+
key = (name, env, fewshots)
|
|
101
|
+
cached = self._cache.get(key)
|
|
102
|
+
if cached:
|
|
103
|
+
return cached
|
|
104
|
+
headers: dict[str, str] = {}
|
|
105
|
+
if key in self._etags:
|
|
106
|
+
headers["If-None-Match"] = self._etags[key]
|
|
107
|
+
params = {"environment": env}
|
|
108
|
+
if not fewshots:
|
|
109
|
+
params["fewshots"] = "off"
|
|
110
|
+
try:
|
|
111
|
+
response = self.session.get(
|
|
112
|
+
f"{self.base_url}/api/v1/fetch/{self.application}/{name}",
|
|
113
|
+
params=params,
|
|
114
|
+
headers=headers,
|
|
115
|
+
timeout=self.timeout,
|
|
116
|
+
)
|
|
117
|
+
if response.status_code == 304 and key in self._last_known_good:
|
|
118
|
+
prompt = self._last_known_good[key]
|
|
119
|
+
else:
|
|
120
|
+
response.raise_for_status()
|
|
121
|
+
prompt = Prompt.from_dict(response.json())
|
|
122
|
+
self._etags[key] = prompt.hash
|
|
123
|
+
self._cache[key] = prompt
|
|
124
|
+
self._last_known_good[key] = prompt
|
|
125
|
+
return prompt
|
|
126
|
+
except requests.RequestException as exc:
|
|
127
|
+
if key in self._last_known_good:
|
|
128
|
+
logger.warning("Prompt API unavailable; using stale %s/%s", name, env)
|
|
129
|
+
return self._last_known_good[key]
|
|
130
|
+
fallback = self._fallback(name, env)
|
|
131
|
+
if fallback:
|
|
132
|
+
logger.warning("Prompt API unavailable; using local fallback %s", name)
|
|
133
|
+
self._last_known_good[key] = fallback
|
|
134
|
+
return fallback
|
|
135
|
+
raise PromptUnavailableError(
|
|
136
|
+
f"Unable to fetch {self.application}/{name} ({env})"
|
|
137
|
+
) from exc
|
|
138
|
+
|
|
139
|
+
def render(
|
|
140
|
+
self,
|
|
141
|
+
name: str,
|
|
142
|
+
*,
|
|
143
|
+
variables: dict[str, Any],
|
|
144
|
+
environment: str | None = None,
|
|
145
|
+
fewshots: bool = True,
|
|
146
|
+
request_id: str | None = None,
|
|
147
|
+
user: str | None = None,
|
|
148
|
+
metadata: dict[str, Any] | None = None,
|
|
149
|
+
) -> str:
|
|
150
|
+
"""`request_id`, `user` and `metadata` describe the original request; they
|
|
151
|
+
travel with the usage report and end up attached to the recorded case."""
|
|
152
|
+
prompt = self.get_prompt(name, environment=environment, fewshots=fewshots)
|
|
153
|
+
rendered = prompt.render(**variables)
|
|
154
|
+
if self.report_usage:
|
|
155
|
+
payload = usage_payload(prompt, variables, request_id, user, metadata)
|
|
156
|
+
threading.Thread(
|
|
157
|
+
target=self._post_usage, args=(payload,), daemon=True
|
|
158
|
+
).start()
|
|
159
|
+
return rendered
|
|
160
|
+
|
|
161
|
+
def _post_usage(self, payload: dict[str, Any]) -> None:
|
|
162
|
+
# requests.post (not self.session): sessions are not thread-safe.
|
|
163
|
+
try:
|
|
164
|
+
requests.post(
|
|
165
|
+
f"{self.base_url}/api/v1/usage", json=payload, timeout=self.timeout
|
|
166
|
+
)
|
|
167
|
+
except requests.RequestException:
|
|
168
|
+
logger.debug("Usage report failed", exc_info=True)
|
|
169
|
+
|
|
170
|
+
def clear_cache(self) -> None:
|
|
171
|
+
self._cache.clear()
|
|
172
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PromptUnavailableError(RuntimeError):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PromptValidationError(ValueError):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class Prompt:
|
|
17
|
+
name: str
|
|
18
|
+
application: str
|
|
19
|
+
environment: str
|
|
20
|
+
version: int
|
|
21
|
+
template: str
|
|
22
|
+
variables: list[dict[str, Any]]
|
|
23
|
+
suggested_model: str | None
|
|
24
|
+
suggested_params: dict[str, Any]
|
|
25
|
+
hash: str
|
|
26
|
+
resolved_at: str
|
|
27
|
+
fragment_versions: dict[str, Any] = field(default_factory=dict)
|
|
28
|
+
# Which example banks this resolution injected, and how many examples each
|
|
29
|
+
# contributed — empty when the prompt uses none, or when they were switched off.
|
|
30
|
+
fewshots: dict[str, Any] = field(default_factory=dict)
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def from_dict(cls, data: dict[str, Any]) -> "Prompt":
|
|
34
|
+
fields = cls.__dataclass_fields__
|
|
35
|
+
return cls(**{key: value for key, value in data.items() if key in fields})
|
|
36
|
+
|
|
37
|
+
def to_dict(self) -> dict[str, Any]:
|
|
38
|
+
return {
|
|
39
|
+
field_name: getattr(self, field_name)
|
|
40
|
+
for field_name in self.__dataclass_fields__
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
def render(self, **values: Any) -> str:
|
|
44
|
+
result = self.template
|
|
45
|
+
known = {variable["name"] for variable in self.variables}
|
|
46
|
+
unknown = sorted(set(values) - known)
|
|
47
|
+
if unknown:
|
|
48
|
+
raise PromptValidationError(f"Unknown variables: {', '.join(unknown)}")
|
|
49
|
+
for variable in self.variables:
|
|
50
|
+
name = variable["name"]
|
|
51
|
+
value = values.get(name, variable.get("default_value"))
|
|
52
|
+
if value is None and variable.get("required", True):
|
|
53
|
+
raise PromptValidationError(f"Missing required variable: {name}")
|
|
54
|
+
if value is None:
|
|
55
|
+
value = ""
|
|
56
|
+
if isinstance(value, (dict, list)):
|
|
57
|
+
value = json.dumps(value, ensure_ascii=False)
|
|
58
|
+
result = re.sub(
|
|
59
|
+
r"\{\{\s*" + re.escape(name) + r"\s*\}\}",
|
|
60
|
+
lambda _: str(value),
|
|
61
|
+
result,
|
|
62
|
+
)
|
|
63
|
+
leftovers = re.findall(r"\{\{\s*([a-z_][a-z0-9_]*)\s*\}\}", result)
|
|
64
|
+
if leftovers:
|
|
65
|
+
raise PromptValidationError(
|
|
66
|
+
f"Unresolved variables: {', '.join(sorted(set(leftovers)))}"
|
|
67
|
+
)
|
|
68
|
+
return result
|
|
69
|
+
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 3
|
|
3
|
+
requires-python = ">=3.10"
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "anyio"
|
|
7
|
+
version = "4.14.2"
|
|
8
|
+
source = { registry = "https://pypi.org/simple" }
|
|
9
|
+
dependencies = [
|
|
10
|
+
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
|
11
|
+
{ name = "idna" },
|
|
12
|
+
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
|
13
|
+
]
|
|
14
|
+
sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" }
|
|
15
|
+
wheels = [
|
|
16
|
+
{ url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" },
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[[package]]
|
|
20
|
+
name = "cachetools"
|
|
21
|
+
version = "6.2.6"
|
|
22
|
+
source = { registry = "https://pypi.org/simple" }
|
|
23
|
+
sdist = { url = "https://files.pythonhosted.org/packages/39/91/d9ae9a66b01102a18cd16db0cf4cd54187ffe10f0865cc80071a4104fbb3/cachetools-6.2.6.tar.gz", hash = "sha256:16c33e1f276b9a9c0b49ab5782d901e3ad3de0dd6da9bf9bcd29ac5672f2f9e6", size = 32363, upload-time = "2026-01-27T20:32:59.956Z" }
|
|
24
|
+
wheels = [
|
|
25
|
+
{ url = "https://files.pythonhosted.org/packages/90/45/f458fa2c388e79dd9d8b9b0c99f1d31b568f27388f2fdba7bb66bbc0c6ed/cachetools-6.2.6-py3-none-any.whl", hash = "sha256:8c9717235b3c651603fff0076db52d6acbfd1b338b8ed50256092f7ce9c85bda", size = 11668, upload-time = "2026-01-27T20:32:58.527Z" },
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[[package]]
|
|
29
|
+
name = "certifi"
|
|
30
|
+
version = "2026.6.17"
|
|
31
|
+
source = { registry = "https://pypi.org/simple" }
|
|
32
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" }
|
|
33
|
+
wheels = [
|
|
34
|
+
{ url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" },
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[[package]]
|
|
38
|
+
name = "charset-normalizer"
|
|
39
|
+
version = "3.4.9"
|
|
40
|
+
source = { registry = "https://pypi.org/simple" }
|
|
41
|
+
sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" }
|
|
42
|
+
wheels = [
|
|
43
|
+
{ url = "https://files.pythonhosted.org/packages/ad/81/8e983840c6e5b93b33c2ba81aa3d52c2e42f0e9a690ce7607a2e61da4a5c/charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a", size = 322240, upload-time = "2026-07-07T14:32:36.236Z" },
|
|
44
|
+
{ url = "https://files.pythonhosted.org/packages/de/d1/b4319dc3229d8272fba305e206fc0a148e2de8d4087917ce62ae6382f359/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616", size = 216475, upload-time = "2026-07-07T14:32:38.142Z" },
|
|
45
|
+
{ url = "https://files.pythonhosted.org/packages/80/33/6c99c1b3e6b8bf730e1bc809b9a2608f224145069114c479a2e9e1494346/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209", size = 238670, upload-time = "2026-07-07T14:32:39.658Z" },
|
|
46
|
+
{ url = "https://files.pythonhosted.org/packages/7f/f4/ffbb83546e1f198ecc70ecd372b65cf2b50f9068b380abd67640f17a8e18/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99", size = 233476, upload-time = "2026-07-07T14:32:41.155Z" },
|
|
47
|
+
{ url = "https://files.pythonhosted.org/packages/e8/5f/b98b8da398637b551e427e7be922bdec19177dc54d6811dcdaa503f23aac/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8", size = 223817, upload-time = "2026-07-07T14:32:42.592Z" },
|
|
48
|
+
{ url = "https://files.pythonhosted.org/packages/36/31/a276bb2e66243072a3fd06fdcab9cbb61a305b02143d70d2bda21d888fa8/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b", size = 207974, upload-time = "2026-07-07T14:32:44.258Z" },
|
|
49
|
+
{ url = "https://files.pythonhosted.org/packages/5e/be/7ee4453d7e88dfbc4104ccd34900b9f2c7c17dac22881865fe0e82424a25/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2", size = 221655, upload-time = "2026-07-07T14:32:45.64Z" },
|
|
50
|
+
{ url = "https://files.pythonhosted.org/packages/1d/85/181c652953eb5276d198f375b1dd641047392050098100a3a02d6534f657/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9", size = 219229, upload-time = "2026-07-07T14:32:47.376Z" },
|
|
51
|
+
{ url = "https://files.pythonhosted.org/packages/0c/e7/aaf6da33fc9f4691cda8f7efbc9f69179d3d39ec8a4799baf273ee1d8db0/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15", size = 209704, upload-time = "2026-07-07T14:32:48.855Z" },
|
|
52
|
+
{ url = "https://files.pythonhosted.org/packages/63/01/f2fb3bd3a73be48b173ee0c6aa8d2497af97d5663a8c4c4b491de4c62f7a/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d", size = 226243, upload-time = "2026-07-07T14:32:50.239Z" },
|
|
53
|
+
{ url = "https://files.pythonhosted.org/packages/c4/02/c57a22739fe05246b0b5783b3bfb6afaac4eebb46f3ececdfb2f048f780e/charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381", size = 150935, upload-time = "2026-07-07T14:32:51.676Z" },
|
|
54
|
+
{ url = "https://files.pythonhosted.org/packages/37/8d/ca39a7559a4797505530d084fd3a49a2c959efbbbff146302fb7be4e3b35/charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee", size = 162314, upload-time = "2026-07-07T14:32:53.193Z" },
|
|
55
|
+
{ url = "https://files.pythonhosted.org/packages/01/da/a44bd7a13d426e69e4894557106cd58669097bfad4a8681123b618fbfc5d/charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419", size = 153075, upload-time = "2026-07-07T14:32:54.554Z" },
|
|
56
|
+
{ url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" },
|
|
57
|
+
{ url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" },
|
|
58
|
+
{ url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" },
|
|
59
|
+
{ url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" },
|
|
60
|
+
{ url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" },
|
|
61
|
+
{ url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" },
|
|
62
|
+
{ url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" },
|
|
63
|
+
{ url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" },
|
|
64
|
+
{ url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" },
|
|
65
|
+
{ url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" },
|
|
66
|
+
{ url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" },
|
|
67
|
+
{ url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" },
|
|
68
|
+
{ url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" },
|
|
69
|
+
{ url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" },
|
|
70
|
+
{ url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" },
|
|
71
|
+
{ url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" },
|
|
72
|
+
{ url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" },
|
|
73
|
+
{ url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" },
|
|
74
|
+
{ url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" },
|
|
75
|
+
{ url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" },
|
|
76
|
+
{ url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" },
|
|
77
|
+
{ url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" },
|
|
78
|
+
{ url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" },
|
|
79
|
+
{ url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" },
|
|
80
|
+
{ url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" },
|
|
81
|
+
{ url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" },
|
|
82
|
+
{ url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" },
|
|
83
|
+
{ url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" },
|
|
84
|
+
{ url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" },
|
|
85
|
+
{ url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" },
|
|
86
|
+
{ url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" },
|
|
87
|
+
{ url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" },
|
|
88
|
+
{ url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" },
|
|
89
|
+
{ url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" },
|
|
90
|
+
{ url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" },
|
|
91
|
+
{ url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" },
|
|
92
|
+
{ url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" },
|
|
93
|
+
{ url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" },
|
|
94
|
+
{ url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" },
|
|
95
|
+
{ url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" },
|
|
96
|
+
{ url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" },
|
|
97
|
+
{ url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" },
|
|
98
|
+
{ url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" },
|
|
99
|
+
{ url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" },
|
|
100
|
+
{ url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" },
|
|
101
|
+
{ url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" },
|
|
102
|
+
{ url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" },
|
|
103
|
+
{ url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" },
|
|
104
|
+
{ url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" },
|
|
105
|
+
{ url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" },
|
|
106
|
+
{ url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" },
|
|
107
|
+
{ url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" },
|
|
108
|
+
{ url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" },
|
|
109
|
+
{ url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" },
|
|
110
|
+
{ url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" },
|
|
111
|
+
{ url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" },
|
|
112
|
+
{ url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" },
|
|
113
|
+
{ url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" },
|
|
114
|
+
{ url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" },
|
|
115
|
+
{ url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" },
|
|
116
|
+
{ url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" },
|
|
117
|
+
{ url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" },
|
|
118
|
+
{ url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" },
|
|
119
|
+
{ url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" },
|
|
120
|
+
{ url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" },
|
|
121
|
+
{ url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" },
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
[[package]]
|
|
125
|
+
name = "exceptiongroup"
|
|
126
|
+
version = "1.3.1"
|
|
127
|
+
source = { registry = "https://pypi.org/simple" }
|
|
128
|
+
dependencies = [
|
|
129
|
+
{ name = "typing-extensions" },
|
|
130
|
+
]
|
|
131
|
+
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
|
132
|
+
wheels = [
|
|
133
|
+
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
[[package]]
|
|
137
|
+
name = "h11"
|
|
138
|
+
version = "0.16.0"
|
|
139
|
+
source = { registry = "https://pypi.org/simple" }
|
|
140
|
+
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
|
141
|
+
wheels = [
|
|
142
|
+
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
[[package]]
|
|
146
|
+
name = "httpcore"
|
|
147
|
+
version = "1.0.9"
|
|
148
|
+
source = { registry = "https://pypi.org/simple" }
|
|
149
|
+
dependencies = [
|
|
150
|
+
{ name = "certifi" },
|
|
151
|
+
{ name = "h11" },
|
|
152
|
+
]
|
|
153
|
+
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
|
154
|
+
wheels = [
|
|
155
|
+
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
[[package]]
|
|
159
|
+
name = "httpx"
|
|
160
|
+
version = "0.28.1"
|
|
161
|
+
source = { registry = "https://pypi.org/simple" }
|
|
162
|
+
dependencies = [
|
|
163
|
+
{ name = "anyio" },
|
|
164
|
+
{ name = "certifi" },
|
|
165
|
+
{ name = "httpcore" },
|
|
166
|
+
{ name = "idna" },
|
|
167
|
+
]
|
|
168
|
+
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
|
169
|
+
wheels = [
|
|
170
|
+
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
[[package]]
|
|
174
|
+
name = "idna"
|
|
175
|
+
version = "3.18"
|
|
176
|
+
source = { registry = "https://pypi.org/simple" }
|
|
177
|
+
sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
|
|
178
|
+
wheels = [
|
|
179
|
+
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
[[package]]
|
|
183
|
+
name = "prompt-manager-client"
|
|
184
|
+
version = "0.1.0"
|
|
185
|
+
source = { editable = "." }
|
|
186
|
+
dependencies = [
|
|
187
|
+
{ name = "cachetools" },
|
|
188
|
+
{ name = "httpx" },
|
|
189
|
+
{ name = "requests" },
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
[package.metadata]
|
|
193
|
+
requires-dist = [
|
|
194
|
+
{ name = "cachetools", specifier = ">=5,<7" },
|
|
195
|
+
{ name = "httpx", specifier = ">=0.27,<1" },
|
|
196
|
+
{ name = "requests", specifier = ">=2.31,<3" },
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
[[package]]
|
|
200
|
+
name = "requests"
|
|
201
|
+
version = "2.34.2"
|
|
202
|
+
source = { registry = "https://pypi.org/simple" }
|
|
203
|
+
dependencies = [
|
|
204
|
+
{ name = "certifi" },
|
|
205
|
+
{ name = "charset-normalizer" },
|
|
206
|
+
{ name = "idna" },
|
|
207
|
+
{ name = "urllib3" },
|
|
208
|
+
]
|
|
209
|
+
sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
|
|
210
|
+
wheels = [
|
|
211
|
+
{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
[[package]]
|
|
215
|
+
name = "typing-extensions"
|
|
216
|
+
version = "4.16.0"
|
|
217
|
+
source = { registry = "https://pypi.org/simple" }
|
|
218
|
+
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
|
|
219
|
+
wheels = [
|
|
220
|
+
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
[[package]]
|
|
224
|
+
name = "urllib3"
|
|
225
|
+
version = "2.7.0"
|
|
226
|
+
source = { registry = "https://pypi.org/simple" }
|
|
227
|
+
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
|
|
228
|
+
wheels = [
|
|
229
|
+
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
|
|
230
|
+
]
|