Cleanup .pyc and .DS_Store, add new modules, remove legacy services

This commit is contained in:
Josako
2025-05-17 18:46:17 +02:00
parent 5c982fcc2c
commit d2a9092f46
93 changed files with 1620 additions and 80 deletions

View File

@@ -0,0 +1,9 @@
from common.services.entitlements.license_period_services import LicensePeriodServices
from common.services.entitlements.license_usage_services import LicenseUsageServices
from common.services.entitlements.license_tier_services import LicenseTierServices
__all__ = [
'LicensePeriodServices',
'LicenseUsageServices',
'LicenseTierServices'
]

View File

@@ -0,0 +1,223 @@
from dateutil.relativedelta import relativedelta
from datetime import datetime as dt, timezone as tz, timedelta
from flask import current_app
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.sql.expression import and_
from common.extensions import db
from common.models.entitlements import LicensePeriod, License, PeriodStatus, LicenseUsage
from common.utils.eveai_exceptions import EveAILicensePeriodsExceeded, EveAIPendingLicensePeriod, EveAINoActiveLicense
from common.utils.model_logging_utils import set_logging_information, update_logging_information
class LicensePeriodServices:
@staticmethod
def find_current_license_period_for_usage(tenant_id: int) -> LicensePeriod:
"""
Find the current license period for a tenant. It ensures the status of the different license periods are adapted
when required, and a LicenseUsage object is created if required.
Args:
tenant_id: The ID of the tenant to find the license period for
Raises:
EveAIException: and derived classes
"""
current_date = dt.now(tz.utc).date()
license_period = (db.session.query(LicensePeriod)
.filter_by(tenant_id=tenant_id)
.filter(and_(LicensePeriod.period_start_date <= current_date,
LicensePeriod.period_end_date >= current_date))
.first())
if not license_period:
license_period = LicensePeriodServices._create_next_license_period_for_usage(tenant_id)
if license_period:
match license_period.status:
case PeriodStatus.UPCOMING:
LicensePeriodServices._complete_last_license_period()
LicensePeriodServices._activate_license_period(license_period)
if not license_period.license_usage:
new_license_usage = LicenseUsage()
new_license_usage.license_period = license_period
try:
db.session.add(new_license_usage)
db.session.commit()
except SQLAlchemyError as e:
db.session.rollback()
current_app.logger.error(
f"Error creating new license usage for license period {license_period.id}: {str(e)}")
raise e
if license_period.status == PeriodStatus.ACTIVE:
return license_period
else:
# Status is PENDING, so no prepaid payment received. There is no license period we can use.
# We allow for a delay of 5 days before raising an exception.
current_date = dt.now(tz.utc).date()
delta = abs(current_date - license_period.period_start_date)
if delta > timedelta(days=current_app.config.get('ENTITLEMENTS_MAX_PENDING_DAYS', 5)):
raise EveAIPendingLicensePeriod()
case PeriodStatus.ACTIVE:
return license_period
case PeriodStatus.PENDING:
return license_period
else:
raise EveAILicensePeriodsExceeded(license_id=None)
@staticmethod
def _create_next_license_period_for_usage(tenant_id) -> LicensePeriod:
"""
Create a new period for this license using the current license configuration
Args:
tenant_id: The ID of the tenant to create the period for
Returns:
LicensePeriod: The newly created license period
"""
current_date = dt.now(tz.utc).date()
# Zoek de actieve licentie voor deze tenant op de huidige datum
the_license = (db.session.query(License)
.filter_by(tenant_id=tenant_id)
.filter(License.start_date <= current_date)
.filter(License.end_date >= current_date)
.first())
if not the_license:
current_app.logger.error(f"No active license found for tenant {tenant_id} on date {current_date}")
raise EveAINoActiveLicense(tenant_id=tenant_id)
next_period_number = 1
if the_license.periods:
# If there are existing periods, get the next sequential number
next_period_number = max(p.period_number for p in the_license.periods) + 1
if next_period_number > the_license.max_periods:
raise EveAILicensePeriodsExceeded(license_id=the_license.id)
new_license_period = LicensePeriod(
license_id=the_license.id,
tenant_id=tenant_id,
period_number=next_period_number,
period_start=the_license.start_date + relativedelta(months=next_period_number-1),
period_end=the_license.end_date + relativedelta(months=next_period_number, days=-1),
status=PeriodStatus.UPCOMING,
)
set_logging_information(new_license_period, dt.now(tz.utc))
new_license_usage = LicenseUsage(
license_period=new_license_period,
tenant_id=tenant_id,
)
set_logging_information(new_license_usage, dt.now(tz.utc))
try:
db.session.add(new_license_period)
db.session.add(new_license_usage)
db.session.commit()
return new_license_period
except SQLAlchemyError as e:
db.session.rollback()
current_app.logger.error(f"Error creating next license period for tenant {tenant_id}: {str(e)}")
raise e
@staticmethod
def _activate_license_period(license_period_id: int = None, license_period: LicensePeriod = None) -> LicensePeriod:
"""
Activate a license period
Args:
license_period_id: The ID of the license period to activate (optional if license_period is provided)
license_period: The LicensePeriod object to activate (optional if license_period_id is provided)
Returns:
LicensePeriod: The activated license period object
Raises:
ValueError: If neither license_period_id nor license_period is provided
"""
if license_period is None and license_period_id is None:
raise ValueError("Either license_period_id or license_period must be provided")
# Get a license period object if only ID was provided
if license_period is None:
license_period = LicensePeriod.query.get_or_404(license_period_id)
if license_period.upcoming_at is not None:
license_period.pending_at.upcoming_at = dt.now(tz.utc)
license_period.status = PeriodStatus.PENDING
if license_period.prepaid_payment:
# There is a payment received for the given period
license_period.active_at = dt.now(tz.utc)
license_period.status = PeriodStatus.ACTIVE
# Copy snapshot fields from the license to the period
the_license = License.query.get_or_404(license_period.license_id)
license_period.currency = the_license.currency
license_period.basic_fee = the_license.basic_fee
license_period.max_storage_mb = the_license.max_storage_mb
license_period.additional_storage_price = the_license.additional_storage_price
license_period.additional_storage_bucket = the_license.additional_storage_bucket
license_period.included_embedding_mb = the_license.included_embedding_mb
license_period.additional_embedding_price = the_license.additional_embedding_price
license_period.additional_embedding_bucket = the_license.additional_embedding_bucket
license_period.included_interaction_tokens = the_license.included_interaction_tokens
license_period.additional_interaction_token_price = the_license.additional_interaction_token_price
license_period.additional_interaction_bucket = the_license.additional_interaction_bucket
license_period.additional_storage_allowed = the_license.additional_storage_allowed
license_period.additional_embedding_allowed = the_license.additional_embedding_allowed
license_period.additional_interaction_allowed = the_license.additional_interaction_allowed
update_logging_information(license_period, dt.now(tz.utc))
if not license_period.license_usage:
license_period.license_usage = LicenseUsage(
tenant_id=license_period.tenant_id,
license_period=license_period,
)
license_period.license_usage.recalculate_storage()
try:
db.session.add(license_period)
db.session.add(license_period.license_usage)
db.session.commit()
except SQLAlchemyError as e:
db.session.rollback()
current_app.logger.error(f"Error activating license period {license_period_id}: {str(e)}")
raise e
return license_period
@staticmethod
def _complete_last_license_period(tenant_id) -> None:
"""
Complete the active or pending license period for a tenant. This is done by setting the status to COMPLETED.
Args:
tenant_id: De ID van de tenant
"""
# Zoek de licenseperiode voor deze tenant met status ACTIVE of PENDING
active_period = (
db.session.query(LicensePeriod)
.filter_by(tenant_id=tenant_id)
.filter(LicensePeriod.status.in_([PeriodStatus.ACTIVE, PeriodStatus.PENDING]))
.first()
)
# Als er geen actieve periode gevonden is, hoeven we niets te doen
if not active_period:
return
# Zet de gevonden periode op COMPLETED
active_period.status = PeriodStatus.COMPLETED
active_period.completed_at = dt.now(tz.utc)
update_logging_information(active_period, dt.now(tz.utc))
try:
db.session.add(active_period)
db.session.commit()
except SQLAlchemyError as e:
db.session.rollback()
current_app.logger.error(f"Error completing period {active_period.id} for {tenant_id}: {str(e)}")
raise e

