Tirando env do commit
This commit is contained in:
parent
e51648489b
commit
5feb9db6c0
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal 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
1
.python-version
Normal file
@ -0,0 +1 @@
|
||||
3.12.2
|
||||
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
BIN
app/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
app/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/config.cpython-312.pyc
Normal file
BIN
app/__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/main.cpython-312.pyc
Normal file
BIN
app/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
0
app/api/__init__.py
Normal file
0
app/api/__init__.py
Normal file
BIN
app/api/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
app/api/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/api/__pycache__/webhook.cpython-312.pyc
Normal file
BIN
app/api/__pycache__/webhook.cpython-312.pyc
Normal file
Binary file not shown.
168
app/api/webhook.py
Normal file
168
app/api/webhook.py
Normal 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
19
app/config.py
Normal 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
38
app/main.py
Normal 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)
|
||||
BIN
app/models/__pycache__/webhook_model.cpython-312.pyc
Normal file
BIN
app/models/__pycache__/webhook_model.cpython-312.pyc
Normal file
Binary file not shown.
75
app/models/webhook_model.py
Normal file
75
app/models/webhook_model.py
Normal 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
0
app/services/__init__.py
Normal file
BIN
app/services/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
app/services/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/services/__pycache__/webhook_service.cpython-312.pyc
Normal file
BIN
app/services/__pycache__/webhook_service.cpython-312.pyc
Normal file
Binary file not shown.
517
app/services/webhook_service.py
Normal file
517
app/services/webhook_service.py
Normal 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
0
app/utils.py
Normal file
5
public_key_signature.b64
Normal file
5
public_key_signature.b64
Normal 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
1
public_key_signature.bin
Normal file
@ -0,0 +1 @@
|
||||
Á°F¼»=wb‘Фå&ìÁ¸Do/¦jêç%*xøfx2÷Qv¦-„5:¡±KŽ@‘w‹‚o<E2809A>óaüR¿I›E]ÇÚŠ‘š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~–Kû˜DÐ üZ<>©uL´Bæ…V>°mFL£×qpcßøJu3‡©àÕù¬_÷Aµ
|
||||
9
requirements.txt
Normal file
9
requirements.txt
Normal 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
53
sign.py
Normal 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
247
venv/bin/Activate.ps1
Normal file
@ -0,0 +1,247 @@
|
||||
<#
|
||||
.Synopsis
|
||||
Activate a Python virtual environment for the current PowerShell session.
|
||||
|
||||
.Description
|
||||
Pushes the python executable for a virtual environment to the front of the
|
||||
$Env:PATH environment variable and sets the prompt to signify that you are
|
||||
in a Python virtual environment. Makes use of the command line switches as
|
||||
well as the `pyvenv.cfg` file values present in the virtual environment.
|
||||
|
||||
.Parameter VenvDir
|
||||
Path to the directory that contains the virtual environment to activate. The
|
||||
default value for this is the parent of the directory that the Activate.ps1
|
||||
script is located within.
|
||||
|
||||
.Parameter Prompt
|
||||
The prompt prefix to display when this virtual environment is activated. By
|
||||
default, this prompt is the name of the virtual environment folder (VenvDir)
|
||||
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
|
||||
|
||||
.Example
|
||||
Activate.ps1
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -Verbose
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||
and shows extra information about the activation as it executes.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
|
||||
Activates the Python virtual environment located in the specified location.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -Prompt "MyPython"
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||
and prefixes the current prompt with the specified string (surrounded in
|
||||
parentheses) while the virtual environment is active.
|
||||
|
||||
.Notes
|
||||
On Windows, it may be required to enable this Activate.ps1 script by setting the
|
||||
execution policy for the user. You can do this by issuing the following PowerShell
|
||||
command:
|
||||
|
||||
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
|
||||
For more information on Execution Policies:
|
||||
https://go.microsoft.com/fwlink/?LinkID=135170
|
||||
|
||||
#>
|
||||
Param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[String]
|
||||
$VenvDir,
|
||||
[Parameter(Mandatory = $false)]
|
||||
[String]
|
||||
$Prompt
|
||||
)
|
||||
|
||||
<# Function declarations --------------------------------------------------- #>
|
||||
|
||||
<#
|
||||
.Synopsis
|
||||
Remove all shell session elements added by the Activate script, including the
|
||||
addition of the virtual environment's Python executable from the beginning of
|
||||
the PATH variable.
|
||||
|
||||
.Parameter NonDestructive
|
||||
If present, do not remove this function from the global namespace for the
|
||||
session.
|
||||
|
||||
#>
|
||||
function global:deactivate ([switch]$NonDestructive) {
|
||||
# Revert to original values
|
||||
|
||||
# The prior prompt:
|
||||
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
|
||||
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
|
||||
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
|
||||
}
|
||||
|
||||
# The prior PYTHONHOME:
|
||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
|
||||
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
|
||||
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
|
||||
}
|
||||
|
||||
# The prior PATH:
|
||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
|
||||
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
|
||||
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
|
||||
}
|
||||
|
||||
# Just remove the VIRTUAL_ENV altogether:
|
||||
if (Test-Path -Path Env:VIRTUAL_ENV) {
|
||||
Remove-Item -Path env:VIRTUAL_ENV
|
||||
}
|
||||
|
||||
# Just remove VIRTUAL_ENV_PROMPT altogether.
|
||||
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
|
||||
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
|
||||
}
|
||||
|
||||
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
|
||||
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
|
||||
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
|
||||
}
|
||||
|
||||
# Leave deactivate function in the global namespace if requested:
|
||||
if (-not $NonDestructive) {
|
||||
Remove-Item -Path function:deactivate
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.Description
|
||||
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
|
||||
given folder, and returns them in a map.
|
||||
|
||||
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
|
||||
two strings separated by `=` (with any amount of whitespace surrounding the =)
|
||||
then it is considered a `key = value` line. The left hand string is the key,
|
||||
the right hand is the value.
|
||||
|
||||
If the value starts with a `'` or a `"` then the first and last character is
|
||||
stripped from the value before being captured.
|
||||
|
||||
.Parameter ConfigDir
|
||||
Path to the directory that contains the `pyvenv.cfg` file.
|
||||
#>
|
||||
function Get-PyVenvConfig(
|
||||
[String]
|
||||
$ConfigDir
|
||||
) {
|
||||
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
|
||||
|
||||
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
|
||||
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
|
||||
|
||||
# An empty map will be returned if no config file is found.
|
||||
$pyvenvConfig = @{ }
|
||||
|
||||
if ($pyvenvConfigPath) {
|
||||
|
||||
Write-Verbose "File exists, parse `key = value` lines"
|
||||
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
|
||||
|
||||
$pyvenvConfigContent | ForEach-Object {
|
||||
$keyval = $PSItem -split "\s*=\s*", 2
|
||||
if ($keyval[0] -and $keyval[1]) {
|
||||
$val = $keyval[1]
|
||||
|
||||
# Remove extraneous quotations around a string value.
|
||||
if ("'""".Contains($val.Substring(0, 1))) {
|
||||
$val = $val.Substring(1, $val.Length - 2)
|
||||
}
|
||||
|
||||
$pyvenvConfig[$keyval[0]] = $val
|
||||
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
|
||||
}
|
||||
}
|
||||
}
|
||||
return $pyvenvConfig
|
||||
}
|
||||
|
||||
|
||||
<# Begin Activate script --------------------------------------------------- #>
|
||||
|
||||
# Determine the containing directory of this script
|
||||
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
$VenvExecDir = Get-Item -Path $VenvExecPath
|
||||
|
||||
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
|
||||
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
|
||||
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
|
||||
|
||||
# Set values required in priority: CmdLine, ConfigFile, Default
|
||||
# First, get the location of the virtual environment, it might not be
|
||||
# VenvExecDir if specified on the command line.
|
||||
if ($VenvDir) {
|
||||
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
|
||||
}
|
||||
else {
|
||||
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
|
||||
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
|
||||
Write-Verbose "VenvDir=$VenvDir"
|
||||
}
|
||||
|
||||
# Next, read the `pyvenv.cfg` file to determine any required value such
|
||||
# as `prompt`.
|
||||
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
|
||||
|
||||
# Next, set the prompt from the command line, or the config file, or
|
||||
# just use the name of the virtual environment folder.
|
||||
if ($Prompt) {
|
||||
Write-Verbose "Prompt specified as argument, using '$Prompt'"
|
||||
}
|
||||
else {
|
||||
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
|
||||
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
|
||||
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
|
||||
$Prompt = $pyvenvCfg['prompt'];
|
||||
}
|
||||
else {
|
||||
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
|
||||
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
|
||||
$Prompt = Split-Path -Path $venvDir -Leaf
|
||||
}
|
||||
}
|
||||
|
||||
Write-Verbose "Prompt = '$Prompt'"
|
||||
Write-Verbose "VenvDir='$VenvDir'"
|
||||
|
||||
# Deactivate any currently active virtual environment, but leave the
|
||||
# deactivate function in place.
|
||||
deactivate -nondestructive
|
||||
|
||||
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
|
||||
# that there is an activated venv.
|
||||
$env:VIRTUAL_ENV = $VenvDir
|
||||
|
||||
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
|
||||
|
||||
Write-Verbose "Setting prompt to '$Prompt'"
|
||||
|
||||
# Set the prompt to include the env name
|
||||
# Make sure _OLD_VIRTUAL_PROMPT is global
|
||||
function global:_OLD_VIRTUAL_PROMPT { "" }
|
||||
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
|
||||
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
|
||||
|
||||
function global:prompt {
|
||||
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
|
||||
_OLD_VIRTUAL_PROMPT
|
||||
}
|
||||
$env:VIRTUAL_ENV_PROMPT = $Prompt
|
||||
}
|
||||
|
||||
# Clear PYTHONHOME
|
||||
if (Test-Path -Path Env:PYTHONHOME) {
|
||||
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
|
||||
Remove-Item -Path Env:PYTHONHOME
|
||||
}
|
||||
|
||||
# Add the venv to the PATH
|
||||
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
|
||||
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
|
||||
70
venv/bin/activate
Normal file
70
venv/bin/activate
Normal file
@ -0,0 +1,70 @@
|
||||
# This file must be used with "source bin/activate" *from bash*
|
||||
# You cannot run it directly
|
||||
|
||||
deactivate () {
|
||||
# reset old environment variables
|
||||
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
|
||||
PATH="${_OLD_VIRTUAL_PATH:-}"
|
||||
export PATH
|
||||
unset _OLD_VIRTUAL_PATH
|
||||
fi
|
||||
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
|
||||
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
|
||||
export PYTHONHOME
|
||||
unset _OLD_VIRTUAL_PYTHONHOME
|
||||
fi
|
||||
|
||||
# Call hash to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
hash -r 2> /dev/null
|
||||
|
||||
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
||||
PS1="${_OLD_VIRTUAL_PS1:-}"
|
||||
export PS1
|
||||
unset _OLD_VIRTUAL_PS1
|
||||
fi
|
||||
|
||||
unset VIRTUAL_ENV
|
||||
unset VIRTUAL_ENV_PROMPT
|
||||
if [ ! "${1:-}" = "nondestructive" ] ; then
|
||||
# Self destruct!
|
||||
unset -f deactivate
|
||||
fi
|
||||
}
|
||||
|
||||
# unset irrelevant variables
|
||||
deactivate nondestructive
|
||||
|
||||
# on Windows, a path can contain colons and backslashes and has to be converted:
|
||||
if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
|
||||
# transform D:\path\to\venv to /d/path/to/venv on MSYS
|
||||
# and to /cygdrive/d/path/to/venv on Cygwin
|
||||
export VIRTUAL_ENV=$(cygpath "/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
27
venv/bin/activate.csh
Normal file
@ -0,0 +1,27 @@
|
||||
# This file must be used with "source bin/activate.csh" *from csh*.
|
||||
# You cannot run it directly.
|
||||
|
||||
# Created by Davide Di Blasi <davidedb@gmail.com>.
|
||||
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
|
||||
|
||||
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
setenv VIRTUAL_ENV "/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
69
venv/bin/activate.fish
Normal file
@ -0,0 +1,69 @@
|
||||
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
|
||||
# (https://fishshell.com/). You cannot run it directly.
|
||||
|
||||
function deactivate -d "Exit virtual environment and return to normal shell environment"
|
||||
# reset old environment variables
|
||||
if test -n "$_OLD_VIRTUAL_PATH"
|
||||
set -gx PATH $_OLD_VIRTUAL_PATH
|
||||
set -e _OLD_VIRTUAL_PATH
|
||||
end
|
||||
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
|
||||
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
|
||||
set -e _OLD_VIRTUAL_PYTHONHOME
|
||||
end
|
||||
|
||||
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
|
||||
set -e _OLD_FISH_PROMPT_OVERRIDE
|
||||
# prevents error when using nested fish instances (Issue #93858)
|
||||
if functions -q _old_fish_prompt
|
||||
functions -e fish_prompt
|
||||
functions -c _old_fish_prompt fish_prompt
|
||||
functions -e _old_fish_prompt
|
||||
end
|
||||
end
|
||||
|
||||
set -e VIRTUAL_ENV
|
||||
set -e VIRTUAL_ENV_PROMPT
|
||||
if test "$argv[1]" != "nondestructive"
|
||||
# Self-destruct!
|
||||
functions -e deactivate
|
||||
end
|
||||
end
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
set -gx VIRTUAL_ENV "/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
8
venv/bin/dotenv
Executable 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
8
venv/bin/httpx
Executable 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
8
venv/bin/normalizer
Executable 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
8
venv/bin/pip
Executable 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
8
venv/bin/pip3
Executable 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
8
venv/bin/pip3.12
Executable 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
8
venv/bin/py.test
Executable 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
8
venv/bin/pytest
Executable 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
1
venv/bin/python
Symbolic link
@ -0,0 +1 @@
|
||||
/home/joaomonezi/.pyenv/versions/3.12.2/bin/python
|
||||
1
venv/bin/python3
Symbolic link
1
venv/bin/python3
Symbolic link
@ -0,0 +1 @@
|
||||
python
|
||||
1
venv/bin/python3.12
Symbolic link
1
venv/bin/python3.12
Symbolic link
@ -0,0 +1 @@
|
||||
python
|
||||
8
venv/bin/uvicorn
Executable file
8
venv/bin/uvicorn
Executable 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())
|
||||
@ -0,0 +1 @@
|
||||
pip
|
||||
@ -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.
|
||||
@ -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
|
||||
@ -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
|
||||
@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: setuptools (75.6.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||
@ -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
|
||||
@ -0,0 +1 @@
|
||||
apscheduler
|
||||
BIN
venv/lib/python3.12/site-packages/__pycache__/py.cpython-312.pyc
Normal file
BIN
venv/lib/python3.12/site-packages/__pycache__/py.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
venv/lib/python3.12/site-packages/_cffi_backend.cpython-312-x86_64-linux-gnu.so
Executable file
BIN
venv/lib/python3.12/site-packages/_cffi_backend.cpython-312-x86_64-linux-gnu.so
Executable file
Binary file not shown.
9
venv/lib/python3.12/site-packages/_pytest/__init__.py
Normal file
9
venv/lib/python3.12/site-packages/_pytest/__init__.py
Normal 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]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
116
venv/lib/python3.12/site-packages/_pytest/_argcomplete.py
Normal file
116
venv/lib/python3.12/site-packages/_pytest/_argcomplete.py
Normal 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
|
||||
22
venv/lib/python3.12/site-packages/_pytest/_code/__init__.py
Normal file
22
venv/lib/python3.12/site-packages/_pytest/_code/__init__.py
Normal 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",
|
||||
]
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user