extract parse_time_range for console app stats related queries (#27626)
Signed-off-by: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com>
This commit is contained in:
@@ -1,7 +1,5 @@
|
|||||||
from datetime import datetime
|
|
||||||
|
|
||||||
import pytz
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from flask import abort
|
||||||
from flask_restx import Resource, marshal_with, reqparse
|
from flask_restx import Resource, marshal_with, reqparse
|
||||||
from flask_restx.inputs import int_range
|
from flask_restx.inputs import int_range
|
||||||
from sqlalchemy import func, or_
|
from sqlalchemy import func, or_
|
||||||
@@ -19,7 +17,7 @@ from fields.conversation_fields import (
|
|||||||
conversation_pagination_fields,
|
conversation_pagination_fields,
|
||||||
conversation_with_summary_pagination_fields,
|
conversation_with_summary_pagination_fields,
|
||||||
)
|
)
|
||||||
from libs.datetime_utils import naive_utc_now
|
from libs.datetime_utils import naive_utc_now, parse_time_range
|
||||||
from libs.helper import DatetimeString
|
from libs.helper import DatetimeString
|
||||||
from libs.login import current_account_with_tenant, login_required
|
from libs.login import current_account_with_tenant, login_required
|
||||||
from models import Conversation, EndUser, Message, MessageAnnotation
|
from models import Conversation, EndUser, Message, MessageAnnotation
|
||||||
@@ -90,25 +88,17 @@ class CompletionConversationApi(Resource):
|
|||||||
|
|
||||||
account = current_user
|
account = current_user
|
||||||
assert account.timezone is not None
|
assert account.timezone is not None
|
||||||
timezone = pytz.timezone(account.timezone)
|
|
||||||
utc_timezone = pytz.utc
|
|
||||||
|
|
||||||
if args["start"]:
|
try:
|
||||||
start_datetime = datetime.strptime(args["start"], "%Y-%m-%d %H:%M")
|
start_datetime_utc, end_datetime_utc = parse_time_range(args["start"], args["end"], account.timezone)
|
||||||
start_datetime = start_datetime.replace(second=0)
|
except ValueError as e:
|
||||||
|
abort(400, description=str(e))
|
||||||
start_datetime_timezone = timezone.localize(start_datetime)
|
|
||||||
start_datetime_utc = start_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
|
|
||||||
|
if start_datetime_utc:
|
||||||
query = query.where(Conversation.created_at >= start_datetime_utc)
|
query = query.where(Conversation.created_at >= start_datetime_utc)
|
||||||
|
|
||||||
if args["end"]:
|
if end_datetime_utc:
|
||||||
end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M")
|
end_datetime_utc = end_datetime_utc.replace(second=59)
|
||||||
end_datetime = end_datetime.replace(second=59)
|
|
||||||
|
|
||||||
end_datetime_timezone = timezone.localize(end_datetime)
|
|
||||||
end_datetime_utc = end_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
|
|
||||||
query = query.where(Conversation.created_at < end_datetime_utc)
|
query = query.where(Conversation.created_at < end_datetime_utc)
|
||||||
|
|
||||||
# FIXME, the type ignore in this file
|
# FIXME, the type ignore in this file
|
||||||
@@ -270,29 +260,21 @@ class ChatConversationApi(Resource):
|
|||||||
|
|
||||||
account = current_user
|
account = current_user
|
||||||
assert account.timezone is not None
|
assert account.timezone is not None
|
||||||
timezone = pytz.timezone(account.timezone)
|
|
||||||
utc_timezone = pytz.utc
|
|
||||||
|
|
||||||
if args["start"]:
|
try:
|
||||||
start_datetime = datetime.strptime(args["start"], "%Y-%m-%d %H:%M")
|
start_datetime_utc, end_datetime_utc = parse_time_range(args["start"], args["end"], account.timezone)
|
||||||
start_datetime = start_datetime.replace(second=0)
|
except ValueError as e:
|
||||||
|
abort(400, description=str(e))
|
||||||
start_datetime_timezone = timezone.localize(start_datetime)
|
|
||||||
start_datetime_utc = start_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
|
|
||||||
|
if start_datetime_utc:
|
||||||
match args["sort_by"]:
|
match args["sort_by"]:
|
||||||
case "updated_at" | "-updated_at":
|
case "updated_at" | "-updated_at":
|
||||||
query = query.where(Conversation.updated_at >= start_datetime_utc)
|
query = query.where(Conversation.updated_at >= start_datetime_utc)
|
||||||
case "created_at" | "-created_at" | _:
|
case "created_at" | "-created_at" | _:
|
||||||
query = query.where(Conversation.created_at >= start_datetime_utc)
|
query = query.where(Conversation.created_at >= start_datetime_utc)
|
||||||
|
|
||||||
if args["end"]:
|
if end_datetime_utc:
|
||||||
end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M")
|
end_datetime_utc = end_datetime_utc.replace(second=59)
|
||||||
end_datetime = end_datetime.replace(second=59)
|
|
||||||
|
|
||||||
end_datetime_timezone = timezone.localize(end_datetime)
|
|
||||||
end_datetime_utc = end_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
|
|
||||||
match args["sort_by"]:
|
match args["sort_by"]:
|
||||||
case "updated_at" | "-updated_at":
|
case "updated_at" | "-updated_at":
|
||||||
query = query.where(Conversation.updated_at <= end_datetime_utc)
|
query = query.where(Conversation.updated_at <= end_datetime_utc)
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
from datetime import datetime
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import pytz
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask import jsonify
|
from flask import abort, jsonify
|
||||||
from flask_restx import Resource, fields, reqparse
|
from flask_restx import Resource, fields, reqparse
|
||||||
|
|
||||||
from controllers.console import api, console_ns
|
from controllers.console import api, console_ns
|
||||||
@@ -11,6 +9,7 @@ from controllers.console.app.wraps import get_app_model
|
|||||||
from controllers.console.wraps import account_initialization_required, setup_required
|
from controllers.console.wraps import account_initialization_required, setup_required
|
||||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
|
from libs.datetime_utils import parse_time_range
|
||||||
from libs.helper import DatetimeString
|
from libs.helper import DatetimeString
|
||||||
from libs.login import current_account_with_tenant, login_required
|
from libs.login import current_account_with_tenant, login_required
|
||||||
from models import AppMode, Message
|
from models import AppMode, Message
|
||||||
@@ -56,26 +55,16 @@ WHERE
|
|||||||
arg_dict = {"tz": account.timezone, "app_id": app_model.id, "invoke_from": InvokeFrom.DEBUGGER}
|
arg_dict = {"tz": account.timezone, "app_id": app_model.id, "invoke_from": InvokeFrom.DEBUGGER}
|
||||||
assert account.timezone is not None
|
assert account.timezone is not None
|
||||||
|
|
||||||
timezone = pytz.timezone(account.timezone)
|
try:
|
||||||
utc_timezone = pytz.utc
|
start_datetime_utc, end_datetime_utc = parse_time_range(args["start"], args["end"], account.timezone)
|
||||||
|
except ValueError as e:
|
||||||
if args["start"]:
|
abort(400, description=str(e))
|
||||||
start_datetime = datetime.strptime(args["start"], "%Y-%m-%d %H:%M")
|
|
||||||
start_datetime = start_datetime.replace(second=0)
|
|
||||||
|
|
||||||
start_datetime_timezone = timezone.localize(start_datetime)
|
|
||||||
start_datetime_utc = start_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
|
|
||||||
|
if start_datetime_utc:
|
||||||
sql_query += " AND created_at >= :start"
|
sql_query += " AND created_at >= :start"
|
||||||
arg_dict["start"] = start_datetime_utc
|
arg_dict["start"] = start_datetime_utc
|
||||||
|
|
||||||
if args["end"]:
|
if end_datetime_utc:
|
||||||
end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M")
|
|
||||||
end_datetime = end_datetime.replace(second=0)
|
|
||||||
|
|
||||||
end_datetime_timezone = timezone.localize(end_datetime)
|
|
||||||
end_datetime_utc = end_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
|
|
||||||
sql_query += " AND created_at < :end"
|
sql_query += " AND created_at < :end"
|
||||||
arg_dict["end"] = end_datetime_utc
|
arg_dict["end"] = end_datetime_utc
|
||||||
|
|
||||||
@@ -120,8 +109,11 @@ class DailyConversationStatistic(Resource):
|
|||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
assert account.timezone is not None
|
assert account.timezone is not None
|
||||||
timezone = pytz.timezone(account.timezone)
|
|
||||||
utc_timezone = pytz.utc
|
try:
|
||||||
|
start_datetime_utc, end_datetime_utc = parse_time_range(args["start"], args["end"], account.timezone)
|
||||||
|
except ValueError as e:
|
||||||
|
abort(400, description=str(e))
|
||||||
|
|
||||||
stmt = (
|
stmt = (
|
||||||
sa.select(
|
sa.select(
|
||||||
@@ -134,18 +126,10 @@ class DailyConversationStatistic(Resource):
|
|||||||
.where(Message.app_id == app_model.id, Message.invoke_from != InvokeFrom.DEBUGGER)
|
.where(Message.app_id == app_model.id, Message.invoke_from != InvokeFrom.DEBUGGER)
|
||||||
)
|
)
|
||||||
|
|
||||||
if args["start"]:
|
if start_datetime_utc:
|
||||||
start_datetime = datetime.strptime(args["start"], "%Y-%m-%d %H:%M")
|
|
||||||
start_datetime = start_datetime.replace(second=0)
|
|
||||||
start_datetime_timezone = timezone.localize(start_datetime)
|
|
||||||
start_datetime_utc = start_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
stmt = stmt.where(Message.created_at >= start_datetime_utc)
|
stmt = stmt.where(Message.created_at >= start_datetime_utc)
|
||||||
|
|
||||||
if args["end"]:
|
if end_datetime_utc:
|
||||||
end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M")
|
|
||||||
end_datetime = end_datetime.replace(second=0)
|
|
||||||
end_datetime_timezone = timezone.localize(end_datetime)
|
|
||||||
end_datetime_utc = end_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
stmt = stmt.where(Message.created_at < end_datetime_utc)
|
stmt = stmt.where(Message.created_at < end_datetime_utc)
|
||||||
|
|
||||||
stmt = stmt.group_by("date").order_by("date")
|
stmt = stmt.group_by("date").order_by("date")
|
||||||
@@ -198,26 +182,17 @@ WHERE
|
|||||||
AND invoke_from != :invoke_from"""
|
AND invoke_from != :invoke_from"""
|
||||||
arg_dict = {"tz": account.timezone, "app_id": app_model.id, "invoke_from": InvokeFrom.DEBUGGER}
|
arg_dict = {"tz": account.timezone, "app_id": app_model.id, "invoke_from": InvokeFrom.DEBUGGER}
|
||||||
assert account.timezone is not None
|
assert account.timezone is not None
|
||||||
timezone = pytz.timezone(account.timezone)
|
|
||||||
utc_timezone = pytz.utc
|
|
||||||
|
|
||||||
if args["start"]:
|
try:
|
||||||
start_datetime = datetime.strptime(args["start"], "%Y-%m-%d %H:%M")
|
start_datetime_utc, end_datetime_utc = parse_time_range(args["start"], args["end"], account.timezone)
|
||||||
start_datetime = start_datetime.replace(second=0)
|
except ValueError as e:
|
||||||
|
abort(400, description=str(e))
|
||||||
start_datetime_timezone = timezone.localize(start_datetime)
|
|
||||||
start_datetime_utc = start_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
|
|
||||||
|
if start_datetime_utc:
|
||||||
sql_query += " AND created_at >= :start"
|
sql_query += " AND created_at >= :start"
|
||||||
arg_dict["start"] = start_datetime_utc
|
arg_dict["start"] = start_datetime_utc
|
||||||
|
|
||||||
if args["end"]:
|
if end_datetime_utc:
|
||||||
end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M")
|
|
||||||
end_datetime = end_datetime.replace(second=0)
|
|
||||||
|
|
||||||
end_datetime_timezone = timezone.localize(end_datetime)
|
|
||||||
end_datetime_utc = end_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
|
|
||||||
sql_query += " AND created_at < :end"
|
sql_query += " AND created_at < :end"
|
||||||
arg_dict["end"] = end_datetime_utc
|
arg_dict["end"] = end_datetime_utc
|
||||||
|
|
||||||
@@ -273,26 +248,17 @@ WHERE
|
|||||||
AND invoke_from != :invoke_from"""
|
AND invoke_from != :invoke_from"""
|
||||||
arg_dict = {"tz": account.timezone, "app_id": app_model.id, "invoke_from": InvokeFrom.DEBUGGER}
|
arg_dict = {"tz": account.timezone, "app_id": app_model.id, "invoke_from": InvokeFrom.DEBUGGER}
|
||||||
assert account.timezone is not None
|
assert account.timezone is not None
|
||||||
timezone = pytz.timezone(account.timezone)
|
|
||||||
utc_timezone = pytz.utc
|
|
||||||
|
|
||||||
if args["start"]:
|
try:
|
||||||
start_datetime = datetime.strptime(args["start"], "%Y-%m-%d %H:%M")
|
start_datetime_utc, end_datetime_utc = parse_time_range(args["start"], args["end"], account.timezone)
|
||||||
start_datetime = start_datetime.replace(second=0)
|
except ValueError as e:
|
||||||
|
abort(400, description=str(e))
|
||||||
start_datetime_timezone = timezone.localize(start_datetime)
|
|
||||||
start_datetime_utc = start_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
|
|
||||||
|
if start_datetime_utc:
|
||||||
sql_query += " AND created_at >= :start"
|
sql_query += " AND created_at >= :start"
|
||||||
arg_dict["start"] = start_datetime_utc
|
arg_dict["start"] = start_datetime_utc
|
||||||
|
|
||||||
if args["end"]:
|
if end_datetime_utc:
|
||||||
end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M")
|
|
||||||
end_datetime = end_datetime.replace(second=0)
|
|
||||||
|
|
||||||
end_datetime_timezone = timezone.localize(end_datetime)
|
|
||||||
end_datetime_utc = end_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
|
|
||||||
sql_query += " AND created_at < :end"
|
sql_query += " AND created_at < :end"
|
||||||
arg_dict["end"] = end_datetime_utc
|
arg_dict["end"] = end_datetime_utc
|
||||||
|
|
||||||
@@ -357,26 +323,17 @@ FROM
|
|||||||
AND m.invoke_from != :invoke_from"""
|
AND m.invoke_from != :invoke_from"""
|
||||||
arg_dict = {"tz": account.timezone, "app_id": app_model.id, "invoke_from": InvokeFrom.DEBUGGER}
|
arg_dict = {"tz": account.timezone, "app_id": app_model.id, "invoke_from": InvokeFrom.DEBUGGER}
|
||||||
assert account.timezone is not None
|
assert account.timezone is not None
|
||||||
timezone = pytz.timezone(account.timezone)
|
|
||||||
utc_timezone = pytz.utc
|
|
||||||
|
|
||||||
if args["start"]:
|
try:
|
||||||
start_datetime = datetime.strptime(args["start"], "%Y-%m-%d %H:%M")
|
start_datetime_utc, end_datetime_utc = parse_time_range(args["start"], args["end"], account.timezone)
|
||||||
start_datetime = start_datetime.replace(second=0)
|
except ValueError as e:
|
||||||
|
abort(400, description=str(e))
|
||||||
start_datetime_timezone = timezone.localize(start_datetime)
|
|
||||||
start_datetime_utc = start_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
|
|
||||||
|
if start_datetime_utc:
|
||||||
sql_query += " AND c.created_at >= :start"
|
sql_query += " AND c.created_at >= :start"
|
||||||
arg_dict["start"] = start_datetime_utc
|
arg_dict["start"] = start_datetime_utc
|
||||||
|
|
||||||
if args["end"]:
|
if end_datetime_utc:
|
||||||
end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M")
|
|
||||||
end_datetime = end_datetime.replace(second=0)
|
|
||||||
|
|
||||||
end_datetime_timezone = timezone.localize(end_datetime)
|
|
||||||
end_datetime_utc = end_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
|
|
||||||
sql_query += " AND c.created_at < :end"
|
sql_query += " AND c.created_at < :end"
|
||||||
arg_dict["end"] = end_datetime_utc
|
arg_dict["end"] = end_datetime_utc
|
||||||
|
|
||||||
@@ -446,26 +403,17 @@ WHERE
|
|||||||
AND m.invoke_from != :invoke_from"""
|
AND m.invoke_from != :invoke_from"""
|
||||||
arg_dict = {"tz": account.timezone, "app_id": app_model.id, "invoke_from": InvokeFrom.DEBUGGER}
|
arg_dict = {"tz": account.timezone, "app_id": app_model.id, "invoke_from": InvokeFrom.DEBUGGER}
|
||||||
assert account.timezone is not None
|
assert account.timezone is not None
|
||||||
timezone = pytz.timezone(account.timezone)
|
|
||||||
utc_timezone = pytz.utc
|
|
||||||
|
|
||||||
if args["start"]:
|
try:
|
||||||
start_datetime = datetime.strptime(args["start"], "%Y-%m-%d %H:%M")
|
start_datetime_utc, end_datetime_utc = parse_time_range(args["start"], args["end"], account.timezone)
|
||||||
start_datetime = start_datetime.replace(second=0)
|
except ValueError as e:
|
||||||
|
abort(400, description=str(e))
|
||||||
start_datetime_timezone = timezone.localize(start_datetime)
|
|
||||||
start_datetime_utc = start_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
|
|
||||||
|
if start_datetime_utc:
|
||||||
sql_query += " AND m.created_at >= :start"
|
sql_query += " AND m.created_at >= :start"
|
||||||
arg_dict["start"] = start_datetime_utc
|
arg_dict["start"] = start_datetime_utc
|
||||||
|
|
||||||
if args["end"]:
|
if end_datetime_utc:
|
||||||
end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M")
|
|
||||||
end_datetime = end_datetime.replace(second=0)
|
|
||||||
|
|
||||||
end_datetime_timezone = timezone.localize(end_datetime)
|
|
||||||
end_datetime_utc = end_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
|
|
||||||
sql_query += " AND m.created_at < :end"
|
sql_query += " AND m.created_at < :end"
|
||||||
arg_dict["end"] = end_datetime_utc
|
arg_dict["end"] = end_datetime_utc
|
||||||
|
|
||||||
@@ -525,26 +473,17 @@ WHERE
|
|||||||
AND invoke_from != :invoke_from"""
|
AND invoke_from != :invoke_from"""
|
||||||
arg_dict = {"tz": account.timezone, "app_id": app_model.id, "invoke_from": InvokeFrom.DEBUGGER}
|
arg_dict = {"tz": account.timezone, "app_id": app_model.id, "invoke_from": InvokeFrom.DEBUGGER}
|
||||||
assert account.timezone is not None
|
assert account.timezone is not None
|
||||||
timezone = pytz.timezone(account.timezone)
|
|
||||||
utc_timezone = pytz.utc
|
|
||||||
|
|
||||||
if args["start"]:
|
try:
|
||||||
start_datetime = datetime.strptime(args["start"], "%Y-%m-%d %H:%M")
|
start_datetime_utc, end_datetime_utc = parse_time_range(args["start"], args["end"], account.timezone)
|
||||||
start_datetime = start_datetime.replace(second=0)
|
except ValueError as e:
|
||||||
|
abort(400, description=str(e))
|
||||||
start_datetime_timezone = timezone.localize(start_datetime)
|
|
||||||
start_datetime_utc = start_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
|
|
||||||
|
if start_datetime_utc:
|
||||||
sql_query += " AND created_at >= :start"
|
sql_query += " AND created_at >= :start"
|
||||||
arg_dict["start"] = start_datetime_utc
|
arg_dict["start"] = start_datetime_utc
|
||||||
|
|
||||||
if args["end"]:
|
if end_datetime_utc:
|
||||||
end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M")
|
|
||||||
end_datetime = end_datetime.replace(second=0)
|
|
||||||
|
|
||||||
end_datetime_timezone = timezone.localize(end_datetime)
|
|
||||||
end_datetime_utc = end_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
|
|
||||||
sql_query += " AND created_at < :end"
|
sql_query += " AND created_at < :end"
|
||||||
arg_dict["end"] = end_datetime_utc
|
arg_dict["end"] = end_datetime_utc
|
||||||
|
|
||||||
@@ -602,26 +541,17 @@ WHERE
|
|||||||
AND invoke_from != :invoke_from"""
|
AND invoke_from != :invoke_from"""
|
||||||
arg_dict = {"tz": account.timezone, "app_id": app_model.id, "invoke_from": InvokeFrom.DEBUGGER}
|
arg_dict = {"tz": account.timezone, "app_id": app_model.id, "invoke_from": InvokeFrom.DEBUGGER}
|
||||||
assert account.timezone is not None
|
assert account.timezone is not None
|
||||||
timezone = pytz.timezone(account.timezone)
|
|
||||||
utc_timezone = pytz.utc
|
|
||||||
|
|
||||||
if args["start"]:
|
try:
|
||||||
start_datetime = datetime.strptime(args["start"], "%Y-%m-%d %H:%M")
|
start_datetime_utc, end_datetime_utc = parse_time_range(args["start"], args["end"], account.timezone)
|
||||||
start_datetime = start_datetime.replace(second=0)
|
except ValueError as e:
|
||||||
|
abort(400, description=str(e))
|
||||||
start_datetime_timezone = timezone.localize(start_datetime)
|
|
||||||
start_datetime_utc = start_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
|
|
||||||
|
if start_datetime_utc:
|
||||||
sql_query += " AND created_at >= :start"
|
sql_query += " AND created_at >= :start"
|
||||||
arg_dict["start"] = start_datetime_utc
|
arg_dict["start"] = start_datetime_utc
|
||||||
|
|
||||||
if args["end"]:
|
if end_datetime_utc:
|
||||||
end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M")
|
|
||||||
end_datetime = end_datetime.replace(second=0)
|
|
||||||
|
|
||||||
end_datetime_timezone = timezone.localize(end_datetime)
|
|
||||||
end_datetime_utc = end_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
|
|
||||||
sql_query += " AND created_at < :end"
|
sql_query += " AND created_at < :end"
|
||||||
arg_dict["end"] = end_datetime_utc
|
arg_dict["end"] = end_datetime_utc
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
from datetime import datetime
|
from flask import abort, jsonify
|
||||||
|
|
||||||
import pytz
|
|
||||||
from flask import jsonify
|
|
||||||
from flask_restx import Resource, reqparse
|
from flask_restx import Resource, reqparse
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
@@ -9,6 +6,7 @@ from controllers.console import api, console_ns
|
|||||||
from controllers.console.app.wraps import get_app_model
|
from controllers.console.app.wraps import get_app_model
|
||||||
from controllers.console.wraps import account_initialization_required, setup_required
|
from controllers.console.wraps import account_initialization_required, setup_required
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
|
from libs.datetime_utils import parse_time_range
|
||||||
from libs.helper import DatetimeString
|
from libs.helper import DatetimeString
|
||||||
from libs.login import current_account_with_tenant, login_required
|
from libs.login import current_account_with_tenant, login_required
|
||||||
from models.enums import WorkflowRunTriggeredFrom
|
from models.enums import WorkflowRunTriggeredFrom
|
||||||
@@ -43,23 +41,11 @@ class WorkflowDailyRunsStatistic(Resource):
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
assert account.timezone is not None
|
assert account.timezone is not None
|
||||||
timezone = pytz.timezone(account.timezone)
|
|
||||||
utc_timezone = pytz.utc
|
|
||||||
|
|
||||||
start_date = None
|
try:
|
||||||
end_date = None
|
start_date, end_date = parse_time_range(args["start"], args["end"], account.timezone)
|
||||||
|
except ValueError as e:
|
||||||
if args["start"]:
|
abort(400, description=str(e))
|
||||||
start_datetime = datetime.strptime(args["start"], "%Y-%m-%d %H:%M")
|
|
||||||
start_datetime = start_datetime.replace(second=0)
|
|
||||||
start_datetime_timezone = timezone.localize(start_datetime)
|
|
||||||
start_date = start_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
|
|
||||||
if args["end"]:
|
|
||||||
end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M")
|
|
||||||
end_datetime = end_datetime.replace(second=0)
|
|
||||||
end_datetime_timezone = timezone.localize(end_datetime)
|
|
||||||
end_date = end_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
|
|
||||||
response_data = self._workflow_run_repo.get_daily_runs_statistics(
|
response_data = self._workflow_run_repo.get_daily_runs_statistics(
|
||||||
tenant_id=app_model.tenant_id,
|
tenant_id=app_model.tenant_id,
|
||||||
@@ -100,23 +86,11 @@ class WorkflowDailyTerminalsStatistic(Resource):
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
assert account.timezone is not None
|
assert account.timezone is not None
|
||||||
timezone = pytz.timezone(account.timezone)
|
|
||||||
utc_timezone = pytz.utc
|
|
||||||
|
|
||||||
start_date = None
|
try:
|
||||||
end_date = None
|
start_date, end_date = parse_time_range(args["start"], args["end"], account.timezone)
|
||||||
|
except ValueError as e:
|
||||||
if args["start"]:
|
abort(400, description=str(e))
|
||||||
start_datetime = datetime.strptime(args["start"], "%Y-%m-%d %H:%M")
|
|
||||||
start_datetime = start_datetime.replace(second=0)
|
|
||||||
start_datetime_timezone = timezone.localize(start_datetime)
|
|
||||||
start_date = start_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
|
|
||||||
if args["end"]:
|
|
||||||
end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M")
|
|
||||||
end_datetime = end_datetime.replace(second=0)
|
|
||||||
end_datetime_timezone = timezone.localize(end_datetime)
|
|
||||||
end_date = end_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
|
|
||||||
response_data = self._workflow_run_repo.get_daily_terminals_statistics(
|
response_data = self._workflow_run_repo.get_daily_terminals_statistics(
|
||||||
tenant_id=app_model.tenant_id,
|
tenant_id=app_model.tenant_id,
|
||||||
@@ -157,23 +131,11 @@ class WorkflowDailyTokenCostStatistic(Resource):
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
assert account.timezone is not None
|
assert account.timezone is not None
|
||||||
timezone = pytz.timezone(account.timezone)
|
|
||||||
utc_timezone = pytz.utc
|
|
||||||
|
|
||||||
start_date = None
|
try:
|
||||||
end_date = None
|
start_date, end_date = parse_time_range(args["start"], args["end"], account.timezone)
|
||||||
|
except ValueError as e:
|
||||||
if args["start"]:
|
abort(400, description=str(e))
|
||||||
start_datetime = datetime.strptime(args["start"], "%Y-%m-%d %H:%M")
|
|
||||||
start_datetime = start_datetime.replace(second=0)
|
|
||||||
start_datetime_timezone = timezone.localize(start_datetime)
|
|
||||||
start_date = start_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
|
|
||||||
if args["end"]:
|
|
||||||
end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M")
|
|
||||||
end_datetime = end_datetime.replace(second=0)
|
|
||||||
end_datetime_timezone = timezone.localize(end_datetime)
|
|
||||||
end_date = end_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
|
|
||||||
response_data = self._workflow_run_repo.get_daily_token_cost_statistics(
|
response_data = self._workflow_run_repo.get_daily_token_cost_statistics(
|
||||||
tenant_id=app_model.tenant_id,
|
tenant_id=app_model.tenant_id,
|
||||||
@@ -214,23 +176,11 @@ class WorkflowAverageAppInteractionStatistic(Resource):
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
assert account.timezone is not None
|
assert account.timezone is not None
|
||||||
timezone = pytz.timezone(account.timezone)
|
|
||||||
utc_timezone = pytz.utc
|
|
||||||
|
|
||||||
start_date = None
|
try:
|
||||||
end_date = None
|
start_date, end_date = parse_time_range(args["start"], args["end"], account.timezone)
|
||||||
|
except ValueError as e:
|
||||||
if args["start"]:
|
abort(400, description=str(e))
|
||||||
start_datetime = datetime.strptime(args["start"], "%Y-%m-%d %H:%M")
|
|
||||||
start_datetime = start_datetime.replace(second=0)
|
|
||||||
start_datetime_timezone = timezone.localize(start_datetime)
|
|
||||||
start_date = start_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
|
|
||||||
if args["end"]:
|
|
||||||
end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M")
|
|
||||||
end_datetime = end_datetime.replace(second=0)
|
|
||||||
end_datetime_timezone = timezone.localize(end_datetime)
|
|
||||||
end_date = end_datetime_timezone.astimezone(utc_timezone)
|
|
||||||
|
|
||||||
response_data = self._workflow_run_repo.get_average_app_interaction_statistics(
|
response_data = self._workflow_run_repo.get_average_app_interaction_statistics(
|
||||||
tenant_id=app_model.tenant_id,
|
tenant_id=app_model.tenant_id,
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import abc
|
|||||||
import datetime
|
import datetime
|
||||||
from typing import Protocol
|
from typing import Protocol
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
|
||||||
|
|
||||||
class _NowFunction(Protocol):
|
class _NowFunction(Protocol):
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@@ -20,3 +22,51 @@ def naive_utc_now() -> datetime.datetime:
|
|||||||
representing current UTC time.
|
representing current UTC time.
|
||||||
"""
|
"""
|
||||||
return _now_func(datetime.UTC).replace(tzinfo=None)
|
return _now_func(datetime.UTC).replace(tzinfo=None)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_time_range(
|
||||||
|
start: str | None, end: str | None, tzname: str
|
||||||
|
) -> tuple[datetime.datetime | None, datetime.datetime | None]:
|
||||||
|
"""
|
||||||
|
Parse time range strings and convert to UTC datetime objects.
|
||||||
|
Handles DST ambiguity and non-existent times gracefully.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
start: Start time string (YYYY-MM-DD HH:MM)
|
||||||
|
end: End time string (YYYY-MM-DD HH:MM)
|
||||||
|
tzname: Timezone name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (start_datetime_utc, end_datetime_utc)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: When time range is invalid or start > end
|
||||||
|
"""
|
||||||
|
tz = pytz.timezone(tzname)
|
||||||
|
utc = pytz.utc
|
||||||
|
|
||||||
|
def _parse(time_str: str | None, label: str) -> datetime.datetime | None:
|
||||||
|
if not time_str:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
dt = datetime.datetime.strptime(time_str, "%Y-%m-%d %H:%M").replace(second=0)
|
||||||
|
except ValueError as e:
|
||||||
|
raise ValueError(f"Invalid {label} time format: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
return tz.localize(dt, is_dst=None).astimezone(utc)
|
||||||
|
except pytz.AmbiguousTimeError:
|
||||||
|
return tz.localize(dt, is_dst=False).astimezone(utc)
|
||||||
|
except pytz.NonExistentTimeError:
|
||||||
|
dt += datetime.timedelta(hours=1)
|
||||||
|
return tz.localize(dt, is_dst=None).astimezone(utc)
|
||||||
|
|
||||||
|
start_dt = _parse(start, "start")
|
||||||
|
end_dt = _parse(end, "end")
|
||||||
|
|
||||||
|
# Range validation
|
||||||
|
if start_dt and end_dt and start_dt > end_dt:
|
||||||
|
raise ValueError("start must be earlier than or equal to end")
|
||||||
|
|
||||||
|
return start_dt, end_dt
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import pytz
|
||||||
|
|
||||||
from libs.datetime_utils import naive_utc_now
|
from libs.datetime_utils import naive_utc_now, parse_time_range
|
||||||
|
|
||||||
|
|
||||||
def test_naive_utc_now(monkeypatch: pytest.MonkeyPatch):
|
def test_naive_utc_now(monkeypatch: pytest.MonkeyPatch):
|
||||||
@@ -20,3 +22,247 @@ def test_naive_utc_now(monkeypatch: pytest.MonkeyPatch):
|
|||||||
naive_time = naive_datetime.time()
|
naive_time = naive_datetime.time()
|
||||||
utc_time = tz_aware_utc_now.time()
|
utc_time = tz_aware_utc_now.time()
|
||||||
assert naive_time == utc_time
|
assert naive_time == utc_time
|
||||||
|
|
||||||
|
|
||||||
|
class TestParseTimeRange:
|
||||||
|
"""Test cases for parse_time_range function."""
|
||||||
|
|
||||||
|
def test_parse_time_range_basic(self):
|
||||||
|
"""Test basic time range parsing."""
|
||||||
|
start, end = parse_time_range("2024-01-01 10:00", "2024-01-01 18:00", "UTC")
|
||||||
|
|
||||||
|
assert start is not None
|
||||||
|
assert end is not None
|
||||||
|
assert start < end
|
||||||
|
assert start.tzinfo == pytz.UTC
|
||||||
|
assert end.tzinfo == pytz.UTC
|
||||||
|
|
||||||
|
def test_parse_time_range_start_only(self):
|
||||||
|
"""Test parsing with only start time."""
|
||||||
|
start, end = parse_time_range("2024-01-01 10:00", None, "UTC")
|
||||||
|
|
||||||
|
assert start is not None
|
||||||
|
assert end is None
|
||||||
|
assert start.tzinfo == pytz.UTC
|
||||||
|
|
||||||
|
def test_parse_time_range_end_only(self):
|
||||||
|
"""Test parsing with only end time."""
|
||||||
|
start, end = parse_time_range(None, "2024-01-01 18:00", "UTC")
|
||||||
|
|
||||||
|
assert start is None
|
||||||
|
assert end is not None
|
||||||
|
assert end.tzinfo == pytz.UTC
|
||||||
|
|
||||||
|
def test_parse_time_range_both_none(self):
|
||||||
|
"""Test parsing with both times None."""
|
||||||
|
start, end = parse_time_range(None, None, "UTC")
|
||||||
|
|
||||||
|
assert start is None
|
||||||
|
assert end is None
|
||||||
|
|
||||||
|
def test_parse_time_range_different_timezones(self):
|
||||||
|
"""Test parsing with different timezones."""
|
||||||
|
# Test with US/Eastern timezone
|
||||||
|
start, end = parse_time_range("2024-01-01 10:00", "2024-01-01 18:00", "US/Eastern")
|
||||||
|
|
||||||
|
assert start is not None
|
||||||
|
assert end is not None
|
||||||
|
assert start.tzinfo == pytz.UTC
|
||||||
|
assert end.tzinfo == pytz.UTC
|
||||||
|
# Verify the times are correctly converted to UTC
|
||||||
|
assert start.hour == 15 # 10 AM EST = 3 PM UTC (in January)
|
||||||
|
assert end.hour == 23 # 6 PM EST = 11 PM UTC (in January)
|
||||||
|
|
||||||
|
def test_parse_time_range_invalid_start_format(self):
|
||||||
|
"""Test parsing with invalid start time format."""
|
||||||
|
with pytest.raises(ValueError, match="time data.*does not match format"):
|
||||||
|
parse_time_range("invalid-date", "2024-01-01 18:00", "UTC")
|
||||||
|
|
||||||
|
def test_parse_time_range_invalid_end_format(self):
|
||||||
|
"""Test parsing with invalid end time format."""
|
||||||
|
with pytest.raises(ValueError, match="time data.*does not match format"):
|
||||||
|
parse_time_range("2024-01-01 10:00", "invalid-date", "UTC")
|
||||||
|
|
||||||
|
def test_parse_time_range_invalid_timezone(self):
|
||||||
|
"""Test parsing with invalid timezone."""
|
||||||
|
with pytest.raises(pytz.exceptions.UnknownTimeZoneError):
|
||||||
|
parse_time_range("2024-01-01 10:00", "2024-01-01 18:00", "Invalid/Timezone")
|
||||||
|
|
||||||
|
def test_parse_time_range_start_after_end(self):
|
||||||
|
"""Test parsing with start time after end time."""
|
||||||
|
with pytest.raises(ValueError, match="start must be earlier than or equal to end"):
|
||||||
|
parse_time_range("2024-01-01 18:00", "2024-01-01 10:00", "UTC")
|
||||||
|
|
||||||
|
def test_parse_time_range_start_equals_end(self):
|
||||||
|
"""Test parsing with start time equal to end time."""
|
||||||
|
start, end = parse_time_range("2024-01-01 10:00", "2024-01-01 10:00", "UTC")
|
||||||
|
|
||||||
|
assert start is not None
|
||||||
|
assert end is not None
|
||||||
|
assert start == end
|
||||||
|
|
||||||
|
def test_parse_time_range_dst_ambiguous_time(self):
|
||||||
|
"""Test parsing during DST ambiguous time (fall back)."""
|
||||||
|
# This test simulates DST fall back where 2:30 AM occurs twice
|
||||||
|
with patch("pytz.timezone") as mock_timezone:
|
||||||
|
# Mock timezone that raises AmbiguousTimeError
|
||||||
|
mock_tz = mock_timezone.return_value
|
||||||
|
|
||||||
|
# Create a mock datetime object for the return value
|
||||||
|
mock_dt = datetime.datetime(2024, 1, 1, 10, 0, 0)
|
||||||
|
mock_utc_dt = mock_dt.replace(tzinfo=pytz.UTC)
|
||||||
|
|
||||||
|
# Create a proper mock for the localized datetime
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
mock_localized_dt = MagicMock()
|
||||||
|
mock_localized_dt.astimezone.return_value = mock_utc_dt
|
||||||
|
|
||||||
|
# Set up side effects: first call raises exception, second call succeeds
|
||||||
|
mock_tz.localize.side_effect = [
|
||||||
|
pytz.AmbiguousTimeError("Ambiguous time"), # First call for start
|
||||||
|
mock_localized_dt, # Second call for start (with is_dst=False)
|
||||||
|
pytz.AmbiguousTimeError("Ambiguous time"), # First call for end
|
||||||
|
mock_localized_dt, # Second call for end (with is_dst=False)
|
||||||
|
]
|
||||||
|
|
||||||
|
start, end = parse_time_range("2024-01-01 10:00", "2024-01-01 18:00", "US/Eastern")
|
||||||
|
|
||||||
|
# Should use is_dst=False for ambiguous times
|
||||||
|
assert mock_tz.localize.call_count == 4 # 2 calls per time (first fails, second succeeds)
|
||||||
|
assert start is not None
|
||||||
|
assert end is not None
|
||||||
|
|
||||||
|
def test_parse_time_range_dst_nonexistent_time(self):
|
||||||
|
"""Test parsing during DST nonexistent time (spring forward)."""
|
||||||
|
with patch("pytz.timezone") as mock_timezone:
|
||||||
|
# Mock timezone that raises NonExistentTimeError
|
||||||
|
mock_tz = mock_timezone.return_value
|
||||||
|
|
||||||
|
# Create a mock datetime object for the return value
|
||||||
|
mock_dt = datetime.datetime(2024, 1, 1, 10, 0, 0)
|
||||||
|
mock_utc_dt = mock_dt.replace(tzinfo=pytz.UTC)
|
||||||
|
|
||||||
|
# Create a proper mock for the localized datetime
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
mock_localized_dt = MagicMock()
|
||||||
|
mock_localized_dt.astimezone.return_value = mock_utc_dt
|
||||||
|
|
||||||
|
# Set up side effects: first call raises exception, second call succeeds
|
||||||
|
mock_tz.localize.side_effect = [
|
||||||
|
pytz.NonExistentTimeError("Non-existent time"), # First call for start
|
||||||
|
mock_localized_dt, # Second call for start (with adjusted time)
|
||||||
|
pytz.NonExistentTimeError("Non-existent time"), # First call for end
|
||||||
|
mock_localized_dt, # Second call for end (with adjusted time)
|
||||||
|
]
|
||||||
|
|
||||||
|
start, end = parse_time_range("2024-01-01 10:00", "2024-01-01 18:00", "US/Eastern")
|
||||||
|
|
||||||
|
# Should adjust time forward by 1 hour for nonexistent times
|
||||||
|
assert mock_tz.localize.call_count == 4 # 2 calls per time (first fails, second succeeds)
|
||||||
|
assert start is not None
|
||||||
|
assert end is not None
|
||||||
|
|
||||||
|
def test_parse_time_range_edge_cases(self):
|
||||||
|
"""Test edge cases for time parsing."""
|
||||||
|
# Test with midnight times
|
||||||
|
start, end = parse_time_range("2024-01-01 00:00", "2024-01-01 23:59", "UTC")
|
||||||
|
assert start is not None
|
||||||
|
assert end is not None
|
||||||
|
assert start.hour == 0
|
||||||
|
assert start.minute == 0
|
||||||
|
assert end.hour == 23
|
||||||
|
assert end.minute == 59
|
||||||
|
|
||||||
|
def test_parse_time_range_different_dates(self):
|
||||||
|
"""Test parsing with different dates."""
|
||||||
|
start, end = parse_time_range("2024-01-01 10:00", "2024-01-02 10:00", "UTC")
|
||||||
|
assert start is not None
|
||||||
|
assert end is not None
|
||||||
|
assert start.date() != end.date()
|
||||||
|
assert (end - start).days == 1
|
||||||
|
|
||||||
|
def test_parse_time_range_seconds_handling(self):
|
||||||
|
"""Test that seconds are properly set to 0."""
|
||||||
|
start, end = parse_time_range("2024-01-01 10:30", "2024-01-01 18:45", "UTC")
|
||||||
|
assert start is not None
|
||||||
|
assert end is not None
|
||||||
|
assert start.second == 0
|
||||||
|
assert end.second == 0
|
||||||
|
|
||||||
|
def test_parse_time_range_timezone_conversion_accuracy(self):
|
||||||
|
"""Test accurate timezone conversion."""
|
||||||
|
# Test with a known timezone conversion
|
||||||
|
start, end = parse_time_range("2024-01-01 12:00", "2024-01-01 12:00", "Asia/Tokyo")
|
||||||
|
|
||||||
|
assert start is not None
|
||||||
|
assert end is not None
|
||||||
|
assert start.tzinfo == pytz.UTC
|
||||||
|
assert end.tzinfo == pytz.UTC
|
||||||
|
# Tokyo is UTC+9, so 12:00 JST = 03:00 UTC
|
||||||
|
assert start.hour == 3
|
||||||
|
assert end.hour == 3
|
||||||
|
|
||||||
|
def test_parse_time_range_summer_time(self):
|
||||||
|
"""Test parsing during summer time (DST)."""
|
||||||
|
# Test with US/Eastern during summer (EDT = UTC-4)
|
||||||
|
start, end = parse_time_range("2024-07-01 12:00", "2024-07-01 12:00", "US/Eastern")
|
||||||
|
|
||||||
|
assert start is not None
|
||||||
|
assert end is not None
|
||||||
|
assert start.tzinfo == pytz.UTC
|
||||||
|
assert end.tzinfo == pytz.UTC
|
||||||
|
# 12:00 EDT = 16:00 UTC
|
||||||
|
assert start.hour == 16
|
||||||
|
assert end.hour == 16
|
||||||
|
|
||||||
|
def test_parse_time_range_winter_time(self):
|
||||||
|
"""Test parsing during winter time (standard time)."""
|
||||||
|
# Test with US/Eastern during winter (EST = UTC-5)
|
||||||
|
start, end = parse_time_range("2024-01-01 12:00", "2024-01-01 12:00", "US/Eastern")
|
||||||
|
|
||||||
|
assert start is not None
|
||||||
|
assert end is not None
|
||||||
|
assert start.tzinfo == pytz.UTC
|
||||||
|
assert end.tzinfo == pytz.UTC
|
||||||
|
# 12:00 EST = 17:00 UTC
|
||||||
|
assert start.hour == 17
|
||||||
|
assert end.hour == 17
|
||||||
|
|
||||||
|
def test_parse_time_range_empty_strings(self):
|
||||||
|
"""Test parsing with empty strings."""
|
||||||
|
# Empty strings are treated as None, so they should not raise errors
|
||||||
|
start, end = parse_time_range("", "2024-01-01 18:00", "UTC")
|
||||||
|
assert start is None
|
||||||
|
assert end is not None
|
||||||
|
|
||||||
|
start, end = parse_time_range("2024-01-01 10:00", "", "UTC")
|
||||||
|
assert start is not None
|
||||||
|
assert end is None
|
||||||
|
|
||||||
|
def test_parse_time_range_malformed_datetime(self):
|
||||||
|
"""Test parsing with malformed datetime strings."""
|
||||||
|
with pytest.raises(ValueError, match="time data.*does not match format"):
|
||||||
|
parse_time_range("2024-13-01 10:00", "2024-01-01 18:00", "UTC")
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="time data.*does not match format"):
|
||||||
|
parse_time_range("2024-01-01 10:00", "2024-01-32 18:00", "UTC")
|
||||||
|
|
||||||
|
def test_parse_time_range_very_long_time_range(self):
|
||||||
|
"""Test parsing with very long time range."""
|
||||||
|
start, end = parse_time_range("2020-01-01 00:00", "2030-12-31 23:59", "UTC")
|
||||||
|
|
||||||
|
assert start is not None
|
||||||
|
assert end is not None
|
||||||
|
assert start < end
|
||||||
|
assert (end - start).days > 3000 # More than 8 years
|
||||||
|
|
||||||
|
def test_parse_time_range_negative_timezone(self):
|
||||||
|
"""Test parsing with negative timezone offset."""
|
||||||
|
start, end = parse_time_range("2024-01-01 12:00", "2024-01-01 12:00", "America/New_York")
|
||||||
|
|
||||||
|
assert start is not None
|
||||||
|
assert end is not None
|
||||||
|
assert start.tzinfo == pytz.UTC
|
||||||
|
assert end.tzinfo == pytz.UTC
|
||||||
|
|||||||
Reference in New Issue
Block a user