kodit 0.3.4__py3-none-any.whl → 0.3.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kodit might be problematic. Click here for more details.

kodit/_version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.3.4'
21
- __version_tuple__ = version_tuple = (0, 3, 4)
20
+ __version__ = version = '0.3.6'
21
+ __version_tuple__ = version_tuple = (0, 3, 6)
kodit/app.py CHANGED
@@ -6,28 +6,47 @@ from contextlib import asynccontextmanager
6
6
  from asgi_correlation_id import CorrelationIdMiddleware
7
7
  from fastapi import FastAPI
8
8
 
9
+ from kodit.application.services.sync_scheduler import SyncSchedulerService
9
10
  from kodit.config import AppContext
10
11
  from kodit.infrastructure.indexing.auto_indexing_service import AutoIndexingService
11
12
  from kodit.mcp import mcp
12
13
  from kodit.middleware import ASGICancelledErrorMiddleware, logging_middleware
13
14
 
14
- # Global auto-indexing service
15
+ # Global services
15
16
  _auto_indexing_service: AutoIndexingService | None = None
17
+ _sync_scheduler_service: SyncSchedulerService | None = None
16
18
 
17
19
 
18
20
  @asynccontextmanager
19
21
  async def app_lifespan(_: FastAPI) -> AsyncIterator[None]:
20
- """Manage application lifespan for auto-indexing."""
21
- global _auto_indexing_service # noqa: PLW0603
22
- # Start auto-indexing service
22
+ """Manage application lifespan for auto-indexing and sync."""
23
+ global _auto_indexing_service, _sync_scheduler_service # noqa: PLW0603
24
+
23
25
  app_context = AppContext()
24
26
  db = await app_context.get_db()
27
+
28
+ # Start auto-indexing service
25
29
  _auto_indexing_service = AutoIndexingService(
26
30
  app_context=app_context,
27
31
  session_factory=db.session_factory,
28
32
  )
29
33
  await _auto_indexing_service.start_background_indexing()
34
+
35
+ # Start sync scheduler service
36
+ if app_context.periodic_sync.enabled:
37
+ _sync_scheduler_service = SyncSchedulerService(
38
+ app_context=app_context,
39
+ session_factory=db.session_factory,
40
+ )
41
+ _sync_scheduler_service.start_periodic_sync(
42
+ interval_seconds=app_context.periodic_sync.interval_seconds
43
+ )
44
+
30
45
  yield
46
+
47
+ # Stop services
48
+ if _sync_scheduler_service:
49
+ await _sync_scheduler_service.stop_periodic_sync()
31
50
  if _auto_indexing_service:
32
51
  await _auto_indexing_service.stop()
33
52
 
