Tirando env do commit

This commit is contained in:
Joao Monezi 2025-06-21 16:19:21 +00:00
parent e51648489b
commit 5feb9db6c0
2549 changed files with 397274 additions and 0 deletions

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
# Python
__pycache__/
*.pyc
# Ambientes Virtuais
venv/
.venv/ # Se você usa .venv como nome
# Variáveis de Ambiente e Chaves
.env
*.pem

1
.python-version Normal file
View File

@ -0,0 +1 @@
3.12.2

0
app/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

0
app/api/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

168
app/api/webhook.py Normal file
View File

@ -0,0 +1,168 @@
# app/api/webhook.py
from fastapi import APIRouter, Request, HTTPException, status
from fastapi.responses import PlainTextResponse, JSONResponse, Response
from pydantic import BaseModel
import json
from config import VERIFY_TOKEN
from models.webhook_model import WebhookEvent
from services.webhook_service import (
handle_message_type, mark_message_as_read,
encrypt_flow_response_data, decrypt_flow_request_data, send_whatsapp_flow # <--- ADICIONAR decrypt_flow_request_data
)
router = APIRouter()
class SendFlowRequest(BaseModel):
to_number: str # O número para onde enviar o Flow (ex: '5582912345678')
# ... (Endpoint GET /webhook) ...
# --- Endpoint para Receber Requisições POST (Incluindo TODOS os Payloads Criptografados e Webhook Padrão) ---
@router.post("/webhook")
async def process_whatsapp_webhook(request: Request):
try:
body = await request.json()
except json.JSONDecodeError as e:
print(f"⚠️ Requisição POST recebida com corpo não-JSON ou JSON inválido: {e}")
return JSONResponse(status_code=status.HTTP_200_OK, content={"status": "corpo inválido, ignorado"})
except Exception as e:
print(f"⚠️ Erro inesperado ao ler o corpo da requisição POST: {e}")
return JSONResponse(status_code=status.HTTP_200_OK, content={"status": "erro de leitura, ignorado"})
# --- Lógica para Requisições de Dados de Flow Criptografadas (incluindo Health Check criptografado) ---
if isinstance(body, dict) and "encrypted_flow_data" in body and "encrypted_aes_key" in body and "initial_vector" in body:
print("🔒 Recebido payload de Flow criptografado.")
try:
# Descriptografa a requisição para obter os dados, a chave AES e o IV originais
decrypted_result = decrypt_flow_request_data(
body["encrypted_flow_data"],
body["encrypted_aes_key"],
body["initial_vector"]
)
decrypted_data = decrypted_result["decrypted_payload"]
aes_key_from_request = decrypted_result["aes_key"]
iv_from_request = decrypted_result["initial_vector"]
print(f"🔓 Dados do Flow descriptografados: {json.dumps(decrypted_data, indent=2)}")
flow_response_data = {
"data": {
"status": "active"
}
}
# Processa o tipo de requisição descriptografada
if decrypted_data.get("action") == "ping":
print("💚 Descriptografado: Ping de dados do Flow. Preparando resposta 'active'.")
else:
print(f"❓ Descriptografado: Outros dados de Flow. Conteúdo: {decrypted_data}")
flow_response_data = {"status": "success", "message": "Dados de Flow processados."}
# Criptografa a resposta usando a chave AES e o IV DA REQUISIÇÃO ORIGINAL
# A função encrypt_flow_response_data AGORA RETORNA A STRING BASE64 DIRETA
encrypted_final_response_string = encrypt_flow_response_data(
flow_response_data,
aes_key_from_request,
iv_from_request
)
print(f"DEBUG: Enviando resposta final criptografada (Base64): {encrypted_final_response_string[:50]}...")
return Response(content=encrypted_final_response_string, media_type="text/plain")
except ValueError as ve:
print(f"❌ ERRO de chave ou dados para descriptografia/criptografia de Flow: {ve}")
return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={"status": f"Erro de chave ou criptografia: {ve}"})
except Exception as e:
print(f"❌ ERRO ao processar dados de Flow criptografados: {e}")
return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={"status": f"Erro interno ao processar Flow: {e}"})
# --- Lógica para Eventos de Webhook Padrão do WhatsApp (mensagens, status) ---
# Este bloco só será acionado se não for um payload de Flow criptografado
# --- Bloco para Eventos Padrão do WhatsApp (mensagens, status, etc.) ---
# Este é o bloco que interessa para o teste de sessão
elif isinstance(body, dict) and "object" in body and "entry" in body:
try:
event = WebhookEvent(**body)
if event.object != 'whatsapp_business_account':
print("❌ Evento recebido, mas não é do tipo 'whatsapp_business_account'. Ignorando.")
return JSONResponse(status_code=status.HTTP_404_NOT_FOUND, content={"status": "ignorado"})
for entry in event.entry:
for change in entry.changes:
if change.field == 'messages':
if change.value and change.value.messages:
for message in change.value.messages:
print(f"\n💬 Mensagem recebida de {message.from_} (Tipo: {message.type}, ID: {message.id})")
await mark_message_as_read(message.id)
# CHAME handle_message_type PASSANDO O OBJETO 'message' COMPLETO
await handle_message_type(message)
else:
print("⚠️ Evento de 'messages' recebido, mas sem mensagens válidas no payload.")
elif change.field == 'statuses':
if change.value and change.value.statuses:
for status_event in change.value.statuses:
print(f" Status da mensagem ID {status_event.id}: {status_event.status} (para {status_event.recipient_id})")
# Futuramente: handle_status_event(status_event)
else:
print("⚠️ Evento de 'statuses' recebido, mas sem status válidos no payload.")
# REMOVA OU COMENTE TEMPORARIAMENTE o bloco 'elif change.field == 'flows':' se estiver lá
# para focar apenas em mensagens/sessão.
# elif change.field == 'flows':
# ...
else:
print(f"⚠️ Evento de campo desconhecido recebido: {change.field}. Payload completo: {change.model_dump_json(indent=2) if hasattr(change, 'model_dump_json') else json.dumps(change.dict(), indent=2)}")
return JSONResponse(status_code=status.HTTP_200_OK, content={"status": "evento processado com sucesso"})
except Exception as e:
print(f"⚠️ Payload recebido não corresponde ao modelo WebhookEvent ou erro interno: {type(e).__name__}: {e}. Payload: {json.dumps(body)}")
return JSONResponse(status_code=status.HTTP_200_OK, content={"status": "payload desconhecido, ignorado"})
# --- Fallback para payloads POST não reconhecidos ---
else:
print(f"❓ Payload POST recebido que não é Health Check, Flow criptografado, nem WebhookEvent padrão. Ignorando. Payload: {json.dumps(body)}")
return JSONResponse(status_code=status.HTTP_200_OK, content={"status": "payload não reconhecido, ignorado"})
@router.post("/send_cadastro_flow")
async def trigger_cadastro_flow(request_data: SendFlowRequest):
"""
Dispara o Flow de cadastro do WhatsApp para um número específico.
"""
target_number = request_data.to_number # <-- ESTA LINHA DEVE ESTAR AQUI E ACESSÍVEL
# --- MUITO IMPORTANTE: Substitua pelo FLOW_ID REAL que você publicou ---
FLOW_ID_DO_SEU_CADASTRO_PUBLICADO = 1094799999286205
if FLOW_ID_DO_SEU_CADASTRO_PUBLICADO == "COLOQUE_AQUI_O_FLOW_ID_DO_SEU_CADASTRO_PUBLICADO":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Por favor, substitua 'COLOQUE_AQUI_O_FLOW_ID_DO_SEU_CADASTRO_PUBLICADO' pelo Flow ID real no código."
)
print(f"DEBUG: Disparando Flow de Cadastro (ID: {FLOW_ID_DO_SEU_CADASTRO_PUBLICADO}) para o número: {target_number}")
try:
response = await send_whatsapp_flow(target_number, FLOW_ID_DO_SEU_CADASTRO_PUBLICADO, "Abrir Formulário de Cadastro")
if response and response.get("status") == "success":
return JSONResponse(
status_code=status.HTTP_200_OK,
content={"message": f"Flow de cadastro enviado com sucesso para {target_number}", "whatsapp_api_response": response["data"]}
)
else:
print(f"DEBUG: send_whatsapp_flow retornou erro: {response.get('message', 'Erro desconhecido')}")
raise HTTPException(
status_code=response.get("code", status.HTTP_500_INTERNAL_SERVER_ERROR),
detail=f"Falha ao enviar Flow: {response.get('message', 'Erro desconhecido')}"
)
except HTTPException:
raise
except Exception as e:
print(f"❌ ERRO INESPERADO na trigger_cadastro_flow: {type(e).__name__}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Erro interno ao disparar Flow: {e}"
)

