Benard John commited on
Commit
5608dbe
·
1 Parent(s): 4b8e879

feat: implement core configuration management, Celery Beat scheduling, and Redis-based PDF stream processing workers.

Browse files
app/config.py CHANGED
@@ -86,6 +86,16 @@ class Settings(BaseSettings):
86
  return [origin.strip() for origin in v.split(",")]
87
  return v
88
 
 
 
 
 
 
 
 
 
 
 
89
  @property
90
  def is_production(self) -> bool:
91
  return self.app_env == "production"
 
86
  return [origin.strip() for origin in v.split(",")]
87
  return v
88
 
89
+ @field_validator("redis_url", mode="after")
90
+ @classmethod
91
+ def validate_redis_url(cls, v: str) -> str:
92
+ if v.startswith("rediss://") and "ssl_cert_reqs" not in v:
93
+ separator = "&" if "?" in v else "?"
94
+ v = f"{v}{separator}ssl_cert_reqs=CERT_NONE"
95
+ import os
96
+ os.environ["REDIS_URL"] = v
97
+ return v
98
+
99
  @property
100
  def is_production(self) -> bool:
101
  return self.app_env == "production"
app/main.py CHANGED
@@ -69,6 +69,11 @@ async def lifespan(app: FastAPI):
69
  init_retrieval_orchestrator()
70
  logger.info("All services initialized. Ukweli is ready to serve.")
71
 
 
 
 
 
 
72
  yield
73
 
74
  # --- Shutdown ---
 
69
  init_retrieval_orchestrator()
70
  logger.info("All services initialized. Ukweli is ready to serve.")
71
 
72
+ # Trigger ingestion to start right away
73
+ from app.scraper.workers.pdf_processor import run_scraper
74
+ logger.info("Triggering initial crawler run on startup...")
75
+ run_scraper.delay()
76
+
77
  yield
78
 
79
  # --- Shutdown ---
app/scraper/workers/pdf_processor.py CHANGED
@@ -32,6 +32,13 @@ app.config_from_object({
32
  'task_track_started': True,
33
  'task_time_limit': 600,
34
  'worker_prefetch_multiplier': 1,
 
 
 
 
 
 
 
35
  })
36
 
37
  logger = logging.getLogger(__name__)
 
32
  'task_track_started': True,
33
  'task_time_limit': 600,
34
  'worker_prefetch_multiplier': 1,
35
+ 'beat_schedule': {
36
+ 'run-scraper-daily': {
37
+ 'task': 'app.scraper.workers.pdf_processor.run_scraper',
38
+ 'schedule': 86400.0, # Run daily (every 24 hours)
39
+ 'args': (None,),
40
+ },
41
+ },
42
  })
43
 
44
  logger = logging.getLogger(__name__)
entrypoint.sh CHANGED
@@ -9,6 +9,14 @@ alembic upgrade head || echo "Database migrations failed, proceeding..."
9
  echo "Starting Celery worker..."
10
  celery -A app.scraper.workers.pdf_processor worker --loglevel=info --pool=solo &
11
 
 
 
 
 
 
 
 
 
12
  # Start FastAPI application
13
  echo "Starting FastAPI backend..."
14
  exec uvicorn app.main:app --host 0.0.0.0 --port 7860
 
9
  echo "Starting Celery worker..."
10
  celery -A app.scraper.workers.pdf_processor worker --loglevel=info --pool=solo &
11
 
12
+ # Start Celery Beat scheduler in the background
13
+ echo "Starting Celery Beat..."
14
+ celery -A app.scraper.workers.pdf_processor beat --loglevel=info &
15
+
16
+ # Start Redis stream consumer in the background (prevents blocking solo worker thread)
17
+ echo "Starting Redis stream consumer..."
18
+ PYTHONPATH=. python -c "from app.scraper.workers.pdf_processor import consume_pdf_stream; consume_pdf_stream()" &
19
+
20
  # Start FastAPI application
21
  echo "Starting FastAPI backend..."
22
  exec uvicorn app.main:app --host 0.0.0.0 --port 7860
tests/test_oag_pipeline.py CHANGED
@@ -184,5 +184,24 @@ class TestOAGPipeline(unittest.TestCase):
184
  self.assertIn("oag_kenya", cmd)
185
  self.assertIn("max_pages=5", cmd)
186
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  if __name__ == '__main__':
188
  unittest.main()
 
184
  self.assertIn("oag_kenya", cmd)
185
  self.assertIn("max_pages=5", cmd)
186
 