View File

@@ -0,0 +1,67 @@
from flask import session, flash, current_app
from datetime import datetime as dt, timezone as tz
from sqlalchemy.exc import SQLAlchemyError
from common.extensions import db
from common.models.entitlements import PartnerServiceLicenseTier
from common.models.user import Partner
from common.utils.eveai_exceptions import EveAINoManagementPartnerService
from common.utils.model_logging_utils import set_logging_information
class LicenseTierServices:
@staticmethod
def associate_license_tier_with_partner(license_tier_id):
"""Associate a license tier with a partner"""
try:
partner_id = session['partner']['id']
# Get partner service (MANAGEMENT_SERVICE type)
partner = Partner.query.get(partner_id)
if not partner:
return
# Find a management service for this partner
management_service = next((service for service in session['partner']['services']
if service.get('type') == 'MANAGEMENT_SERVICE'), None)
if not management_service:
flash("Cannot associate license tier with partner. No management service defined for partner", "danger")
current_app.logger.error(f"No Management Service defined for partner {partner_id}"
f"trying to associate license tier {license_tier_id}.")
raise EveAINoManagementPartnerService()
# Check if the association already exists
existing_association = PartnerServiceLicenseTier.query.filter_by(
partner_service_id=management_service['id'],
license_tier_id=license_tier_id
).first()
if existing_association:
# Association already exists, nothing to do
flash("License tier was already associated with partner", "info")
current_app.logger.info(f"Association between partner service {management_service['id']} and "
f"license tier {license_tier_id} already exists.")
return
# Create the association
association = PartnerServiceLicenseTier(
partner_service_id=management_service['id'],
license_tier_id=license_tier_id
)
set_logging_information(association, dt.now(tz.utc))
db.session.add(association)
db.session.commit()
flash("Successfully associated license tier to partner", "success")
current_app.logger.info(f"Successfully associated license tier {license_tier_id} with "
f"partner service {management_service['id']}")
return True
except SQLAlchemyError as e:
db.session.rollback()
flash("Failed to associated license tier with partner service due to an internal error. "
"Please contact the System Administrator", "danger")
current_app.logger.error(f"Error associating license tier {license_tier_id} with partner: {str(e)}")
raise e