19
app/config.py Normal file
View File

@ -0,0 +1,19 @@
import os
from dotenv import load_dotenv
# Carrega as variáveis de ambiente do arquivo .env (na raiz do projeto)
load_dotenv()
# --- Variáveis de Configuração ---
VERIFY_TOKEN = os.getenv("WEBHOOK_VERIFICATION_TOKEN")
WHATSAPP_ACCESS_TOKEN = os.getenv("WHATSAPP_ACCESS_TOKEN")
WHATSAPP_PHONE_NUMBER_ID = os.getenv("WHATSAPP_PHONE_NUMBER_ID")
FLOW_PRIVATE_KEY_PASSWORD = os.getenv("FLOW_PRIVATE_KEY_PASSWORD")
# Verificação para garantir que as variáveis críticas estão presentes
if not VERIFY_TOKEN:
raise ValueError("WEBHOOK_VERIFICATION_TOKEN não está definido no .env")
if not WHATSAPP_ACCESS_TOKEN:
raise ValueError("WHATSAPP_ACCESS_TOKEN não está definido no .env")
if not WHATSAPP_PHONE_NUMBER_ID:
raise ValueError("WHATSAPP_PHONE_NUMBER_ID não está definido no .env")

38
app/main.py Normal file
View File

@ -0,0 +1,38 @@
# app/main.py
from fastapi import FastAPI
from api import webhook
import uvicorn
from services.webhook_service import scheduler # <-- Importar o scheduler
app = FastAPI(
title="Consultme WhatsApp API",
description="API para integração com o WhatsApp Cloud API para a Consultme.",
version="0.0.1",
)
# Inclui as rotas definidas em app/api/webhook.py
app.include_router(webhook.router)
# Rota de teste simples para verificar se a API está no ar (opcional)
@app.get("/")
async def root():
return {"message": "API do WhatsApp Consultme está rodando!"}
# --- Eventos de Startup e Shutdown do FastAPI para o Scheduler ---
@app.on_event("startup")
async def startup_event():
if not scheduler.running:
scheduler.start()
print("INFO: APScheduler iniciado na inicialização da aplicação.")
@app.on_event("shutdown")
async def shutdown_event():
if scheduler.running:
scheduler.shutdown()
print("INFO: APScheduler encerrado no desligamento da aplicação.")
# --- FIM DOS EVENTOS DO SCHEDULER ---
# Exemplo de uso de Uvicorn para rodar o FastAPI
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=5000)

Binary file not shown.

View File

@ -0,0 +1,75 @@
from typing import List, Optional, Union
from pydantic import BaseModel, Field
class TextMessage(BaseModel):
body: str
class ButtonMessage(BaseModel):
# O WhatsApp envia 'id' e 'title' para as respostas de botões,
# mas o Pydantic pode esperar 'payload' e 'text' se for para envio.
# Para *recebimento*, precisamos mapear o que vem.
# O payload que você recebeu mostra 'id' e 'title'.
# Para a *resposta* de um botão:
id: str # O ID do botão clicado (que você definiu no seu código)
title: str # O texto do botão clicado
# No seu handle_button_response, você espera 'payload'.
# Isso indica que ButtonMessage foi usado para ENVIO antes.
# Para *recebimento*, a Meta envia 'id' e 'title'.
# A melhor prática é ter dois modelos, um para envio e um para recebimento,
# ou usar aliases condicionais.
# Pela sua mensagem de erro, o Pydantic (na validação do WebhookEvent)
# esperava 'payload' e 'text' para o `button_reply`.
# Vamos renomear e usar aliases para compatibilidade com o que a Meta envia E o que o seu handler espera.
# O `id` do botão clicado é o `payload` que você espera no seu handler.
# O `title` do botão clicado é o `text` que você espera.
# Modelagem para RECEBIMENTO de resposta de botão:
payload: str = Field(alias="id") # Mapeia 'id' do JSON recebido para 'payload' no Python
text: str = Field(alias="title") # Mapeia 'title' do JSON recebido para 'text' no Python
class ListReply(BaseModel):
id: str
title: str
class InteractiveMessage(BaseModel):
type: str # 'list_reply', 'button_reply', etc.
list_reply: Optional[ListReply] = None
button_reply: Optional[ButtonMessage] = None # Renomeado para button_reply para consistência
class Message(BaseModel): # ESTA É A CLASSE 'Message' QUE ESTAVA FALTANDO!
from_: str = Field(alias="from") # 'from' é palavra reservada em Python, então usamos alias
id: str
timestamp: str
type: str # 'text', 'image', 'button', 'interactive', etc.
text: Optional[TextMessage] = None
button: Optional[ButtonMessage] = None # <-- AQUI! Garanta que está usando o ButtonMessage ajustado
interactive: Optional[InteractiveMessage] = None
# Adicione outros tipos de mensagem aqui se precisar processá-los (image, audio, video, etc.)
class Status(BaseModel):
id: str
status: str # 'sent', 'delivered', 'read', 'failed'
timestamp: str
recipient_id: str
# Adicione outros campos de status se necessário
class ChangeValue(BaseModel):
messaging_product: str
metadata: dict # Pode conter phone_number_id, display_phone_number
messages: Optional[List[Message]] = None
statuses: Optional[List[Status]] = None # Para atualizações de status
class Change(BaseModel):
field: str # 'messages' ou 'statuses'
value: ChangeValue
class Entry(BaseModel):
id: str
changes: List[Change]
class WebhookEvent(BaseModel): # Este é o seu modelo principal do evento
object: str # 'whatsapp_business_account'
entry: List[Entry]

0
app/services/__init__.py Normal file
View File

Binary file not shown.

View File

