@@ -56,6 +56,68 @@ def _get_default_cron_schedule(self, ctx: ApiContext) -> str:
5656 )
5757 return cron_schedule
5858
59+ def _validate_cron_schedule_for_source (
60+ self , cron_schedule : str , source : schemas .Source , ctx : ApiContext
61+ ) -> None :
62+ """Validate CRON schedule based on source capabilities.
63+
64+ Args:
65+ cron_schedule: The CRON expression to validate
66+ source: The source model
67+ ctx: API context
68+
69+ Raises:
70+ HTTPException: If the schedule is invalid for the source
71+ """
72+ import re
73+
74+ if not cron_schedule :
75+ return
76+
77+ # Parse the CRON expression to check if it's minute-level
78+ # We need to distinguish between:
79+ # - "*/N * * * *" where N < 60 - runs every N minutes (minute-level)
80+ # - "* * * * *" - runs every minute (minute-level)
81+ # - "0 * * * *" - runs at minute 0 of every hour (hourly, not minute-level)
82+ # - "30 2 * * *" - runs at 2:30 AM daily (daily, not minute-level)
83+
84+ # Check for patterns that run more frequently than hourly
85+ # Pattern 1: */N where N < 60 (e.g., */5, */15, */30)
86+ interval_pattern = r"^\*/([1-5]?[0-9]) \* \* \* \*$"
87+ match = re .match (interval_pattern , cron_schedule )
88+
89+ if match :
90+ interval = int (match .group (1 ))
91+ if interval < 60 :
92+ # This is sub-hourly (minute-level)
93+ if not source .supports_continuous :
94+ raise HTTPException (
95+ status_code = 400 ,
96+ detail = f"Source '{ source .short_name } ' does not support continuous syncs. "
97+ f"Minimum schedule interval is 1 hour (e.g., '0 * * * *' for hourly)." ,
98+ )
99+ # For continuous sources, sub-hourly is allowed
100+ ctx .logger .info (
101+ f"Source '{ source .short_name } ' supports continuous syncs, "
102+ f"allowing minute-level schedule: { cron_schedule } "
103+ )
104+ return
105+
106+ # Pattern 2: * * * * * (every minute)
107+ if cron_schedule == "* * * * *" :
108+ if not source .supports_continuous :
109+ raise HTTPException (
110+ status_code = 400 ,
111+ detail = f"Source '{ source .short_name } ' does not support continuous syncs. "
112+ f"Minimum schedule interval is 1 hour (e.g., '0 * * * *' for hourly)." ,
113+ )
114+ ctx .logger .info (
115+ f"Source '{ source .short_name } ' supports continuous syncs, "
116+ f"allowing every-minute schedule: { cron_schedule } "
117+ )
118+
119+ # All other patterns (including "0 * * * *" for hourly) are allowed
120+
59121 """Clean service with automatic auth method inference.
60122
61123 Key improvements:
@@ -225,11 +287,15 @@ async def update(
225287 ctx : ApiContext ,
226288 ) -> SourceConnection :
227289 """Update a source connection."""
290+ # First check if the source connection exists
228291 source_conn = await crud .source_connection .get (db , id = id , ctx = ctx )
229292 if not source_conn :
230293 raise HTTPException (status_code = 404 , detail = "Source connection not found" )
231294
232295 async with UnitOfWork (db ) as uow :
296+ # Re-fetch the source_conn within the UoW session to avoid session mismatch
297+ source_conn = await crud .source_connection .get (uow .session , id = id , ctx = ctx )
298+
233299 # Update fields
234300 update_data = obj_in .model_dump (exclude_unset = True )
235301
@@ -242,20 +308,32 @@ async def update(
242308 del update_data ["config" ]
243309
244310 # Handle schedule update
245- if "schedule" in update_data and update_data ["schedule" ]:
311+ if (
312+ "schedule" in update_data and update_data ["schedule" ]
313+ ): # TODO: only if actually different
246314 if source_conn .sync_id :
315+ new_cron = update_data ["schedule" ].get ("cron" )
316+ if new_cron :
317+ # Get the source to validate schedule
318+ source = await self ._get_and_validate_source (
319+ uow .session , source_conn .short_name
320+ )
321+ self ._validate_cron_schedule_for_source (new_cron , source , ctx )
247322 await self ._update_sync_schedule (
248323 uow .session ,
249324 source_conn .sync_id ,
250- update_data [ "schedule" ]. get ( "cron" ) ,
325+ new_cron ,
251326 ctx ,
252327 uow ,
253328 )
254329 del update_data ["schedule" ]
255330
256331 # Handle credential update (direct auth only)
257332 if "credentials" in update_data :
258- auth_method = self ._determine_auth_method (source_conn )
333+ # Use the schema function that works with database models
334+ from airweave .schemas .source_connection import determine_auth_method
335+
336+ auth_method = determine_auth_method (source_conn )
259337 if auth_method != AuthenticationMethod .DIRECT :
260338 raise HTTPException (
261339 status_code = 400 ,
@@ -365,6 +443,9 @@ async def _create_with_direct_auth(
365443 if cron_schedule is None :
366444 cron_schedule = self ._get_default_cron_schedule (ctx )
367445
446+ # Validate the schedule for this source
447+ self ._validate_cron_schedule_for_source (cron_schedule , source , ctx )
448+
368449 sync , sync_job = await self ._create_sync_without_schedule (
369450 uow .session ,
370451 obj_in .name ,
@@ -400,6 +481,7 @@ async def _create_with_direct_auth(
400481 cron_schedule = cron_schedule ,
401482 db = uow .session ,
402483 ctx = ctx ,
484+ uow = uow ,
403485 )
404486
405487 # Convert to schemas while still in session
@@ -582,6 +664,9 @@ async def _create_with_oauth_token(
582664 if cron_schedule is None :
583665 cron_schedule = self ._get_default_cron_schedule (ctx )
584666
667+ # Validate the schedule for this source
668+ self ._validate_cron_schedule_for_source (cron_schedule , source , ctx )
669+
585670 sync , sync_job = await self ._create_sync_without_schedule (
586671 uow .session ,
587672 obj_in .name ,
@@ -617,6 +702,7 @@ async def _create_with_oauth_token(
617702 cron_schedule = cron_schedule ,
618703 db = uow .session ,
619704 ctx = ctx ,
705+ uow = uow ,
620706 )
621707
622708 # Convert to schemas while still in session
@@ -728,6 +814,9 @@ async def _create_with_auth_provider(
728814 if cron_schedule is None :
729815 cron_schedule = self ._get_default_cron_schedule (ctx )
730816
817+ # Validate the schedule for this source
818+ self ._validate_cron_schedule_for_source (cron_schedule , source , ctx )
819+
731820 sync , sync_job = await self ._create_sync_without_schedule (
732821 uow .session ,
733822 obj_in .name ,
@@ -765,6 +854,7 @@ async def _create_with_auth_provider(
765854 cron_schedule = cron_schedule ,
766855 db = uow .session ,
767856 ctx = ctx ,
857+ uow = uow ,
768858 )
769859
770860 # Convert to schemas while still in session
0 commit comments