187
+ def test_redis_url_validation(self):
188
+ from app.config import Settings
189
+
190
+ # Test default/standard redis URL is untouched
191
+ settings_redis = Settings(redis_url="redis://localhost:6379")
192
+ self.assertEqual(settings_redis.redis_url, "redis://localhost:6379")
193
+
194
+ # Test rediss URL without ssl_cert_reqs is updated
195
+ settings_rediss = Settings(redis_url="rediss://localhost:6379/0")
196
+ self.assertEqual(settings_rediss.redis_url, "rediss://localhost:6379/0?ssl_cert_reqs=CERT_NONE")
197
+
198
+ # Test rediss URL with existing query param is updated properly
199
+ settings_rediss_param = Settings(redis_url="rediss://localhost:6379/0?foo=bar")
200
+ self.assertEqual(settings_rediss_param.redis_url, "rediss://localhost:6379/0?foo=bar&ssl_cert_reqs=CERT_NONE")
201
+
202
+ # Test rediss URL that already has ssl_cert_reqs is untouched
203
+ settings_rediss_explicit = Settings(redis_url="rediss://localhost:6379/0?ssl_cert_reqs=CERT_REQUIRED")
204
+ self.assertEqual(settings_rediss_explicit.redis_url, "rediss://localhost:6379/0?ssl_cert_reqs=CERT_REQUIRED")
205
+
206
  if __name__ == '__main__':
207
  unittest.main()
web/src/components.d.ts CHANGED
@@ -11,16 +11,32 @@ declare module 'vue' {
11
  AuthModal: typeof import('./components/AuthModal.vue')['default']
12
  ChatMessage: typeof import('./components/ChatMessage.vue')['default']
13
  NAlert: typeof import('naive-ui')['NAlert']
 
 
 
14
  NButton: typeof import('naive-ui')['NButton']
 
 
15
  NConfigProvider: typeof import('naive-ui')['NConfigProvider']
 
 
16
  NDialogProvider: typeof import('naive-ui')['NDialogProvider']
 
 
17
  NIcon: typeof import('naive-ui')['NIcon']
18
  NInput: typeof import('naive-ui')['NInput']
 
 
19
  NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
20
  NMessageProvider: typeof import('naive-ui')['NMessageProvider']
21
  NModal: typeof import('naive-ui')['NModal']
22
  NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
23
  NResult: typeof import('naive-ui')['NResult']
 
 
 
 
 
24
  ReportCard: typeof import('./components/ReportCard.vue')['default']
25
  ReportListItem: typeof import('./components/ReportListItem.vue')['default']
26
  RouterLink: typeof import('vue-router')['RouterLink']
 
11
  AuthModal: typeof import('./components/AuthModal.vue')['default']
12
  ChatMessage: typeof import('./components/ChatMessage.vue')['default']
13
  NAlert: typeof import('naive-ui')['NAlert']
14
+ NAvatar: typeof import('naive-ui')['NAvatar']
15
+ NBadge: typeof import('naive-ui')['NBadge']
16
+ NBlockquote: typeof import('naive-ui')['NBlockquote']
17
  NButton: typeof import('naive-ui')['NButton']
18
+ NButtonGroup: typeof import('naive-ui')['NButtonGroup']
19
+ NCard: typeof import('naive-ui')['NCard']
20
  NConfigProvider: typeof import('naive-ui')['NConfigProvider']
21
+ NDescriptions: typeof import('naive-ui')['NDescriptions']
22
+ NDescriptionsItem: typeof import('naive-ui')['NDescriptionsItem']
23
  NDialogProvider: typeof import('naive-ui')['NDialogProvider']
24
+ NDivider: typeof import('naive-ui')['NDivider']
25
+ NEmpty: typeof import('naive-ui')['NEmpty']
26
  NIcon: typeof import('naive-ui')['NIcon']
27
  NInput: typeof import('naive-ui')['NInput']
28
+ NList: typeof import('naive-ui')['NList']
29
+ NListItem: typeof import('naive-ui')['NListItem']
30
  NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
31
  NMessageProvider: typeof import('naive-ui')['NMessageProvider']
32
  NModal: typeof import('naive-ui')['NModal']
33
  NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
34
  NResult: typeof import('naive-ui')['NResult']
35
+ NSkeleton: typeof import('naive-ui')['NSkeleton']
36
+ NStep: typeof import('naive-ui')['NStep']
37
+ NSteps: typeof import('naive-ui')['NSteps']
38
+ NTag: typeof import('naive-ui')['NTag']
39
+ NThing: typeof import('naive-ui')['NThing']
40
  ReportCard: typeof import('./components/ReportCard.vue')['default']
41
  ReportListItem: typeof import('./components/ReportListItem.vue')['default']
42
  RouterLink: typeof import('vue-router')['RouterLink']