@ -0,0 +1,517 @@
import httpx
from typing import List, Dict, Any, Optional
import json
import base64
import os
# ... (imports existentes no topo) ...
from datetime import datetime, timedelta
import asyncio # <-- NOVO: Para agendamento assíncrono
from apscheduler.schedulers.asyncio import AsyncIOScheduler # <-- NOVO: Agendador
# --- NOVO: Scheduler para agendar a limpeza de sessões ---
scheduler = AsyncIOScheduler()
# Iniciar o scheduler quando o aplicativo iniciar (isso será feito no main.py ou api/webhook.py)
# ... (carregamento de chaves e outras funções) ...
# --- Variáveis Globais para Gerenciamento de Sessão (APENAS PARA TESTE) ---
# Em produção, isso seria um banco de dados
ACTIVE_SESSIONS = {} # Dicionário para armazenar sessões ativas
SESSION_TIMEOUT_SECONDS = 120 # 5 minutos (5 * 60 segundos)
# --- FIM DAS VARIÁVEIS GLOBAIS DE SESSÃO ---
# Importações para criptografia
from cryptography.hazmat.primitives.asymmetric import padding # <-- Esta linha está correta!
from cryptography.hazmat.primitives import hashes # <-- Esta importação está correta!
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key
from cryptography.hazmat.backends import default_backend
from config import WHATSAPP_ACCESS_TOKEN, WHATSAPP_PHONE_NUMBER_ID, VERIFY_TOKEN, FLOW_PRIVATE_KEY_PASSWORD # <-- ADICIONADO
# Importe Message para type hinting. Certifique-se que webhook_model.py está correto primeiro!
from models.webhook_model import Message, TextMessage, ButtonMessage, InteractiveMessage, ListReply # Adicione os modelos necessários
FLOW_PRIVATE_KEY_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../private.pem')
FLOW_PRIVATE_KEY = None # Garante que a variável começa como None antes do try/except
# --- NOVOS PRINTS PARA DEPURACAO DA CHAVE PRIVADA ---
print("\n--- INICIANDO DEPURACAO DE CARREGAMENTO DE CHAVE PRIVADA DO FLOW ---")
print(f"DEBUG: Caminho configurado para a chave privada: {FLOW_PRIVATE_KEY_PATH}")
# Verifica se o arquivo existe antes de tentar abrir
if not os.path.exists(FLOW_PRIVATE_KEY_PATH):
print(f"DEBUG: ATENÇÃO: Arquivo da chave privada NÃO ENCONTRADO em {FLOW_PRIVATE_KEY_PATH} antes de tentar carregar.")
else:
print(f"DEBUG: Arquivo da chave privada ENCONTRADO em {FLOW_PRIVATE_KEY_PATH}. Tentando ler...")
try:
# Tenta ler o conteúdo do arquivo para ter certeza que é acessível e não vazio
with open(FLOW_PRIVATE_KEY_PATH, "rb") as key_file_check:
key_content_check = key_file_check.read(100) # Lê apenas os primeiros 100 bytes
print(f"DEBUG: Primeiros 100 bytes do arquivo da chave (preview): {key_content_check!r}...")
if not key_content_check:
print("DEBUG: ATENÇÃO: Arquivo da chave privada está VAZIO.")
except Exception as e_check:
print(f"DEBUG: ERRO ao tentar PRÉ-LER o arquivo da chave: {type(e_check).__name__}: {e_check}")
print(f"DEBUG: Valor de FLOW_PRIVATE_KEY_PASSWORD (carregado do .env/config): '{FLOW_PRIVATE_KEY_PASSWORD}' (Tipo: {type(FLOW_PRIVATE_KEY_PASSWORD).__name__})")
# --- FIM DOS PRINTS DE DEPURACAO DA CHAVE PRIVADA ---
try:
with open(FLOW_PRIVATE_KEY_PATH, "rb") as key_file:
key_content = key_file.read()
# CONDIÇÃO CRÍTICA: Use a senha se ela existir, ou None se a chave não tiver senha
password_to_use = FLOW_PRIVATE_KEY_PASSWORD.encode('utf-8') if FLOW_PRIVATE_KEY_PASSWORD else None
print(f"DEBUG: Senha que será usada para carregar a chave: {'(presente)' if password_to_use else '(ausente/None)'}")
FLOW_PRIVATE_KEY = load_pem_private_key(
key_content,
password=password_to_use, # <<< ALTERADO PARA USAR A SENHA
backend=default_backend()
)
print(f"✅ Chave privada do Flow carregada de: {FLOW_PRIVATE_KEY_PATH}")
except FileNotFoundError:
print(f"❌ ERRO: Chave privada do Flow não encontrada em {FLOW_PRIVATE_KEY_PATH}. Verifique o caminho e as permissões.")
FLOW_PRIVATE_KEY = None
except Exception as e:
# A exceção agora provavelmente será relacionada à senha se o arquivo for encontrado
# OU se o formato da chave está errado para o password=None/password='suasenha'
print(f"❌ ERRO ao carregar chave privada do Flow: {type(e).__name__}: {e}. Verifique se a senha está correta ou se a chave não tem senha. Conteúdo da chave (primeiros 50 bytes): {key_content[:50]!r}...")
FLOW_PRIVATE_KEY = None
WHATSAPP_FLOW_PUBLIC_KEY_META_FOR_ENCRYPTION_OF_AES = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsFw96WUer6yyZLQFkUNS
Vkv++EEKVVVXca3TcCJqR9aaOepiyKuUT0Il8ctdaZHaTFeEQnM82xbaijEATtGv
qmX+zZRnMnRbI63GZm9QM3pnAbs/iXji5PfLbD5AGt8vDUyldlwGu+2e3PHWHPM2
MKR/+yHEEmgXWWkCYzgdhwIbsaGQNFdOXykfVpdQLt8237E1VEDEebEP3GlUagvo
7k+NQQ+tDHNfAeQjcIHiTCPE39hSFdH7W413HDiu/0sX02ATv+QCppovDBboQdL4
OP4nbnvbRn7HHIFIPskpkm5dPfRuDrejb/Q72c1FcuALBAPFpJYCsjUM3JSfa854
IwIDAQAB-----END PUBLIC KEY-----""" # <<< COLOQUE A SUA CHAVE PÚBLICA (DO public_flow_key.pem) AQUI! <<<
try:
WHATSAPP_FLOW_PUBLIC_KEY_META_FOR_ENCRYPTION_OF_AES_OBJECT = load_pem_public_key(
WHATSAPP_FLOW_PUBLIC_KEY_META_FOR_ENCRYPTION_OF_AES.encode('utf-8'),
backend=default_backend()
)
print("✅ Chave pública da Meta (assumida) para criptografia de retorno (Flows) carregada.")
except Exception as e:
print(f"❌ ERRO ao carregar chave pública da Meta para Flow (para criptografia de retorno): {e}")
WHATSAPP_FLOW_PUBLIC_KEY_META_FOR_ENCRYPTION_OF_AES_OBJECT = None
def decrypt_flow_request_data(
encrypted_flow_data_b64: str,
encrypted_aes_key_b64: str,
initial_vector_b64: str
) -> Dict[str, Any]: # << AQUI ESTÁ A MUDANÇA: AGORA RETORNA UM DICIONÁRIO COMPLETO
"""
Descriptografa os dados de requisição recebidos de um Flow do WhatsApp.
Retorna os dados descriptografados, a chave AES e o IV.
"""
if not FLOW_PRIVATE_KEY:
raise ValueError("Chave privada do Flow não carregada. Não é possível descriptografar a requisição.")
flow_data = base64.b64decode(encrypted_flow_data_b64)
iv = base64.b64decode(initial_vector_b64)
encrypted_flow_data_body = flow_data[:-16]
encrypted_flow_data_tag = flow_data[-16:]
encrypted_aes_key = base64.b64decode(encrypted_aes_key_b64)
aes_key = FLOW_PRIVATE_KEY.decrypt(
encrypted_aes_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
decryptor = Cipher(
algorithms.AES(aes_key),
modes.GCM(iv, encrypted_flow_data_tag),
backend=default_backend()
).decryptor()
decrypted_data_bytes = decryptor.update(encrypted_flow_data_body) + decryptor.finalize()
decrypted_data = json.loads(decrypted_data_bytes.decode("utf-8"))
# Retorna os dados, a chave AES e o IV para uso na resposta
return {
"decrypted_payload": decrypted_data,
"aes_key": aes_key,
"initial_vector": iv
}
def encrypt_flow_response_data(response_data: Dict[str, Any], aes_key: bytes, initial_vector: bytes) -> str:
"""
Criptografa os dados de resposta do Flow para enviar ao WhatsApp.
Usa a chave AES e o IV (invertido) recebidos na solicitação original.
Retorna a string completa codificada em Base64.
"""
# Nenhuma chave pública RSA é usada AQUI para criptografar a AES_KEY.
# A AES_KEY e o IV são passados da requisição original.
# 1. Preparar initialization vector para resposta (invertendo bits do IV original)
flipped_iv = bytearray()
for byte in initial_vector:
flipped_iv.append(byte ^ 0xFF) # XOR com 0xFF para inverter bits
# 2. Criptografar o payload de resposta (JSON) usando AES-GCM
plaintext_bytes = json.dumps(response_data).encode('utf-8')
encryptor = Cipher(algorithms.AES(aes_key), modes.GCM(flipped_iv), backend=default_backend()).encryptor()
encrypted_data_bytes = encryptor.update(plaintext_bytes) + encryptor.finalize()
tag = encryptor.tag # Tag de autenticação do GCM
# 3. CONSTRUIR A STRING FINAL Base64:
# A Meta espera o IV e o TAG concatenados ao final dos dados criptografados,
# e então TUDO isso é Base64 codificado. O exemplo Django mostrou isso implicitamente.
# Payload final em bytes: encrypted_data_bytes + flipped_iv + tag
# ATENÇÃO: O exemplo Django concatena o IV no payload final, mas a documentação
# da Meta para AES-GCM geralmente não o faz, ela espera que o IV seja separado no cabeçalho.
# No entanto, a documentação que você me passou disse "append authentication tag
# generated during encryption to the end of the encryption result; encode the whole output as base64 string".
#
# Vamos seguir o exemplo Django para a resposta. O exemplo Django para `encrypt_response`
# retorna base64(encryptor.update(...) + encryptor.finalize() + encryptor.tag).
# Ele NÃO inclui a chave AES criptografada aqui, porque a Meta já tem a chave AES.
# O retorno deve ser SOMENTE o dado criptografado + tag, Base64 codificado.
# O IV e a chave AES são gerenciados pela Meta.
# A ÚLTIMA TENTATIVA DA METAMORA: Retornar a STRING Base64 pura dos dados criptografados + tag
# (sem o JSON aninhado que estávamos construindo)
# A resposta final é apenas a CONCATENAÇÃO dos dados criptografados e o tag, codificado em Base64.
final_payload_bytes = encrypted_data_bytes + tag # Dados criptografados + Tag
base64_encoded_final_response = base64.b64encode(final_payload_bytes).decode('utf-8')
return base64_encoded_final_response
# --- Funções de Gerenciamento de Sessão ---
def get_session_state(sender_id: str) -> Optional[Dict[str, Any]]:
return ACTIVE_SESSIONS.get(sender_id)
async def clean_session_and_notify(sender_id: str): # <-- FUNÇÃO ASSÍNCRONA PARA LIMPAR E NOTIFICAR
"""
Função assíncrona que limpa a sessão e envia a mensagem de timeout.
Chamada pelo scheduler.
"""
if sender_id in ACTIVE_SESSIONS: # Verifica se a sessão ainda está ativa (não foi atualizada antes do timeout)
timeout_message = "Sua sessão foi encerrada por inatividade. Por favor, envie uma nova mensagem para iniciar um novo atendimento. 😊"
print(f"DEBUG_SESSION: Enviando mensagem de timeout para {sender_id}.")
await send_text_message(sender_id, timeout_message) # <-- CHAMA A FUNÇÃO DE ENVIO
del ACTIVE_SESSIONS[sender_id]
print(f"DEBUG_SESSION: Sessão para {sender_id} encerrada/limpa pelo agendador.")
else:
print(f"DEBUG_SESSION: Sessão para {sender_id} já limpa ou atualizada antes do agendamento.")
def start_or_update_session(sender_id: str):
"""
Inicia uma nova sessão para o usuário ou atualiza o timestamp da última atividade.
Agenda ou reagenda a tarefa de limpeza.
"""
current_time = datetime.now()
ACTIVE_SESSIONS[sender_id] = {
"last_activity_time": current_time,
"current_state": "INICIO" # Ou qualquer estado inicial padrão
}
print(f"DEBUG_SESSION: Sessão para {sender_id} iniciada/atualizada em {current_time.strftime('%Y-%m-%d %H:%M:%S')}.")
# Remover tarefa agendada anterior para esta sessão, se houver
job_id = f"session_clean_{sender_id}"
if scheduler.get_job(job_id):
scheduler.remove_job(job_id)
print(f"DEBUG_SESSION: Tarefa de limpeza anterior para {sender_id} cancelada.")
# Agendar nova tarefa de limpeza para esta sessão
scheduler.add_job(
clean_session_and_notify,
'date', # Agendamento único para uma data/hora específica
run_date=current_time + timedelta(seconds=SESSION_TIMEOUT_SECONDS),
args=[sender_id],
id=job_id,
replace_existing=True # Para garantir que não haja duplicatas, embora remove_job já ajude
)
print(f"DEBUG_SESSION: Tarefa de limpeza para {sender_id} agendada para {current_time + timedelta(seconds=SESSION_TIMEOUT_SECONDS)}.")
# --- Lógicas de Tratamento de Mensagens Recebidas (Funções Auxiliares) ---
# Estas funções contêm a lógica de como seu bot irá interagir.
# Esta função precisa ser definida no arquivo!
async def handle_message_type(message: Message):
sender_id = message.from_
# 1. Iniciar ou atualizar a sessão para o remetente atual
# Isso vai automaticamente agendar/reagendar a limpeza.
start_or_update_session(sender_id) # Esta função agora agenda a limpeza!
# 2. Recuperar o estado da sessão (se houver, para uso futuro com grafos)
session_data = get_session_state(sender_id)
if session_data:
print(f"DEBUG_SESSION: Estado atual da sessão para {sender_id}: {session_data['current_state']}")
else:
# Isso não deveria acontecer se start_or_update_session funcionou
print(f"DEBUG_SESSION: Sessão para {sender_id} não encontrada após start_or_update_session (possível erro).")
if message.type == 'text' and message.text:
await handle_text_message(sender_id, message.text.body) # Passe sender_id
elif message.type == 'button' and message.button:
await handle_button_response(sender_id, message.button.payload) # Passe sender_id
elif message.type == 'interactive' and message.interactive:
if message.interactive.type == 'list_reply' and message.interactive.list_reply:
await handle_list_response(sender_id, message.interactive.list_reply.id, message.interactive.list_reply.title) # Passe sender_id
elif message.interactive.type == 'button_reply' and message.interactive.button_reply:
await handle_button_response(sender_id, message.interactive.button_reply.payload) # Passe sender_id
else:
print(f" Tipo interativo desconhecido recebido: {message.interactive.type}")
await send_text_message(sender_id, "Desculpe, não entendi essa interação interativa.")
elif message.type == 'image':
print(' Recebi uma imagem!')
await send_text_message(sender_id, "Que legal! Recebi sua imagem. No momento, só consigo processar texto e interações.")
else:
print(f" Tipo de mensagem não suportado: {message.type}")
await send_text_message(sender_id, "Desculpe, não entendi o tipo de mensagem que você enviou.")
async def handle_text_message(sender_id: str, text: str):
lower_text = text.lower()
if lower_text != "" :
await send_text_message(sender_id, "Olá! Você iniciou o Consultme. Escolha uma das opções de consulta no menu abaixo:")
await send_interactive_menu(sender_id) # Chama a função para enviar um menu interativo
elif 'menu' in lower_text:
await send_interactive_menu(sender_id)
elif 'cadastro' in lower_text:
await send_text_message(sender_id, "Para iniciar seu cadastro, por favor, me diga seu nome completo:")
# Implemente lógica para salvar o estado do usuário aqui (ex: em um DB)
elif 'ajuda' in lower_text:
await send_text_message(sender_id, "Posso te guiar com as funcionalidades principais. Escolha uma opção do menu ou digite uma pergunta.")
await send_interactive_menu(sender_id)
else:
await send_text_message(sender_id, f"Recebi sua mensagem: \"{text}\". Parece que não entendi bem. Você pode digitar 'menu' para ver as opções disponíveis ou 'ajuda'.")
async def handle_button_response(sender_id: str, payload: str):
response_text = ''
if payload == 'OPTION_AGENDAR':
response_text = "Certo! Para agendar um serviço, qual serviço você precisa e a data/hora preferida?"
elif payload == 'OPTION_STATUS':
response_text = "Para verificar o status de seu pedido, informe o número do seu pedido."
elif payload == 'OPTION_FALAR_ATENDENTE':
response_text = "Encaminhando você para um de nossos atendentes. Aguarde, por favor."
else:
response_text = "Não entendi a opção de botão selecionada. Tente novamente ou digite 'menu'."
await send_text_message(sender_id, response_text)
async def handle_list_response(sender_id: str, list_id: str, list_title: str):
response_text = ''
if list_id == 'item_reparo_geral':
response_text = f"Você selecionou \"{list_title}\". Qual o problema específico que você precisa de reparo?"
elif list_id == 'item_instalacao':
response_text = f"Você selecionou \"{list_title}\". Qual tipo de instalação você precisa?"
elif list_id == 'item_duvidas':
response_text = f"Você selecionou \"{list_title}\". Por favor, digite sua pergunta."
elif list_id == 'item_reclamacoes':
response_text = f"Você selecionou \"{list_title}\". Por favor, descreva o problema em detalhes."
else:
response_text = "Opção de lista não reconhecida. Tente novamente ou digite 'menu'."
await send_text_message(sender_id, response_text)
# --- Funções de Envio de Mensagens para o WhatsApp ---
# Estas funções fazem as chamadas à API da Meta para enviar mensagens.
async def send_text_message(to: str, text: str):
url = f"https://graph.facebook.com/v23.0/{WHATSAPP_PHONE_NUMBER_ID}/messages"
headers = {
'Authorization': f'Bearer {WHATSAPP_ACCESS_TOKEN}',
'Content-Type': 'application/json'
}
payload = {
"messaging_product": "whatsapp",
"to": to,
"type": "text",
"text": {
"preview_url": False,
"body": text
}
}
async with httpx.AsyncClient() as client:
try:
response = await client.post(url, headers=headers, json=payload)
response.raise_for_status()
print("✅ Mensagem de texto enviada com sucesso:", response.json())
except httpx.HTTPStatusError as e:
print(f"❌ Erro HTTP ao enviar mensagem de texto: {e.response.status_code} - {e.response.text}")
except httpx.RequestError as e:
print(f"❌ Erro de rede ao enviar mensagem de texto: {e}")
async def send_interactive_buttons(to: str, header_text: str, body_text: str, buttons_array: List[Dict[str, str]]):
url = f"https://graph.facebook.com/v23.0/{WHATSAPP_PHONE_NUMBER_ID}/messages"
headers = {
'Authorization': f'Bearer {WHATSAPP_ACCESS_TOKEN}',
'Content-Type': 'application/json'
}
payload = {
"messaging_product": "whatsapp",
"to": to,
"type": "interactive",
"interactive": {
"type": "button",
"header": { "type": "text", "text": header_text },
"body": { "text": body_text },
"action": {
"buttons": [{"type": "reply", "reply": {"id": btn["id"], "title": btn["title"]}} for btn in buttons_array]
}
}
}
async with httpx.AsyncClient() as client:
try:
response = await client.post(url, headers=headers, json=payload)
response.raise_for_status()
print("✅ Mensagem com botões enviada com sucesso:", response.json())
except httpx.HTTPStatusError as e:
print(f"❌ Erro HTTP ao enviar mensagem com botões: {e.response.status_code} - {e.response.text}")
except httpx.RequestError as e:
print(f"❌ Erro de rede ao enviar mensagem com botões: {e}")
async def send_interactive_list(to: str, header_text: str, body_text: str, button_title: str, sections_array: List[Dict[str, Any]]):
url = f"https://graph.facebook.com/v23.0/{WHATSAPP_PHONE_NUMBER_ID}/messages"
headers = {
'Authorization': f'Bearer {WHATSAPP_ACCESS_TOKEN}',
'Content-Type': 'application/json'
}
payload = {
"messaging_product": "whatsapp",
"to": to,
"type": "interactive",
"interactive": {
"type": "list",
"header": { "type": "text", "text": header_text },
"body": { "text": body_text },
"action": {
"button": button_title,
"sections": [
{
"title": section["title"],
"rows": [{"id": row["id"], "title": row["title"], "description": row.get("description", "")} for row in section["rows"]]
} for section in sections_array
]
}
}
}
async with httpx.AsyncClient() as client:
try:
response = await client.post(url, headers=headers, json=payload)
response.raise_for_status()
print("✅ Mensagem de lista enviada com sucesso:", response.json())
except httpx.HTTPStatusError as e:
print(f"❌ Erro HTTP ao enviar mensagem de lista: {e.response.status_code} - {e.response.text}")
except httpx.RequestError as e:
print(f"❌ Erro de rede ao enviar mensagem de lista: {e}")
async def mark_message_as_read(message_id: str):
url = f"https://graph.facebook.com/v23.0/{WHATSAPP_PHONE_NUMBER_ID}/messages"
headers = {
'Authorization': f'Bearer {WHATSAPP_ACCESS_TOKEN}',
'Content-Type': 'application/json'
}
payload = {
"messaging_product": "whatsapp",
"status": "read",
"message_id": message_id
}
async with httpx.AsyncClient() as client:
try:
response = await client.post(url, headers=headers, json=payload)
response.raise_for_status()
except httpx.HTTPStatusError as e:
print(f"❌ Erro HTTP ao marcar mensagem {message_id} como lida: {e.response.status_code} - {e.response.text}")
except httpx.RequestError as e:
print(f"❌ Erro de rede ao marcar mensagem {message_id} como lida: {e}")
# --- Função de Exemplo para Enviar um Menu Interativo ---
async def send_interactive_menu(to: str):
buttons = [
{"id": 'OPTION_AGENDAR', "title": 'Total do CP'},
{"id": 'OPTION_STATUS', "title": 'Total das suas Lojas'},
{"id": 'OPTION_FALAR_ATENDENTE', "title": 'De uma Loja'}
]
await send_interactive_buttons(to, "Menu Principal", "Escolha a Dimensão das Lojas que você quer visulizar o indicador:", buttons)
async def send_whatsapp_flow(to: str, flow_id: str, flow_cta: str, screen: str = "welcome_screen") -> Dict[str, Any]:
url = f"https://graph.facebook.com/v18.0/{WHATSAPP_PHONE_NUMBER_ID}/messages"
headers = {
'Authorization': f'Bearer {WHATSAPP_ACCESS_TOKEN}',
'Content-Type': 'application/json'
}
payload = {
"recipient_type": "individual", # <-- ADICIONADO: Conforme a documentação
"messaging_product": "whatsapp",
"to": to,
"type": "interactive",
"interactive": {
"type": "flow",
"header": {
"type": "text",
"text": "Preencha seu Cadastro"
},
"body": {
"text": "Clique no botão abaixo para preencher o formulário de cadastro."
},
"footer": {
"text": "Consultme - Seu parceiro digital"
},
"action": {
"name": "flow",
"parameters": {
"flow_id": flow_id, # Usando flow_id (geralmente melhor que flow_name)
"flow_cta": flow_cta,
# --- ATENÇÃO: MUDAR PARA flow_message_version: "3" (STRING LITERAL "3") ---
# Já que o exemplo da documentação é v18.0 com "3", vamos seguir esse exato
"flow_message_version": "3" # <--- ALTERE ESTA LINHA PARA "3"
}
}
}
}
# ... (restante da função, incluindo DEBUG PRINTS e try/except) ...
print(f"DEBUG_FLOW: Payload a ser enviado para o Flow API: {json.dumps(payload, indent=2)}") # Verifique o payload final no log
async with httpx.AsyncClient() as client:
try:
response = await client.post(url, headers=headers, json=payload)
response.raise_for_status()
print("✅ Flow enviado com sucesso:", response.json())
return {"status": "success", "data": response.json()}
except httpx.HTTPStatusError as e:
print(f"❌ ERRO HTTP ao enviar Flow (Status: {e.response.status_code}, Detalhe: {e.response.text}): {e}")
# print(f"DEBUG_FLOW_ERROR_RESPONSE: {e.response.json()}") # Descomente para ver o erro JSON completo da Meta
return {"status": "error", "message": e.response.text, "code": e.response.status_code}
except httpx.RequestError as e:
print(f"❌ ERRO de rede ao enviar Flow: {e}")
return {"status": "error", "message": str(e), "code": 500}
except Exception as e:
print(f"❌ ERRO INESPERADO ao enviar Flow: {type(e).__name__}: {e}")
return {"status": "error", "message": f"Erro inesperado: {str(e)}", "code": 500}
# ... (send_interactive_menu, handle_button_response, etc.) .

0
app/utils.py Normal file
View File

5
public_key_signature.b64 Normal file
View File

@ -0,0 +1,5 @@
wbBGvLs9B3dikdCk5SbswbhEby+maurnJSp4+GZ4MvdRdqYthDUUOqGxS45AkRl3i4JvgfNh/BxS
v0kem0URXcfaipGaNAlTTJB72gOkSjOnx+pG9o3vYdzGem2aqvXHDfiVmd0kGZcYU/3uk4GVyMRB
R6CWRhgSSfjiSYr5VWqLoVVZmwSDc0V/WeFxsSMbHfDI46Y8qC4CcsyOPEBe4QGucoWu3JyrG+pg
erC36NRduE4miTJUZk6QTYf6mIi2vZ6ST91w98A/2FAz69yvRER+lkv7mETQoPwRWo+pdUy0QuaF
Vj6wbUZMo9cGcXBj3/hKdTOHqeDVFfmsX/dBtQ==

1
public_key_signature.bin Normal file
View File

@ -0,0 +1 @@
Á°F¼»=wbФå&ìÁ¸Do/¦jêç%*xøfx2÷Qv¦-„5:¡±KŽ@wo<E2809A>óaüR¿IE]ÇÚŠš4 SL<53>¤J3§ÇêFö<46>ïaÜÆzmšªõÇ ø•™Ý$Sýî“<C3AE>•ÈÄAG FIøâIŠùUj¡UYƒsEYáq±#ðÈã¦<¨.rÌŽ<@^á®r…®Üœ«ê`z°·èÔ]¸N&‰2TfN<66>M‡ú˜ˆ¶½žOÝp÷À?ØP3ëܯDD~˜ üZ<>©uL´Bæ…V>°mFL£×qpcßøJu3‡©àÕù¬_÷Aµ

9
requirements.txt Normal file
View File

@ -0,0 +1,9 @@
fastapi==0.95.1
uvicorn==0.22.0
pydantic==1.10.4
python-dotenv==0.21.0 # Para carregar variáveis de ambiente do .env
requests==2.28.1 # Para fazer chamadas HTTP (caso precise de integração com outras APIs)
pytest==7.2.2 # Para testes
httpx==0.27.0
cryptography
apscheduler

53
sign.py Normal file
View File

@ -0,0 +1,53 @@
import base64
import requests
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import serialization
# Caminhos dos arquivos
private_key_path = "/home/joaomonezi/Consultme/private_key.pem"
public_key_path = "public_key.pem"
# 1. Carregar chave privada do negócio
with open(private_key_path, "rb") as key_file:
private_key = serialization.load_pem_private_key(
key_file.read(),
password=None, # coloque a senha aqui se sua chave for protegida
)
# 2. Ler conteúdo da chave pública do endpoint (que será assinada)
with open(public_key_path, "rb") as f:
public_key_data = f.read()
# 3. Assinar a chave pública com a chave privada
signature = private_key.sign(
public_key_data,
padding.PKCS1v15(),
hashes.SHA256()
)
# 4. Codificar assinatura em base64 para enviar na API
signature_b64 = base64.b64encode(signature).decode()
# 5. Chave pública em string para enviar (PEM format)
public_key_str = public_key_data.decode()
# 6. Montar JSON para upload
payload = {
"public_key": public_key_str,
"signature": signature_b64
}
# 7. Fazer upload via API (exemplo genérico, ajuste URL e headers)
api_url = "https://consultme.grupoginseng.com.br/v1/business/public_key"
headers = {
"Authorization": "EAAe3eozOvZCMBO5vngFSqhg823ohKbusZCz2kbkjsrOZAGcDOOnsXQcK17KGu8Gy6oANKHdpT0ZAIJ7422VtZClrdcGNugMXratBvct2xbyo1MuBHZAowFuJq2wsWYAXoR6jypNTuXsHcYwfTeojY5AXsBTOILQEmeqZClzZAvFL7ZBhf51Pku6P2TMAjFdPzeovDZCWtdTbfB4MNTOkDlxkwjSuDxl8ktxoCXH8C52p88azU2casUEqQZD",
"Content-Type": "application/json"
}
response = requests.post(api_url, json=payload, headers=headers)
if response.status_code == 200:
print("Chave pública assinada enviada com sucesso!")
else:
print(f"Erro no upload: {response.status_code} - {response.text}")

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

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

70
venv/bin/activate Normal file
View File

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

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

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

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

@ -0,0 +1,69 @@
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
# (https://fishshell.com/). You cannot run it directly.
function deactivate -d "Exit virtual environment and return to normal shell environment"
# reset old environment variables
if test -n "$_OLD_VIRTUAL_PATH"
set -gx PATH $_OLD_VIRTUAL_PATH
set -e _OLD_VIRTUAL_PATH
end
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
set -e _OLD_VIRTUAL_PYTHONHOME
end
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
set -e _OLD_FISH_PROMPT_OVERRIDE
# prevents error when using nested fish instances (Issue #93858)
if functions -q _old_fish_prompt
functions -e fish_prompt
functions -c _old_fish_prompt fish_prompt
functions -e _old_fish_prompt
end
end
set -e VIRTUAL_ENV
set -e VIRTUAL_ENV_PROMPT
if test "$argv[1]" != "nondestructive"
# Self-destruct!
functions -e deactivate
end
end
# Unset irrelevant variables.
deactivate nondestructive
set -gx VIRTUAL_ENV "/home/joaomonezi/Consultme/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

8
venv/bin/dotenv Executable file
View File

@ -0,0 +1,8 @@
#!/home/joaomonezi/Consultme/venv/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from dotenv.__main__ import cli
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(cli())

8
venv/bin/httpx Executable file
View File

@ -0,0 +1,8 @@
#!/home/joaomonezi/Consultme/venv/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from httpx import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
venv/bin/normalizer Executable file
View File

@ -0,0 +1,8 @@
#!/home/joaomonezi/Consultme/venv/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from charset_normalizer.cli.normalizer import cli_detect
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(cli_detect())

8
venv/bin/pip Executable file
View File

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

8
venv/bin/pip3 Executable file
View File

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

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

@ -0,0 +1,8 @@
#!/home/joaomonezi/Consultme/venv/bin/python
# -*- 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/py.test Executable file
View File

@ -0,0 +1,8 @@
#!/home/joaomonezi/Consultme/venv/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from pytest import console_main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(console_main())

8
venv/bin/pytest Executable file
View File

@ -0,0 +1,8 @@
#!/home/joaomonezi/Consultme/venv/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from pytest import console_main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(console_main())

1
venv/bin/python Symbolic link
View File

@ -0,0 +1 @@
/home/joaomonezi/.pyenv/versions/3.12.2/bin/python

1
venv/bin/python3 Symbolic link
View File

@ -0,0 +1 @@
python

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

@ -0,0 +1 @@
python

8
venv/bin/uvicorn Executable file
View File

@ -0,0 +1,8 @@
#!/home/joaomonezi/Consultme/venv/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from uvicorn.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

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

View File

@ -0,0 +1,147 @@
Metadata-Version: 2.1
Name: APScheduler
Version: 3.11.0
Summary: In-process task scheduler with Cron-like capabilities
Author-email: Alex Grönholm <alex.gronholm@nextday.fi>
License: MIT
Project-URL: Documentation, https://apscheduler.readthedocs.io/en/3.x/
Project-URL: Changelog, https://apscheduler.readthedocs.io/en/3.x/versionhistory.html
Project-URL: Source code, https://github.com/agronholm/apscheduler
Project-URL: Issue tracker, https://github.com/agronholm/apscheduler/issues
Keywords: scheduling,cron
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.8
Description-Content-Type: text/x-rst
License-File: LICENSE.txt
Requires-Dist: tzlocal>=3.0
Requires-Dist: backports.zoneinfo; python_version < "3.9"
Provides-Extra: etcd
Requires-Dist: etcd3; extra == "etcd"
Requires-Dist: protobuf<=3.21.0; extra == "etcd"
Provides-Extra: gevent
Requires-Dist: gevent; extra == "gevent"
Provides-Extra: mongodb
Requires-Dist: pymongo>=3.0; extra == "mongodb"
Provides-Extra: redis
Requires-Dist: redis>=3.0; extra == "redis"
Provides-Extra: rethinkdb
Requires-Dist: rethinkdb>=2.4.0; extra == "rethinkdb"
Provides-Extra: sqlalchemy
Requires-Dist: sqlalchemy>=1.4; extra == "sqlalchemy"
Provides-Extra: tornado
Requires-Dist: tornado>=4.3; extra == "tornado"
Provides-Extra: twisted
Requires-Dist: twisted; extra == "twisted"
Provides-Extra: zookeeper
Requires-Dist: kazoo; extra == "zookeeper"
Provides-Extra: test
Requires-Dist: APScheduler[etcd,mongodb,redis,rethinkdb,sqlalchemy,tornado,zookeeper]; extra == "test"
Requires-Dist: pytest; extra == "test"
Requires-Dist: anyio>=4.5.2; extra == "test"
Requires-Dist: PySide6; (platform_python_implementation == "CPython" and python_version < "3.14") and extra == "test"
Requires-Dist: gevent; python_version < "3.14" and extra == "test"
Requires-Dist: pytz; extra == "test"
Requires-Dist: twisted; python_version < "3.14" and extra == "test"
Provides-Extra: doc
Requires-Dist: packaging; extra == "doc"
Requires-Dist: sphinx; extra == "doc"
Requires-Dist: sphinx-rtd-theme>=1.3.0; extra == "doc"
.. image:: https://github.com/agronholm/apscheduler/workflows/Python%20codeqa/test/badge.svg?branch=3.x
:target: https://github.com/agronholm/apscheduler/actions?query=workflow%3A%22Python+codeqa%2Ftest%22+branch%3A3.x
:alt: Build Status
.. image:: https://coveralls.io/repos/github/agronholm/apscheduler/badge.svg?branch=3.x
:target: https://coveralls.io/github/agronholm/apscheduler?branch=3.x
:alt: Code Coverage
.. image:: https://readthedocs.org/projects/apscheduler/badge/?version=3.x
:target: https://apscheduler.readthedocs.io/en/master/?badge=3.x
:alt: Documentation
Advanced Python Scheduler (APScheduler) is a Python library that lets you schedule your Python code
to be executed later, either just once or periodically. You can add new jobs or remove old ones on
the fly as you please. If you store your jobs in a database, they will also survive scheduler
restarts and maintain their state. When the scheduler is restarted, it will then run all the jobs
it should have run while it was offline [#f1]_.
Among other things, APScheduler can be used as a cross-platform, application specific replacement
to platform specific schedulers, such as the cron daemon or the Windows task scheduler. Please
note, however, that APScheduler is **not** a daemon or service itself, nor does it come with any
command line tools. It is primarily meant to be run inside existing applications. That said,
APScheduler does provide some building blocks for you to build a scheduler service or to run a
dedicated scheduler process.
APScheduler has three built-in scheduling systems you can use:
* Cron-style scheduling (with optional start/end times)
* Interval-based execution (runs jobs on even intervals, with optional start/end times)
* One-off delayed execution (runs jobs once, on a set date/time)
You can mix and match scheduling systems and the backends where the jobs are stored any way you
like. Supported backends for storing jobs include:
* Memory
* `SQLAlchemy <http://www.sqlalchemy.org/>`_ (any RDBMS supported by SQLAlchemy works)
* `MongoDB <http://www.mongodb.org/>`_
* `Redis <http://redis.io/>`_
* `RethinkDB <https://www.rethinkdb.com/>`_
* `ZooKeeper <https://zookeeper.apache.org/>`_
* `Etcd <https://etcd.io/>`_
APScheduler also integrates with several common Python frameworks, like:
* `asyncio <http://docs.python.org/3.4/library/asyncio.html>`_ (:pep:`3156`)
* `gevent <http://www.gevent.org/>`_
* `Tornado <http://www.tornadoweb.org/>`_
* `Twisted <http://twistedmatrix.com/>`_
* `Qt <http://qt-project.org/>`_ (using either
`PyQt <http://www.riverbankcomputing.com/software/pyqt/intro>`_ ,
`PySide6 <https://wiki.qt.io/Qt_for_Python>`_ ,
`PySide2 <https://wiki.qt.io/Qt_for_Python>`_ or
`PySide <http://qt-project.org/wiki/PySide>`_)
There are third party solutions for integrating APScheduler with other frameworks:
* `Django <https://github.com/jarekwg/django-apscheduler>`_
* `Flask <https://github.com/viniciuschiele/flask-apscheduler>`_
.. [#f1] The cutoff period for this is also configurable.
Documentation
-------------
Documentation can be found `here <https://apscheduler.readthedocs.io/>`_.
Source
------
The source can be browsed at `Github <https://github.com/agronholm/apscheduler/tree/3.x>`_.
Reporting bugs
--------------
A `bug tracker <https://github.com/agronholm/apscheduler/issues>`_ is provided by Github.
Getting help
------------
If you have problems or other questions, you can either:
* Ask in the `apscheduler <https://gitter.im/apscheduler/Lobby>`_ room on Gitter
* Ask on the `APScheduler GitHub discussion forum <https://github.com/agronholm/apscheduler/discussions>`_, or
* Ask on `StackOverflow <http://stackoverflow.com/questions/tagged/apscheduler>`_ and tag your
question with the ``apscheduler`` tag

View File

@ -0,0 +1,86 @@
APScheduler-3.11.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
APScheduler-3.11.0.dist-info/LICENSE.txt,sha256=YWP3mH37ONa8MgzitwsvArhivEESZRbVUu8c1DJH51g,1130
APScheduler-3.11.0.dist-info/METADATA,sha256=Mve2P3vZbWWDb5V-XfZO80hkih9E6s00Nn5ptU2__9w,6374
APScheduler-3.11.0.dist-info/RECORD,,
APScheduler-3.11.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
APScheduler-3.11.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
APScheduler-3.11.0.dist-info/entry_points.txt,sha256=HSDTxgulLTgymfXK2UNCPP1ib5rlQSFgZJEg72vto3s,1181
APScheduler-3.11.0.dist-info/top_level.txt,sha256=O3oMCWxG-AHkecUoO6Ze7-yYjWrttL95uHO8-RFdYvE,12
apscheduler/__init__.py,sha256=hOpI9oJuk5l5I_VtdsHPous2Qr-ZDX573e7NaYRWFUs,380
apscheduler/__pycache__/__init__.cpython-312.pyc,,
apscheduler/__pycache__/events.cpython-312.pyc,,
apscheduler/__pycache__/job.cpython-312.pyc,,
apscheduler/__pycache__/util.cpython-312.pyc,,
apscheduler/events.py,sha256=W_Wg5aTBXDxXhHtimn93ZEjV3x0ntF-Y0EAVuZPhiXY,3591
apscheduler/executors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
apscheduler/executors/__pycache__/__init__.cpython-312.pyc,,
apscheduler/executors/__pycache__/asyncio.cpython-312.pyc,,
apscheduler/executors/__pycache__/base.cpython-312.pyc,,
apscheduler/executors/__pycache__/debug.cpython-312.pyc,,
apscheduler/executors/__pycache__/gevent.cpython-312.pyc,,
apscheduler/executors/__pycache__/pool.cpython-312.pyc,,
apscheduler/executors/__pycache__/tornado.cpython-312.pyc,,
apscheduler/executors/__pycache__/twisted.cpython-312.pyc,,
apscheduler/executors/asyncio.py,sha256=g0ArcxefoTnEqtyr_IRc-M3dcj0bhuvHcxwRp2s3nDE,1768
apscheduler/executors/base.py,sha256=HErgd8d1g0-BjXnylLcFyoo6GU3wHgW9GJVaFNMV7dI,7116
apscheduler/executors/debug.py,sha256=15_ogSBzl8RRCfBYDnkIV2uMH8cLk1KImYmBa_NVGpc,573
apscheduler/executors/gevent.py,sha256=_ZFpbn7-tH5_lAeL4sxEyPhxyUTtUUSrH8s42EHGQ2w,761
apscheduler/executors/pool.py,sha256=q_shxnvXLjdcwhtKyPvQSYngOjAeKQO8KCvZeb19RSQ,2683
apscheduler/executors/tornado.py,sha256=lb6mshRj7GMLz3d8StwESnlZsAfrNmW78Wokcg__Lk8,1581
apscheduler/executors/twisted.py,sha256=YUEDnaPbP_M0lXCmNAW_yPiLKwbO9vD3KMiBFQ2D4h0,726
apscheduler/job.py,sha256=GzOGMfOM6STwd3HWArVAylO-1Kb0f2qA_PRuXs5LPk4,11153
apscheduler/jobstores/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
apscheduler/jobstores/__pycache__/__init__.cpython-312.pyc,,
apscheduler/jobstores/__pycache__/base.cpython-312.pyc,,
apscheduler/jobstores/__pycache__/etcd.cpython-312.pyc,,
apscheduler/jobstores/__pycache__/memory.cpython-312.pyc,,
apscheduler/jobstores/__pycache__/mongodb.cpython-312.pyc,,
apscheduler/jobstores/__pycache__/redis.cpython-312.pyc,,
apscheduler/jobstores/__pycache__/rethinkdb.cpython-312.pyc,,
apscheduler/jobstores/__pycache__/sqlalchemy.cpython-312.pyc,,
apscheduler/jobstores/__pycache__/zookeeper.cpython-312.pyc,,
apscheduler/jobstores/base.py,sha256=ZDOgMtHLaF3TPUOQwmkBIDcpnHU0aUhtzZOGmMGaJn8,4416
apscheduler/jobstores/etcd.py,sha256=O7C40CGlnn3cPinchJEs2sWcqnzEZQt3c6WnhgPRSdQ,5703
apscheduler/jobstores/memory.py,sha256=HmOs7FbrOoQNywz-yfq2v5esGDHeKE_mvMNFDeGZ31E,3595
apscheduler/jobstores/mongodb.py,sha256=mCIwcKiWcicM2qdAQn51QBEkGlNfbk_73Oi6soShNcM,5319
apscheduler/jobstores/redis.py,sha256=El-H2eUfZjPZca7vwy10B9gZv5RzRucbkDu7Ti07vyM,5482
apscheduler/jobstores/rethinkdb.py,sha256=SdT3jPrhxnmBoL4IClDfHsez4DpREnYEsHndIP8idHA,5922
apscheduler/jobstores/sqlalchemy.py,sha256=2jaq3ZcoXEyIqqvYf3eloaP-_ZAqojt0EuWWvQ2LMRg,6799
apscheduler/jobstores/zookeeper.py,sha256=32bEZNJNniPwmYXBITZ3eSRBq6hipqPKDqh4q4NiZvc,6439
apscheduler/schedulers/__init__.py,sha256=POEy7n3BZgccZ44atMvxj0w5PejN55g-55NduZUZFqQ,406
apscheduler/schedulers/__pycache__/__init__.cpython-312.pyc,,
apscheduler/schedulers/__pycache__/asyncio.cpython-312.pyc,,
apscheduler/schedulers/__pycache__/background.cpython-312.pyc,,
apscheduler/schedulers/__pycache__/base.cpython-312.pyc,,
apscheduler/schedulers/__pycache__/blocking.cpython-312.pyc,,
apscheduler/schedulers/__pycache__/gevent.cpython-312.pyc,,
apscheduler/schedulers/__pycache__/qt.cpython-312.pyc,,
apscheduler/schedulers/__pycache__/tornado.cpython-312.pyc,,
apscheduler/schedulers/__pycache__/twisted.cpython-312.pyc,,
apscheduler/schedulers/asyncio.py,sha256=Jo7tgHP1STnMSxNVAWPSkFpmBLngavivTsG9sF0QoWM,1893
apscheduler/schedulers/background.py,sha256=sRNrikUhpyblvA5RCpKC5Djvf3-b6NHvnXTblxlqIaM,1476
apscheduler/schedulers/base.py,sha256=hvnvcI1DOC9bmvrFk8UiLlGxsXKHtMpEHLDEe63mQ_s,48342
apscheduler/schedulers/blocking.py,sha256=138rf9X1C-ZxWVTVAO_pyfYMBKhkqO2qZqJoyGInv5c,872
apscheduler/schedulers/gevent.py,sha256=zS5nHQUkQMrn0zKOaFnUyiG0fXTE01yE9GXVNCdrd90,987
apscheduler/schedulers/qt.py,sha256=6BHOCi8e6L3wXTWwQDjNl8w_GJF_dY6iiO3gEtCJgmI,1241
apscheduler/schedulers/tornado.py,sha256=dQBQKrTtZLPHuhuzZgrT-laU-estPRWGv9W9kgZETnY,1890
apscheduler/schedulers/twisted.py,sha256=sRkI3hosp-OCLVluR_-wZFCz9auxqqWYauZhtOAoRU4,1778
apscheduler/triggers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
apscheduler/triggers/__pycache__/__init__.cpython-312.pyc,,
apscheduler/triggers/__pycache__/base.cpython-312.pyc,,
apscheduler/triggers/__pycache__/calendarinterval.cpython-312.pyc,,
apscheduler/triggers/__pycache__/combining.cpython-312.pyc,,
apscheduler/triggers/__pycache__/date.cpython-312.pyc,,
apscheduler/triggers/__pycache__/interval.cpython-312.pyc,,
apscheduler/triggers/base.py,sha256=8iKllubaexF456IK9jfi56QTrVIfDDPLavUc8wTlnL0,1333
apscheduler/triggers/calendarinterval.py,sha256=BaH5rbTSVbPk3VhFwA3zORLSuZtYmFudS8GF0YxB51E,7411
apscheduler/triggers/combining.py,sha256=LO0YKgBk8V5YfQ-L3qh8Fb6w0BvNOBghTFeAvZx3_P8,4044
apscheduler/triggers/cron/__init__.py,sha256=ByWq4Q96gUWr4AwKoRRA9BD5ZVBvwQ6BtQMhafdStjw,9753
apscheduler/triggers/cron/__pycache__/__init__.cpython-312.pyc,,
apscheduler/triggers/cron/__pycache__/expressions.cpython-312.pyc,,
apscheduler/triggers/cron/__pycache__/fields.cpython-312.pyc,,
apscheduler/triggers/cron/expressions.py,sha256=89n_HxA0826xBJb8RprVzUDECs0dnZ_rX2wVkVsq6l8,9056
apscheduler/triggers/cron/fields.py,sha256=RVbf6Lcyvg-3CqNzEZsfxzQ_weONCIiq5LGDzA3JUAw,3618
apscheduler/triggers/date.py,sha256=ZS_TMjUCSldqlZsUUjlwvuWeMKeDXqqAMcZVFGYpam4,1698
apscheduler/triggers/interval.py,sha256=u6XLrxlaWA41zvIByQvRLHTAuvkibG2fAZAxrWK3118,4679
apscheduler/util.py,sha256=Lz2ddoeIpufXzW-HWnW5J08ijkXWGElDLVJf0DiPa84,13564

View File

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

View File

@ -0,0 +1,25 @@
[apscheduler.executors]
asyncio = apscheduler.executors.asyncio:AsyncIOExecutor
debug = apscheduler.executors.debug:DebugExecutor
gevent = apscheduler.executors.gevent:GeventExecutor
processpool = apscheduler.executors.pool:ProcessPoolExecutor
threadpool = apscheduler.executors.pool:ThreadPoolExecutor
tornado = apscheduler.executors.tornado:TornadoExecutor
twisted = apscheduler.executors.twisted:TwistedExecutor
[apscheduler.jobstores]
etcd = apscheduler.jobstores.etcd:EtcdJobStore
memory = apscheduler.jobstores.memory:MemoryJobStore
mongodb = apscheduler.jobstores.mongodb:MongoDBJobStore
redis = apscheduler.jobstores.redis:RedisJobStore
rethinkdb = apscheduler.jobstores.rethinkdb:RethinkDBJobStore
sqlalchemy = apscheduler.jobstores.sqlalchemy:SQLAlchemyJobStore
zookeeper = apscheduler.jobstores.zookeeper:ZooKeeperJobStore
[apscheduler.triggers]
and = apscheduler.triggers.combining:AndTrigger
calendarinterval = apscheduler.triggers.calendarinterval:CalendarIntervalTrigger
cron = apscheduler.triggers.cron:CronTrigger
date = apscheduler.triggers.date:DateTrigger
interval = apscheduler.triggers.interval:IntervalTrigger
or = apscheduler.triggers.combining:OrTrigger

View File

@ -0,0 +1 @@
apscheduler

View File

@ -0,0 +1,9 @@
__all__ = ["__version__", "version_tuple"]
try:
from ._version import version as __version__, version_tuple
except ImportError: # pragma: no cover
# broken installation, we don't even try
# unknown only works because we do poor mans version compare
__version__ = "unknown"
version_tuple = (0, 0, "unknown") # type:ignore[assignment]

View File

@ -0,0 +1,116 @@
"""Allow bash-completion for argparse with argcomplete if installed.
Needs argcomplete>=0.5.6 for python 3.2/3.3 (older versions fail
to find the magic string, so _ARGCOMPLETE env. var is never set, and
this does not need special code).
Function try_argcomplete(parser) should be called directly before
the call to ArgumentParser.parse_args().
The filescompleter is what you normally would use on the positional
arguments specification, in order to get "dirname/" after "dirn<TAB>"
instead of the default "dirname ":
optparser.add_argument(Config._file_or_dir, nargs='*').completer=filescompleter
Other, application specific, completers should go in the file
doing the add_argument calls as they need to be specified as .completer
attributes as well. (If argcomplete is not installed, the function the
attribute points to will not be used).
SPEEDUP
=======
The generic argcomplete script for bash-completion
(/etc/bash_completion.d/python-argcomplete.sh)
uses a python program to determine startup script generated by pip.
You can speed up completion somewhat by changing this script to include
# PYTHON_ARGCOMPLETE_OK
so the python-argcomplete-check-easy-install-script does not
need to be called to find the entry point of the code and see if that is
marked with PYTHON_ARGCOMPLETE_OK.
INSTALL/DEBUGGING
=================
To include this support in another application that has setup.py generated
scripts:
- Add the line:
# PYTHON_ARGCOMPLETE_OK
near the top of the main python entry point.
- Include in the file calling parse_args():
from _argcomplete import try_argcomplete, filescompleter
Call try_argcomplete just before parse_args(), and optionally add
filescompleter to the positional arguments' add_argument().
If things do not work right away:
- Switch on argcomplete debugging with (also helpful when doing custom
completers):
export _ARC_DEBUG=1
- Run:
python-argcomplete-check-easy-install-script $(which appname)
echo $?
will echo 0 if the magic line has been found, 1 if not.
- Sometimes it helps to find early on errors using:
_ARGCOMPLETE=1 _ARC_DEBUG=1 appname
which should throw a KeyError: 'COMPLINE' (which is properly set by the
global argcomplete script).
"""
import argparse
import os
import sys
from glob import glob
from typing import Any
from typing import List
from typing import Optional
class FastFilesCompleter:
"""Fast file completer class."""
def __init__(self, directories: bool = True) -> None:
self.directories = directories
def __call__(self, prefix: str, **kwargs: Any) -> List[str]:
# Only called on non option completions.
if os.path.sep in prefix[1:]:
prefix_dir = len(os.path.dirname(prefix) + os.path.sep)
else:
prefix_dir = 0
completion = []
globbed = []
if "*" not in prefix and "?" not in prefix:
# We are on unix, otherwise no bash.
if not prefix or prefix[-1] == os.path.sep:
globbed.extend(glob(prefix + ".*"))
prefix += "*"
globbed.extend(glob(prefix))
for x in sorted(globbed):
if os.path.isdir(x):
x += "/"
# Append stripping the prefix (like bash, not like compgen).
completion.append(x[prefix_dir:])
return completion
if os.environ.get("_ARGCOMPLETE"):
try:
import argcomplete.completers
except ImportError:
sys.exit(-1)
filescompleter: Optional[FastFilesCompleter] = FastFilesCompleter()
def try_argcomplete(parser: argparse.ArgumentParser) -> None:
argcomplete.autocomplete(parser, always_complete_options=False)
else:
def try_argcomplete(parser: argparse.ArgumentParser) -> None:
pass
filescompleter = None

View File

@ -0,0 +1,22 @@
"""Python inspection/code generation API."""
from .code import Code
from .code import ExceptionInfo
from .code import filter_traceback
from .code import Frame
from .code import getfslineno
from .code import Traceback
from .code import TracebackEntry
from .source import getrawcode
from .source import Source
__all__ = [
"Code",
"ExceptionInfo",
"filter_traceback",
"Frame",
"getfslineno",
"getrawcode",
"Traceback",
"TracebackEntry",
"Source",
]

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