LiKenun commited on
Commit
307cacc
·
1 Parent(s): 005a292

First working Docker build and run

Browse files
.dockerignore CHANGED
@@ -46,7 +46,7 @@ coverage.xml
46
 
47
  # Environments
48
  .env
49
- .venv
50
  env/
51
  venv/
52
  ENV/
 
46
 
47
  # Environments
48
  .env
49
+ .venv/
50
  env/
51
  venv/
52
  ENV/
Dockerfile CHANGED
@@ -13,13 +13,13 @@ RUN apt-get update \
13
  && apt-get clean \
14
  && rm -rf /var/lib/apt/lists/*
15
 
16
- # Copy project files.
17
- COPY pyproject.toml README.md ./
18
- COPY src/ ./src/
19
-
20
  # Install Python dependencies.
21
- RUN pip install --no-cache-dir --upgrade pip \
22
- && pip install --no-cache-dir .
 
 
 
 
23
 
24
  # Create a non-root user and switch to it.
25
  RUN useradd -m appuser
 
13
  && apt-get clean \
14
  && rm -rf /var/lib/apt/lists/*
15
 
 
 
 
 
16
  # Install Python dependencies.
17
+ COPY pyproject.toml ./
18
+ RUN pip install --no-cache-dir --upgrade pip
19
+
20
+ # Copy project files and build.
21
+ COPY src/ ./src/
22
+ RUN pip install --no-cache-dir .
23
 
24
  # Create a non-root user and switch to it.
25
  RUN useradd -m appuser
pyproject.toml CHANGED
@@ -20,6 +20,7 @@ classifiers = [
20
  ]
21
  dependencies = [
22
  "pydantic>=2.0.0",
 
23
  "fastapi>=0.100.0",
24
  "uvicorn>=0.22.0",
25
  "loguru>=0.7.0",
@@ -27,6 +28,7 @@ dependencies = [
27
  "httpx>=0.24.1",
28
  "tenacity>=8.2.2",
29
  "pybreaker>=1.0.2",
 
30
  "apscheduler>=3.10.1",
31
  "slack-sdk>=3.21.3",
32
  "pymongo>=4.4.1",
 
20
  ]
21
  dependencies = [
22
  "pydantic>=2.0.0",
23
+ "pydantic-settings>=2.0.0",
24
  "fastapi>=0.100.0",
25
  "uvicorn>=0.22.0",
26
  "loguru>=0.7.0",
 
28
  "httpx>=0.24.1",
29
  "tenacity>=8.2.2",
30
  "pybreaker>=1.0.2",
31
+ "pytz>=2025.2",
32
  "apscheduler>=3.10.1",
33
  "slack-sdk>=3.21.3",
34
  "pymongo>=4.4.1",
src/ctp_slack_bot/api/app.py DELETED
File without changes
src/ctp_slack_bot/api/main.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from contextlib import asynccontextmanager
3
+
4
+ from fastapi import FastAPI
5
+ from loguru import logger
6
+
7
+ from ctp_slack_bot.api.routes import router
8
+ from ctp_slack_bot.core.config import settings
9
+ from ctp_slack_bot.core.logging import setup_logging
10
+ from ctp_slack_bot.tasks.scheduler import start_scheduler, stop_scheduler
11
+
12
+
13
+ @asynccontextmanager
14
+ async def lifespan(app: FastAPI):
15
+ """
16
+ Lifespan context manager for FastAPI application.
17
+ Handles startup and shutdown events.
18
+ """
19
+ # Setup logging
20
+ #setup_logging()
21
+ logger.info("Starting application")
22
+
23
+ # Start scheduler
24
+ #scheduler = start_scheduler()
25
+ #logger.info("Started scheduler")
26
+
27
+ yield
28
+
29
+ # Shutdown
30
+ logger.info("Shutting down application")
31
+ #stop_scheduler(scheduler)
32
+ #logger.info("Stopped scheduler")
33
+
34
+
35
+ app = FastAPI(
36
+ title="CTP Slack Bot",
37
+ description="A Slack bot for processing and analyzing Zoom transcripts using AI",
38
+ version="0.1.0",
39
+ lifespan=lifespan,
40
+ )
41
+
42
+ # Include routers
43
+ app.include_router(router)
44
+
45
+
46
+ @app.get("/health")
47
+ async def health_check():
48
+ """Health check endpoint"""
49
+ return {"status": "healthy"}
50
+
51
+
52
+ if __name__ == "__main__":
53
+ import uvicorn
54
+
55
+ uvicorn.run(
56
+ "main:app",
57
+ host="localhost", #settings.API_HOST,
58
+ port="8000" #settings.API_PORT,
59
+ #reload=settings.DEBUG,
60
+ )
src/ctp_slack_bot/api/routes.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status
2
+ from loguru import logger
3
+
4
+ #from ctp_slack_bot.api.dependencies import get_slack_service, get_transcript_service
5
+ #from ctp_slack_bot.models.transcript import TranscriptRequest, TranscriptResponse
6
+ #from ctp_slack_bot.services.slack_service import SlackService
7
+ #from ctp_slack_bot.services.transcript_service import TranscriptService
8
+
9
+ router = APIRouter(prefix="/api/v1")
10
+
11
+
12
+ # @router.post("/transcripts/analyze", response_model=TranscriptResponse)
13
+ # async def analyze_transcript(
14
+ # request: TranscriptRequest,
15
+ # transcript_service: TranscriptService = Depends(get_transcript_service),
16
+ # ):
17
+ # """
18
+ # Analyze a Zoom transcript and return insights.
19
+ # """
20
+ # logger.info(f"Analyzing transcript: {request.transcript_id}")
21
+ # try:
22
+ # result = await transcript_service.analyze_transcript(request)
23
+ # return result
24
+ # except Exception as e:
25
+ # logger.error(f"Error analyzing transcript: {e}")
26
+ # raise HTTPException(
27
+ # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
28
+ # detail="Failed to analyze transcript",
29
+ # )
30
+
31
+
32
+ # @router.post("/slack/message")
33
+ # async def send_slack_message(
34
+ # channel: str,
35
+ # message: str,
36
+ # slack_service: SlackService = Depends(get_slack_service),
37
+ # ):
38
+ # """
39
+ # Send a message to a Slack channel.
40
+ # """
41
+ # logger.info(f"Sending message to Slack channel: {channel}")
42
+ # try:
43
+ # result = await slack_service.send_message(channel, message)
44
+ # return {"status": "success", "message_ts": result.get("ts")}
45
+ # except Exception as e:
46
+ # logger.error(f"Error sending Slack message: {e}")
47
+ # raise HTTPException(
48
+ # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
49
+ # detail="Failed to send Slack message",
50
+ # )
51
+
52
+
53
+ # @router.post("/slack/webhook", include_in_schema=False)
54
+ # async def slack_webhook(
55
+ # slack_service: SlackService = Depends(get_slack_service),
56
+ # ):
57
+ # """
58
+ # Webhook endpoint for Slack events.
59
+ # """
60
+ # # This would typically handle Slack verification and event processing
61
+ # return {"challenge": "challenge_token"}
src/ctp_slack_bot/api/routes/__init__.py DELETED
File without changes
src/ctp_slack_bot/core/config.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from functools import lru_cache
2
+ from typing import Literal, Optional
3
+
4
+ from pydantic import Field, SecretStr, validator
5
+ from pydantic_settings import BaseSettings, SettingsConfigDict
6
+
7
+
8
+ class Settings(BaseSettings):
9
+ """
10
+ Application settings loaded from environment variables.
11
+ """
12
+ # API Configuration
13
+ API_HOST: str = "0.0.0.0"
14
+ API_PORT: int = 8000
15
+ DEBUG: bool = False
16
+
17
+ # MongoDB Configuration
18
+ MONGODB_URI: Optional[SecretStr] = None # TODO: Remove optionality
19
+ MONGODB_DB_NAME: str = "ctp_slack_bot"
20
+
21
+ # Slack Configuration
22
+ SLACK_BOT_TOKEN: Optional[SecretStr] = None # TODO: Remove optionality
23
+ SLACK_SIGNING_SECRET: Optional[SecretStr] = None # TODO: Remove optionality
24
+ SLACK_APP_TOKEN: Optional[SecretStr] = None
25
+
26
+ # Hugging Face Configuration
27
+ HF_API_TOKEN: Optional[SecretStr] = None
28
+
29
+ # Logging Configuration
30
+ LOG_LEVEL: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
31
+ LOG_FORMAT: Literal["text", "json"] = "json"
32
+
33
+ # APScheduler Configuration
34
+ SCHEDULER_TIMEZONE: str = "UTC"
35
+
36
+ @validator("MONGODB_URI")
37
+ def validate_mongodb_uri(cls, v):
38
+ """Validate MongoDB URI format"""
39
+ #if not v.get_secret_value().startswith("mongodb"):
40
+ # raise ValueError("MONGODB_URI must be a valid MongoDB connection string")
41
+ return v
42
+
43
+ model_config = SettingsConfigDict(
44
+ env_file=".env",
45
+ env_file_encoding="utf-8",
46
+ case_sensitive=True,
47
+ )
48
+
49
+
50
+ @lru_cache
51
+ def get_settings() -> Settings:
52
+ """
53
+ Get cached settings instance.
54
+ """
55
+ return Settings()
56
+
57
+
58
+ settings = get_settings()
src/ctp_slack_bot/core/logging.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import sys
3
+ from typing import Dict, Union
4
+
5
+ from loguru import logger
6
+
7
+ from ctp_slack_bot.core.config import settings
8
+
9
+
10
+ class InterceptHandler(logging.Handler):
11
+ """
12
+ Intercept standard logging messages toward Loguru.
13
+
14
+ This handler intercepts all standard logging messages and redirects them
15
+ to Loguru, allowing unified logging across the application.
16
+ """
17
+
18
+ def emit(self, record: logging.LogRecord) -> None:
19
+ # Get corresponding Loguru level if it exists
20
+ try:
21
+ level = logger.level(record.levelname).name
22
+ except ValueError:
23
+ level = record.levelno
24
+
25
+ # Find caller from where the logged message originated
26
+ frame, depth = logging.currentframe(), 2
27
+ while frame and frame.f_code.co_filename == logging.__file__:
28
+ frame = frame.f_back
29
+ depth += 1
30
+
31
+ logger.opt(depth=depth, exception=record.exc_info).log(
32
+ level, record.getMessage()
33
+ )
34
+
35
+
36
+ def setup_logging() -> None:
37
+ """
38
+ Configure logging with Loguru.
39
+
40
+ This function sets up Loguru as the main logging provider,
41
+ configures the log format based on settings, and intercepts
42
+ standard logging messages.
43
+ """
44
+ # Remove default loguru handler
45
+ logger.remove()
46
+
47
+ # Determine log format
48
+ if settings.LOG_FORMAT == "json":
49
+ log_format = {
50
+ "time": "{time:YYYY-MM-DD HH:mm:ss.SSS}",
51
+ "level": "{level}",
52
+ "message": "{message}",
53
+ "module": "{module}",
54
+ "function": "{function}",
55
+ "line": "{line}",
56
+ }
57
+ format_string = lambda record: record["message"]
58
+ else:
59
+ format_string = (
60
+ "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
61
+ "<level>{level: <8}</level> | "
62
+ "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
63
+ "<level>{message}</level>"
64
+ )
65
+
66
+ # Add console handler
67
+ logger.add(
68
+ sys.stderr,
69
+ format=format_string,
70
+ level=settings.LOG_LEVEL,
71
+ serialize=(settings.LOG_FORMAT == "json"),
72
+ backtrace=True,
73
+ diagnose=True,
74
+ )
75
+
76
+ # Add file handler for non-DEBUG environments
77
+ if settings.LOG_LEVEL != "DEBUG":
78
+ logger.add(
79
+ "logs/app.log",
80
+ rotation="10 MB",
81
+ retention="1 week",
82
+ compression="zip",
83
+ format=format_string,
84
+ level=settings.LOG_LEVEL,
85
+ serialize=(settings.LOG_FORMAT == "json"),
86
+ )
87
+
88
+ # Intercept standard logging messages
89
+ logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
90
+
91
+ # Update logging levels for some noisy libraries
92
+ for logger_name in [
93
+ "uvicorn",
94
+ "uvicorn.error",
95
+ "fastapi",
96
+ "httpx",
97
+ "apscheduler",
98
+ "pymongo",
99
+ ]:
100
+ logging.getLogger(logger_name).setLevel(logging.INFO)
101
+
102
+ logger.info(f"Logging configured with level {settings.LOG_LEVEL}")
src/ctp_slack_bot/tasks/scheduler.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from typing import Optional
3
+
4
+ from apscheduler.schedulers.asyncio import AsyncIOScheduler
5
+ from apscheduler.triggers.cron import CronTrigger
6
+ from loguru import logger
7
+ from pytz import timezone
8
+
9
+ from ctp_slack_bot.core.config import settings
10
+ #from ctp_slack_bot.tasks.error_report import send_error_report
11
+ #from ctp_slack_bot.tasks.transcript_cleanup import cleanup_old_transcripts
12
+
13
+
14
+ def start_scheduler() -> AsyncIOScheduler:
15
+ """
16
+ Start and configure the APScheduler instance.
17
+
18
+ Returns:
19
+ AsyncIOScheduler: Configured scheduler instance
20
+ """
21
+ scheduler = AsyncIOScheduler(timezone=timezone(settings.SCHEDULER_TIMEZONE))
22
+
23
+ # Add jobs to the scheduler
24
+
25
+ # Daily error report at 7 AM
26
+ # scheduler.add_job(
27
+ # send_error_report,
28
+ # CronTrigger(hour=7, minute=0),
29
+ # id="daily_error_report",
30
+ # name="Daily Error Report",
31
+ # replace_existing=True,
32
+ # )
33
+
34
+ # Weekly transcript cleanup on Sundays at 1 AM
35
+ # scheduler.add_job(
36
+ # cleanup_old_transcripts,
37
+ # CronTrigger(day_of_week="sun", hour=1, minute=0),
38
+ # id="weekly_transcript_cleanup",
39
+ # name="Weekly Transcript Cleanup",
40
+ # replace_existing=True,
41
+ # )
42
+
43
+ # Start the scheduler
44
+ scheduler.start()
45
+ logger.info("Scheduler started with timezone: {}", settings.SCHEDULER_TIMEZONE)
46
+ logger.info("Next run for error report: {}",
47
+ scheduler.get_job("daily_error_report").next_run_time)
48
+ logger.info("Next run for transcript cleanup: {}",
49
+ scheduler.get_job("weekly_transcript_cleanup").next_run_time)
50
+
51
+ return scheduler
52
+
53
+
54
+ def stop_scheduler(scheduler: Optional[AsyncIOScheduler] = None) -> None:
55
+ """
56
+ Shutdown the scheduler gracefully.
57
+
58
+ Args:
59
+ scheduler: The scheduler instance to shut down
60
+ """
61
+ if scheduler is not None and scheduler.running:
62
+ logger.info("Shutting down scheduler")
63
+ scheduler.shutdown(wait=False)
64
+ logger.info("Scheduler shutdown complete")