v0.1.0
Changes: - added rabbitmq hearbeat, consumer and publisher - fixed vqe to run with rabbitmq - added frontend via fasthtml - added proper .env configuration
This commit is contained in:
86
Dockerfile_build/source/connections/keycloak.py
Normal file
86
Dockerfile_build/source/connections/keycloak.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import os
|
||||
from datetime import timedelta
|
||||
|
||||
from connections.local_files import (
|
||||
delete_client_info,
|
||||
delete_token,
|
||||
delete_verifier,
|
||||
load_token,
|
||||
save_token,
|
||||
)
|
||||
from fastcore.xtras import datetime
|
||||
|
||||
from keycloak import KeycloakOpenID
|
||||
|
||||
KEYCLOAK_CONFIG = {
|
||||
"server_url": os.environ["KEYCLOAK_URL"],
|
||||
"realm_name": os.environ["KEYCLOAK_REALM_NAME"],
|
||||
"client_id": os.environ["KEACLOAK_CLIENT_ID"],
|
||||
}
|
||||
|
||||
# Инициализация клиента Keycloak
|
||||
keycloak_openid = KeycloakOpenID(
|
||||
server_url=KEYCLOAK_CONFIG["server_url"],
|
||||
realm_name=KEYCLOAK_CONFIG["realm_name"],
|
||||
client_id=KEYCLOAK_CONFIG["client_id"],
|
||||
)
|
||||
|
||||
|
||||
def refresh_access_token() -> dict | None:
|
||||
"""Refresh the access token using refresh token"""
|
||||
token_data = load_token()
|
||||
if not token_data or not token_data.get("refresh_token"):
|
||||
return None
|
||||
|
||||
try:
|
||||
# Use Keycloak's refresh token endpoint
|
||||
refresh_token = token_data["refresh_token"]
|
||||
|
||||
# Get new token pair using refresh token
|
||||
new_tokens = keycloak_openid.refresh_token(refresh_token)
|
||||
|
||||
# Calculate new expiration time
|
||||
expires_in = new_tokens.get("expires_in", 3600)
|
||||
expires_at = datetime.now() + timedelta(seconds=expires_in)
|
||||
|
||||
# Update token data
|
||||
updated_token_data = {
|
||||
"access_token": new_tokens["access_token"],
|
||||
"refresh_token": new_tokens.get(
|
||||
"refresh_token", refresh_token
|
||||
), # Some providers return new refresh token
|
||||
"expires_at": expires_at.isoformat(),
|
||||
"id_token": new_tokens.get("id_token", token_data.get("id_token")),
|
||||
"user_info": token_data.get("user_info"), # Preserve user info
|
||||
}
|
||||
|
||||
# Save updated tokens
|
||||
save_token(updated_token_data)
|
||||
|
||||
return updated_token_data
|
||||
|
||||
except Exception as e:
|
||||
# If refresh fails, clear all tokens and force re-login
|
||||
delete_token()
|
||||
delete_client_info()
|
||||
delete_verifier()
|
||||
return None
|
||||
|
||||
|
||||
def get_valid_access_token() -> str | None:
|
||||
"""Get a valid access token, refreshing if necessary"""
|
||||
token_data = load_token()
|
||||
|
||||
if not token_data or not token_data.get("expires_at"):
|
||||
return None
|
||||
|
||||
# Check if token is expired or about to expire (within 60 seconds)
|
||||
expires_at = datetime.fromisoformat(str(token_data.get("expires_at")))
|
||||
if expires_at <= datetime.now() + timedelta(seconds=60):
|
||||
# Token is expired or about to expire, try to refresh
|
||||
new_token_data = refresh_access_token()
|
||||
if new_token_data:
|
||||
return new_token_data["access_token"]
|
||||
return None
|
||||
|
||||
return token_data["access_token"]
|
||||
69
Dockerfile_build/source/connections/local_files.py
Normal file
69
Dockerfile_build/source/connections/local_files.py
Normal file
@@ -0,0 +1,69 @@
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Локальные файлы для хранения данных
|
||||
TOKEN_FILE = Path(os.environ["STORAGE_PATH"] + "/token.json")
|
||||
VERIFIER_FILE = Path(os.environ["STORAGE_PATH"] + "/pkce_verifier.json")
|
||||
CLIENT_INFO_FILE = Path(os.environ["STORAGE_PATH"] + "/client_info.json")
|
||||
|
||||
|
||||
def save_token(token_data: dict):
|
||||
"""Сохранить токен в файл"""
|
||||
with open(TOKEN_FILE, "w") as f:
|
||||
json.dump(token_data, f, indent=2)
|
||||
|
||||
|
||||
def load_token() -> dict | None:
|
||||
"""Загрузить токен из файла"""
|
||||
if not TOKEN_FILE.exists():
|
||||
return None
|
||||
with open(TOKEN_FILE, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def delete_token():
|
||||
"""Удалить файл с токеном"""
|
||||
if TOKEN_FILE.exists():
|
||||
TOKEN_FILE.unlink()
|
||||
|
||||
|
||||
def save_verifier(verifier: str):
|
||||
"""Сохранить PKCE верификатор в файл"""
|
||||
with open(VERIFIER_FILE, "w") as f:
|
||||
json.dump({"verifier": verifier}, f)
|
||||
|
||||
|
||||
def load_verifier() -> str | None:
|
||||
"""Загрузить PKCE верификатор из файла"""
|
||||
if not VERIFIER_FILE.exists():
|
||||
return None
|
||||
with open(VERIFIER_FILE, "r") as f:
|
||||
data = json.load(f)
|
||||
return data.get("verifier")
|
||||
|
||||
|
||||
def delete_verifier():
|
||||
"""Удалить файл с PKCE верификатором"""
|
||||
if VERIFIER_FILE.exists():
|
||||
VERIFIER_FILE.unlink()
|
||||
|
||||
|
||||
def save_client_info(client_info: dict):
|
||||
"""Сохранить информацию о клиенте локально"""
|
||||
with open(CLIENT_INFO_FILE, "w") as f:
|
||||
json.dump(client_info, f, indent=2)
|
||||
|
||||
|
||||
def load_client_info() -> dict | None:
|
||||
"""Загрузить информацию о клиенте"""
|
||||
if not CLIENT_INFO_FILE.exists():
|
||||
return None
|
||||
with open(CLIENT_INFO_FILE, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def delete_client_info():
|
||||
"""Удалить файл с информацией о клиенте"""
|
||||
if CLIENT_INFO_FILE.exists():
|
||||
CLIENT_INFO_FILE.unlink()
|
||||
139
Dockerfile_build/source/connections/quantum_backend.py
Normal file
139
Dockerfile_build/source/connections/quantum_backend.py
Normal file
@@ -0,0 +1,139 @@
|
||||
import os
|
||||
from typing import Any, Dict
|
||||
|
||||
import requests
|
||||
|
||||
# Global var from env
|
||||
QUANTUM_BACKEND_URL = os.getenv(
|
||||
"QUANTUM_BACKEND_URL", os.environ["QUNATUM_BACKEND_URL"]
|
||||
)
|
||||
|
||||
|
||||
def get_or_create_device_by_name(
|
||||
system_name: str, max_qubits: int, access_token
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get or create a device by name.
|
||||
|
||||
Args:
|
||||
system_name: Name of the system
|
||||
max_qubits: Maximum number of qubits
|
||||
|
||||
Returns:
|
||||
Device data as dictionary
|
||||
|
||||
Raises:
|
||||
requests.RequestException: If the request fails
|
||||
"""
|
||||
url = f"{QUANTUM_BACKEND_URL}/machine/"
|
||||
payload = {"system_name": system_name, "max_qubits": max_qubits}
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(url, json=payload, headers=headers)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.RequestException as e:
|
||||
raise Exception(f"Failed to get or create device: {e}") from e
|
||||
|
||||
|
||||
def get_device_by_id(system_id: int, access_token) -> Dict[str, Any]:
|
||||
"""
|
||||
Get device by ID.
|
||||
|
||||
Args:
|
||||
system_id: ID of the system
|
||||
|
||||
Returns:
|
||||
Device data as dictionary
|
||||
|
||||
Raises:
|
||||
requests.RequestException: If the request fails
|
||||
"""
|
||||
url = f"{QUANTUM_BACKEND_URL}/machine/system"
|
||||
params = {"system_id": system_id}
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.get(url, params=params, headers=headers)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.RequestException as e:
|
||||
raise Exception(f"Failed to get device by ID {system_id}: {e}") from e
|
||||
|
||||
|
||||
def update_device_data(
|
||||
system_id: int,
|
||||
system_name: str,
|
||||
system_description: str,
|
||||
max_qubits: int,
|
||||
access_token,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Update device data.
|
||||
|
||||
Args:
|
||||
system_id: ID of the system
|
||||
system_name: New system name
|
||||
system_description: New system description
|
||||
max_qubits: New maximum qubits
|
||||
|
||||
Returns:
|
||||
Updated device data as dictionary
|
||||
|
||||
Raises:
|
||||
requests.RequestException: If the request fails
|
||||
"""
|
||||
url = f"{QUANTUM_BACKEND_URL}/machine"
|
||||
payload = {
|
||||
"system_id": system_id,
|
||||
"system_name": system_name,
|
||||
"system_description": system_description,
|
||||
"max_qubits": max_qubits,
|
||||
}
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.put(url, json=payload, headers=headers)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.RequestException as e:
|
||||
raise Exception(f"Failed to update device data for ID {system_id}: {e}") from e
|
||||
|
||||
|
||||
def get_comp_system_file(experiment_type_id: int) -> bytes:
|
||||
"""
|
||||
Get comp_system file as a string/blob.
|
||||
|
||||
Args:
|
||||
experiment_type_id: ID of the experiment type
|
||||
|
||||
Returns:
|
||||
Python module content as string
|
||||
|
||||
Raises:
|
||||
requests.RequestException: If the request fails
|
||||
"""
|
||||
url = f"{QUANTUM_BACKEND_URL}/experiment/types/comp-system"
|
||||
params = {"experiment_type_id": experiment_type_id}
|
||||
|
||||
try:
|
||||
response = requests.get(url, params=params)
|
||||
response.raise_for_status()
|
||||
return response.content
|
||||
except requests.RequestException as e:
|
||||
raise Exception(
|
||||
f"Failed to get comp_system file for experiment_type_id {experiment_type_id}: {e}"
|
||||
) from e
|
||||
97
Dockerfile_build/source/connections/rabbitmq.py
Normal file
97
Dockerfile_build/source/connections/rabbitmq.py
Normal file
@@ -0,0 +1,97 @@
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
import aio_pika
|
||||
from aio_pika.abc import (
|
||||
AbstractChannel,
|
||||
AbstractRobustConnection,
|
||||
)
|
||||
from connections.keycloak import (
|
||||
get_valid_access_token,
|
||||
)
|
||||
from connections.local_files import (
|
||||
load_client_info,
|
||||
)
|
||||
|
||||
|
||||
class RabbitMQManager:
|
||||
"""Singleton manager for RabbitMQ connection and channels."""
|
||||
|
||||
_instance: Optional["RabbitMQManager"] = None
|
||||
_connection: Optional[AbstractRobustConnection] = None
|
||||
_consumer_channel: Optional[AbstractChannel] = None
|
||||
_publisher_channel: Optional[AbstractChannel] = None
|
||||
_heartbeat_channel: Optional[AbstractChannel] = None
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
async def connect(self):
|
||||
"""Establish the main connection if not already connected."""
|
||||
client_info = load_client_info()
|
||||
if not client_info:
|
||||
return None
|
||||
access_token = get_valid_access_token()
|
||||
if not access_token:
|
||||
return None
|
||||
if self._connection is None or self._connection.is_closed:
|
||||
print("Creating new RabbitMQ connection...")
|
||||
self._connection = await aio_pika.connect_robust(
|
||||
host=os.environ["RABBITMQ_HOST"],
|
||||
port=int(os.environ["RABBITMQ_PORT"]),
|
||||
login=str(client_info["system_id"]),
|
||||
password=access_token,
|
||||
virtualhost="/",
|
||||
)
|
||||
print("RabbitMQ connection established")
|
||||
return self._connection
|
||||
|
||||
async def get_consumer_channel(self):
|
||||
"""Get channel for consuming messages."""
|
||||
if self._connection:
|
||||
if self._consumer_channel is None or self._consumer_channel.is_closed:
|
||||
self._consumer_channel = await self._connection.channel()
|
||||
await self._consumer_channel.set_qos(prefetch_count=1, global_=True)
|
||||
print("Consumer channel created")
|
||||
return self._consumer_channel
|
||||
return None
|
||||
|
||||
async def get_publisher_channel(self):
|
||||
"""Get channel for publishing regular messages."""
|
||||
if self._connection:
|
||||
if self._publisher_channel is None or self._publisher_channel.is_closed:
|
||||
self._publisher_channel = await self._connection.channel()
|
||||
print("Publisher channel created")
|
||||
return self._publisher_channel
|
||||
return None
|
||||
|
||||
async def get_heartbeat_channel(self):
|
||||
"""Get channel for heartbeat publishing."""
|
||||
if self._connection:
|
||||
if self._heartbeat_channel is None or self._heartbeat_channel.is_closed:
|
||||
self._heartbeat_channel = await self._connection.channel()
|
||||
print("Heartbeat channel created")
|
||||
return self._heartbeat_channel
|
||||
return None
|
||||
|
||||
async def close(self):
|
||||
"""Close all channels and the main connection gracefully."""
|
||||
print("Closing RabbitMQ channels and connection...")
|
||||
|
||||
for channel in [
|
||||
self._consumer_channel,
|
||||
self._publisher_channel,
|
||||
self._heartbeat_channel,
|
||||
]:
|
||||
if channel and not channel.is_closed:
|
||||
await channel.close()
|
||||
|
||||
if self._connection and not self._connection.is_closed:
|
||||
await self._connection.close()
|
||||
print("RabbitMQ connection closed")
|
||||
|
||||
|
||||
# Create global singleton instance
|
||||
rabbitmq_manager = RabbitMQManager()
|
||||
Reference in New Issue
Block a user