View File

@@ -0,0 +1,143 @@
from dateutil.relativedelta import relativedelta
from flask import session, current_app, flash
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.sql.expression import text
from common.extensions import db, cache_manager
from common.models.entitlements import PartnerServiceLicenseTier, License, LicenseUsage, LicensePeriod, PeriodStatus
from common.models.user import Partner, PartnerTenant
from common.services.entitlements import LicensePeriodServices
from common.utils.database import Database
from common.utils.eveai_exceptions import EveAINoManagementPartnerService, EveAINoActiveLicense, \
EveAIStorageQuotaExceeded, EveAIEmbeddingQuotaExceeded, EveAIInteractionQuotaExceeded, EveAILicensePeriodsExceeded, \
EveAIException
from common.utils.model_logging_utils import set_logging_information, update_logging_information
from datetime import datetime as dt, timezone as tz
from common.utils.security_utils import current_user_has_role
class LicenseUsageServices:
@staticmethod
def check_storage_and_embedding_quota(tenant_id: int, file_size_mb: float) -> None:
"""
Check if a tenant can add a new document without exceeding storage and embedding quotas
Args:
tenant_id: ID of the tenant
file_size_mb: Size of the file in MB
Raises:
EveAIStorageQuotaExceeded: If storage quota would be exceeded
EveAIEmbeddingQuotaExceeded: If embedding quota would be exceeded
EveAINoActiveLicense: If no active license is found
EveAIException: For other errors
"""
# Get active license period
license_period = LicensePeriodServices.find_current_license_period_for_usage(tenant_id)
# Early return if both overruns are allowed - no need to check usage at all
if license_period.additional_storage_allowed and license_period.additional_embedding_allowed:
return
# Check storage quota only if overruns are not allowed
if not license_period.additional_storage_allowed:
LicenseUsageServices._validate_storage_quota(license_period, file_size_mb)
# Check embedding quota only if overruns are not allowed
if not license_period.additional_embedding_allowed:
LicenseUsageServices._validate_embedding_quota(license_period, file_size_mb)
@staticmethod
def check_embedding_quota(tenant_id: int, file_size_mb: float) -> None:
"""
Check if a tenant can re-embed a document without exceeding embedding quota
Args:
tenant_id: ID of the tenant
file_size_mb: Size of the file in MB
Raises:
EveAIEmbeddingQuotaExceeded: If embedding quota would be exceeded
EveAINoActiveLicense: If no active license is found
EveAIException: For other errors
"""
# Get active license period
license_period = LicensePeriodServices.find_current_license_period_for_usage(tenant_id)
# Early return if both overruns are allowed - no need to check usage at all
if license_period.additional_embedding_allowed:
return
# Check embedding quota
LicenseUsageServices._validate_embedding_quota(license_period, file_size_mb)
@staticmethod
def check_interaction_quota(tenant_id: int) -> None:
"""
Check if a tenant can execute a specialist without exceeding interaction quota. As it is impossible to estimate
the number of interaction tokens, we only check if the interaction quota are exceeded. So we might have a
limited overrun.
Args:
tenant_id: ID of the tenant
Raises:
EveAIInteractionQuotaExceeded: If interaction quota would be exceeded
EveAINoActiveLicense: If no active license is found
EveAIException: For other errors
"""
# Get active license period
license_period = LicensePeriodServices.find_current_license_period_for_usage(tenant_id)
# Early return if both overruns are allowed - no need to check usage at all
if license_period.additional_interaction_allowed:
return
# Convert tokens to M tokens and check interaction quota
LicenseUsageServices._validate_interaction_quota(license_period)
@staticmethod
def _validate_storage_quota(license_period: LicensePeriod, additional_mb: float) -> None:
"""Check storage quota and raise exception if exceeded"""
current_storage = license_period.license_usage.storage_mb_used or 0
projected_storage = current_storage + additional_mb
max_storage = license_period.max_storage_mb
# Hard limit check (we only get here if overruns are NOT allowed)
if projected_storage > max_storage:
raise EveAIStorageQuotaExceeded(
current_usage=current_storage,
limit=max_storage,
additional=additional_mb
)
@staticmethod
def _validate_embedding_quota(license_period: LicensePeriod, additional_mb: float) -> None:
"""Check embedding quota and raise exception if exceeded"""
current_embedding = license_period.license_usage.embedding_mb_used or 0
projected_embedding = current_embedding + additional_mb
max_embedding = license_period.included_embedding_mb
# Hard limit check (we only get here if overruns are NOT allowed)
if projected_embedding > max_embedding:
raise EveAIEmbeddingQuotaExceeded(
current_usage=current_embedding,
limit=max_embedding,
additional=additional_mb
)
@staticmethod
def _validate_interaction_quota(license_period) -> None:
"""Check interaction quota and raise exception if exceeded (tokens in millions). We might have an overrun!"""
current_tokens = license_period.license_usage.interaction_total_tokens_used / 1_000_000 or 0
max_tokens = license_period.included_interaction_tokens
# Hard limit check (we only get here if overruns are NOT allowed)
if current_tokens > max_tokens:
raise EveAIInteractionQuotaExceeded(
current_usage=current_tokens,
limit=max_tokens
)