@@ -0,0 +1,128 @@
1
+ """Service for scheduling periodic sync operations."""
2
+
3
+ import asyncio
4
+ from collections.abc import Callable
5
+ from contextlib import suppress
6
+
7
+ import structlog
8
+ from sqlalchemy.ext.asyncio import AsyncSession
9
+
10
+ from kodit.application.factories.code_indexing_factory import (
11
+ create_code_indexing_application_service,
12
+ )
13
+ from kodit.config import AppContext
14
+ from kodit.domain.services.index_query_service import IndexQueryService
15
+ from kodit.infrastructure.indexing.fusion_service import ReciprocalRankFusionService
16
+ from kodit.infrastructure.sqlalchemy.index_repository import SqlAlchemyIndexRepository
17
+
18
+
19
+ class SyncSchedulerService:
20
+ """Service for scheduling periodic sync operations."""
21
+
22
+ def __init__(
23
+ self,
24
+ app_context: AppContext,
25
+ session_factory: Callable[[], AsyncSession],
26
+ ) -> None:
27
+ """Initialize the sync scheduler service."""
28
+ self.app_context = app_context
29
+ self.session_factory = session_factory
30
+ self.log = structlog.get_logger(__name__)
31
+ self._sync_task: asyncio.Task | None = None
32
+ self._shutdown_event = asyncio.Event()
33
+
34
+ def start_periodic_sync(self, interval_seconds: float = 1800) -> None:
35
+ """Start periodic sync of all indexes."""
36
+ self.log.info("Starting periodic sync", interval_seconds=interval_seconds)
37
+
38
+ self._sync_task = asyncio.create_task(self._sync_loop(interval_seconds))
39
+
40
+ async def stop_periodic_sync(self) -> None:
41
+ """Stop the periodic sync task."""
42
+ self.log.info("Stopping periodic sync")
43
+ self._shutdown_event.set()
44
+
45
+ if self._sync_task and not self._sync_task.done():
46
+ self._sync_task.cancel()
47
+ with suppress(asyncio.CancelledError):
48
+ await self._sync_task
49
+
50
+ async def _sync_loop(self, interval_seconds: float) -> None:
51
+ """Run the sync loop at the specified interval."""
52
+ while not self._shutdown_event.is_set():
53
+ try:
54
+ await self._perform_sync()
55
+ except Exception as e:
56
+ self.log.exception("Sync operation failed", error=e)
57
+
58
+ # Wait for the interval or until shutdown
59
+ try:
60
+ await asyncio.wait_for(
61
+ self._shutdown_event.wait(), timeout=interval_seconds
62
+ )
63
+ # If we reach here, shutdown was requested
64
+ break
65
+ except TimeoutError:
66
+ # Continue to next sync cycle
67
+ continue
68
+
69
+ async def _perform_sync(self) -> None:
70
+ """Perform a sync operation on all indexes."""
71
+ self.log.info("Starting sync operation")
72
+
73
+ async with self.session_factory() as session:
74
+ # Create services
75
+ service = create_code_indexing_application_service(
76
+ app_context=self.app_context,
77
+ session=session,
78
+ )
79
+ index_query_service = IndexQueryService(
80
+ index_repository=SqlAlchemyIndexRepository(session=session),
81
+ fusion_service=ReciprocalRankFusionService(),
82
+ )
83
+
84
+ # Get all existing indexes
85
+ all_indexes = await index_query_service.list_indexes()
86
+
87
+ if not all_indexes:
88
+ self.log.info("No indexes found to sync")
89
+ return
90
+
91
+ self.log.info("Syncing indexes", count=len(all_indexes))
92
+
93
+ success_count = 0
94
+ failure_count = 0
95
+
96
+ # Sync each index
97
+ for index in all_indexes:
98
+ try:
99
+ self.log.info(
100
+ "Syncing index",
101
+ index_id=index.id,
102
+ source=str(index.source.working_copy.remote_uri),
103
+ )
104
+
105
+ await service.run_index(index, progress_callback=None)
106
+ success_count += 1
107
+
108
+ self.log.info(
109
+ "Index sync completed",
110
+ index_id=index.id,
111
+ source=str(index.source.working_copy.remote_uri),
112
+ )
113
+
114
+ except Exception as e:
115
+ failure_count += 1
116
+ self.log.exception(
117
+ "Index sync failed",
118
+ index_id=index.id,
119
+ source=str(index.source.working_copy.remote_uri),
120
+ error=e,
121
+ )
122
+
123
+ self.log.info(
124
+ "Sync operation completed",
125
+ total=len(all_indexes),
126
+ success=success_count,
127
+ failures=failure_count,
128
+ )
kodit/cli.py CHANGED
@@ -63,11 +63,105 @@ def cli(
63
63
  ctx.obj = config
64
64
 
65
65
 
66
+ async def _handle_auto_index(
67
+ app_context: AppContext,
68
+ sources: list[str], # noqa: ARG001
69
+ ) -> list[str]:
70
+ """Handle auto-index option and return sources to process."""
71
+ log = structlog.get_logger(__name__)
72
+ log.info("Auto-indexing configuration", config=app_context.auto_indexing)
73
+ if not app_context.auto_indexing or not app_context.auto_indexing.sources:
74
+ click.echo("No auto-index sources configured.")
75
+ return []
76
+ auto_sources = app_context.auto_indexing.sources
77
+ click.echo(f"Auto-indexing {len(auto_sources)} configured sources...")
78
+ return [source.uri for source in auto_sources]
79
+
80
+
81
+ async def _handle_sync(
82
+ service: Any,
83
+ index_query_service: IndexQueryService,
84
+ sources: list[str],
85
+ ) -> None:
86
+ """Handle sync operation."""
87
+ log = structlog.get_logger(__name__)
88
+ log_event("kodit.cli.index.sync")
89
+
90
+ # Get all existing indexes
91
+ all_indexes = await index_query_service.list_indexes()
92
+
93
+ if not all_indexes:
94
+ click.echo("No existing indexes found to sync.")
95
+ return
96
+
97
+ # Filter indexes if specific sources are provided
98
+ indexes_to_sync = all_indexes
99
+ if sources:
100
+ # Filter indexes that match the provided sources
101
+ source_uris = set(sources)
102
+ indexes_to_sync = [
103
+ index for index in all_indexes
104
+ if str(index.source.working_copy.remote_uri) in source_uris
105
+ ]
106
+
107
+ if not indexes_to_sync:
108
+ click.echo(
109
+ f"No indexes found for the specified sources: {', '.join(sources)}"
110
+ )
111
+ return
112
+
113
+ click.echo(f"Syncing {len(indexes_to_sync)} indexes...")
114
+
115
+ # Sync each index
116
+ for index in indexes_to_sync:
117
+ click.echo(f"Syncing: {index.source.working_copy.remote_uri}")
118
+
119
+ # Create progress callback for this sync operation
120
+ progress_callback = create_multi_stage_progress_callback()
121
+
122
+ try:
123
+ await service.run_index(index, progress_callback)
124
+ click.echo(f"✓ Sync completed: {index.source.working_copy.remote_uri}")
125
+ except Exception as e:
126
+ log.exception("Sync failed", index_id=index.id, error=e)
127
+ click.echo(
128
+ f"✗ Sync failed: {index.source.working_copy.remote_uri} - {e}"
129
+ )
130
+
131
+
132
+ async def _handle_list_indexes(index_query_service: IndexQueryService) -> None:
133
+ """Handle listing all indexes."""
134
+ log_event("kodit.cli.index.list")
135
+ # No source specified, list all indexes
136
+ indexes = await index_query_service.list_indexes()
137
+ headers: list[str | Cell] = [
138
+ "ID",
139
+ "Created At",
140
+ "Updated At",
141
+ "Source",
142
+ "Num Snippets",
143
+ ]
144
+ data = [
145
+ [
146
+ index.id,
147
+ index.created_at,
148
+ index.updated_at,
149
+ index.source.working_copy.remote_uri,
150
+ len(index.source.working_copy.files),
151
+ ]
152
+ for index in indexes
153
+ ]
154
+ click.echo(Table(headers=headers, data=data))
155
+
156
+
66
157
  @cli.command()
67
158
  @click.argument("sources", nargs=-1)
68
159
  @click.option(
69
160
  "--auto-index", is_flag=True, help="Index all configured auto-index sources"
70
161
  )
162
+ @click.option(
163
+ "--sync", is_flag=True, help="Sync existing indexes with their remotes"
164
+ )
71
165
  @with_app_context
72
166
  @with_session
73
167
  async def index(
@@ -76,8 +170,9 @@ async def index(
76
170
  sources: list[str],
77
171
  *, # Force keyword-only arguments
78
172
  auto_index: bool,
173
+ sync: bool,
79
174
  ) -> None:
80
- """List indexes, or index data sources."""
175
+ """List indexes, index data sources, or sync existing indexes."""
81
176
  log = structlog.get_logger(__name__)
82
177
  service = create_code_indexing_application_service(
83
178
  app_context=app_context,
@@ -89,36 +184,16 @@ async def index(
89
184
  )
90
185
 
91
186
  if auto_index:
92
- log.info("Auto-indexing configuration", config=app_context.auto_indexing)
93
- if not app_context.auto_indexing or not app_context.auto_indexing.sources:
94
- click.echo("No auto-index sources configured.")
187
+ sources = await _handle_auto_index(app_context, sources)
188
+ if not sources:
95
189
  return
96
- auto_sources = app_context.auto_indexing.sources
97
- click.echo(f"Auto-indexing {len(auto_sources)} configured sources...")
98
- sources = [source.uri for source in auto_sources]
190
+
191
+ if sync:
192
+ await _handle_sync(service, index_query_service, sources)
193
+ return
99
194
 
100
195
  if not sources:
101
- log_event("kodit.cli.index.list")
102
- # No source specified, list all indexes
103
- indexes = await index_query_service.list_indexes()
104
- headers: list[str | Cell] = [
105
- "ID",
106
- "Created At",
107
- "Updated At",
108
- "Source",
109
- "Num Snippets",
110
- ]
111
- data = [
112
- [
113
- index.id,
114
- index.created_at,
115
- index.updated_at,
116
- index.source.working_copy.remote_uri,
117
- len(index.source.working_copy.files),
118
- ]
119
- for index in indexes
120
- ]
121
- click.echo(Table(headers=headers, data=data))
196
+ await _handle_list_indexes(index_query_service)
122
197
  return
123
198
  # Handle source indexing
124
199
  for source in sources:
kodit/config.py CHANGED
@@ -81,6 +81,18 @@ class AutoIndexingConfig(BaseModel):
81
81
  return v
82
82
 
83
83
 
84
+ class PeriodicSyncConfig(BaseModel):
85
+ """Configuration for periodic/scheduled syncing."""
86
+
87
+ enabled: bool = Field(default=True, description="Enable periodic sync")
88
+ interval_seconds: float = Field(
89
+ default=1800, description="Interval between automatic syncs in seconds"
90
+ )
91
+ retry_attempts: int = Field(
92
+ default=3, description="Number of retry attempts for failed syncs"
93
+ )
94
+
95
+
84
96
  class CustomAutoIndexingEnvSource(EnvSettingsSource):
85
97
  """Custom environment source for parsing AutoIndexingConfig."""
86
98
 
@@ -173,6 +185,9 @@ class AppContext(BaseSettings):
173
185
  auto_indexing: AutoIndexingConfig | None = Field(
174
186
  default=AutoIndexingConfig(), description="Auto-indexing configuration"
175
187
  )
188
+ periodic_sync: PeriodicSyncConfig = Field(
189
+ default=PeriodicSyncConfig(), description="Periodic sync configuration"
190
+ )
176
191
  _db: Database | None = None
177
192
 
178
193
  def model_post_init(self, _: Any) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kodit
3
- Version: 0.3.4
3
+ Version: 0.3.6
4
4
  Summary: Code indexing for better AI code generation
5
5
  Project-URL: Homepage, https://docs.helixml.tech/kodit/
6
6
  Project-URL: Documentation, https://docs.helixml.tech/kodit/
@@ -102,6 +102,7 @@ code. This index is used to build a snippet library, ready for ingestion into an
102
102
  - **NEW in 0.3**: Index private repositories via a PAT
103
103
  - **NEW in 0.3**: Improved progress monitoring and reporting during indexing
104
104
  - **NEW in 0.3**: Advanced code slicing infrastructure with Tree-sitter parsing
105
+ - **NEW in 0.4**: Automatic periodic sync to keep indexes up-to-date
105
106
 
106
107
  ### MCP Server
107
108
 
@@ -1,9 +1,9 @@
1
1
  kodit/.gitignore,sha256=ztkjgRwL9Uud1OEi36hGQeDGk3OLK1NfDEO8YqGYy8o,11
2
2
  kodit/__init__.py,sha256=aEKHYninUq1yh6jaNfvJBYg-6fenpN132nJt1UU6Jxs,59
3
- kodit/_version.py,sha256=Mxups7YfGBY2vvCok_hLJKCtU6O1WHQJfsMY-bAJ0Yg,511
4
- kodit/app.py,sha256=uv67TE83fZE7wrA7cz-sKosFrAXlKRr1B7fT-X_gMZQ,2103
5
- kodit/cli.py,sha256=xh2MNA4sUTA3_yVbHGz-CRpbS0TTzKwf33XbScA74Gw,16626
6
- kodit/config.py,sha256=VUoUi2t2yGhqOtm5MSZuaasNSklH50hfWn6GOrz3jnU,7518
3
+ kodit/_version.py,sha256=5ZRqa3YWFznn7rzKg8d9Vi2wEKohJwIFxhrpOUL3pYQ,511
4
+ kodit/app.py,sha256=Q6M9XcMY4s-srHiWjvaPrrkzUveYBRGMOgnlomoIDRo,2738
5
+ kodit/cli.py,sha256=7iP1rHVoObnKosNJD87a_JBdidFJtIxo_1fpkELb_jc,18894
6
+ kodit/config.py,sha256=Dh1ybowJyOqZIoQnvF3DykaX2ze1MC0Rjs9oK1iZ7cs,8060
7
7
  kodit/database.py,sha256=kI9yBm4uunsgV4-QeVoCBL0wLzU4kYmYv5qZilGnbPE,1740
8
8
  kodit/log.py,sha256=WOsLRitpCBtJa5IcsyZpKr146kXXHK2nU5VA90gcJdQ,8736
9
9
  kodit/mcp.py,sha256=OPscMbGQ05nFHJ_UkntobocZ6Y9wO2ZyRx1tVj7XSsY,6016
@@ -14,6 +14,7 @@ kodit/application/factories/__init__.py,sha256=bU5CvEnaBePZ7JbkCOp1MGTNP752bnU2u
14
14
  kodit/application/factories/code_indexing_factory.py,sha256=R9f0wsj4-3NJFS5SEt_-OIGR_s_01gJXaL3PkZd8MlU,5911
15
15
  kodit/application/services/__init__.py,sha256=p5UQNw-H5sxQvs5Etfte93B3cJ1kKW6DNxK34uFvU1E,38
16
16
  kodit/application/services/code_indexing_application_service.py,sha256=SuIuyBoSPOSjj5VaXIbxcYqaTEeMuUCu7w1tO8orrOY,14656
17
+ kodit/application/services/sync_scheduler.py,sha256=7ZWM0ACiOrTcsW300m52fTqfWMLFmgRRZ6YUPrgUaUk,4621
17
18
  kodit/domain/__init__.py,sha256=TCpg4Xx-oF4mKV91lo4iXqMEfBT1OoRSYnbG-zVWolA,66
18
19
  kodit/domain/entities.py,sha256=Mcku1Wmk3Xl3YJhY65_RoiLeffOLKOHI0uCAXWJrmvQ,8698
19
20
  kodit/domain/errors.py,sha256=yIsgCjM_yOFIg8l7l-t7jM8pgeAX4cfPq0owf7iz3DA,106
@@ -82,8 +83,8 @@ kodit/migrations/versions/__init__.py,sha256=9-lHzptItTzq_fomdIRBegQNm4Znx6pVjwD
82
83
  kodit/migrations/versions/c3f5137d30f5_index_all_the_things.py,sha256=r7ukmJ_axXLAWewYx-F1fEmZ4JbtFd37i7cSb0tq3y0,1722
83
84
  kodit/utils/__init__.py,sha256=DPEB1i8evnLF4Ns3huuAYg-0pKBFKUFuiDzOKG9r-sw,33
84
85
  kodit/utils/path_utils.py,sha256=thK6YGGNvQThdBaCYCCeCvS1L8x-lwl3AoGht2jnjGw,1645
85
- kodit-0.3.4.dist-info/METADATA,sha256=rZKGzihHJy8kShI8lybNHWbhB7z4knuoLGLZ3ba987w,6871
86
- kodit-0.3.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
87
- kodit-0.3.4.dist-info/entry_points.txt,sha256=hoTn-1aKyTItjnY91fnO-rV5uaWQLQ-Vi7V5et2IbHY,40
88
- kodit-0.3.4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
89
- kodit-0.3.4.dist-info/RECORD,,
86
+ kodit-0.3.6.dist-info/METADATA,sha256=-niGc2LDxpWdJXSNSFCu9Xwp0nu6O96Be-Cb1RTTwSA,6940
87
+ kodit-0.3.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
88
+ kodit-0.3.6.dist-info/entry_points.txt,sha256=hoTn-1aKyTItjnY91fnO-rV5uaWQLQ-Vi7V5et2IbHY,40
89
+ kodit-0.3.6.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
90
+ kodit-0.3.6.dist-info/RECORD,,
File without changes