compair-core 0.3.10__py3-none-any.whl → 0.3.12__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 compair-core might be problematic. Click here for more details.

compair_core/api.py CHANGED
@@ -13,6 +13,7 @@ import psutil
13
13
  from celery.result import AsyncResult
14
14
  from fastapi import APIRouter, Body, Depends, File, Form, Header, HTTPException, Query, Request, UploadFile
15
15
  from fastapi.responses import HTMLResponse, RedirectResponse, StreamingResponse
16
+ from fastapi.routing import APIRoute
16
17
  from sqlalchemy import distinct, func, select, or_
17
18
  from sqlalchemy.orm import joinedload, Session
18
19
 
@@ -46,6 +47,7 @@ redis_client = redis.Redis.from_url(redis_url) if (redis and redis_url) else Non
46
47
  #from compair.main import process_document
47
48
 
48
49
  router = APIRouter()
50
+ core_router = APIRouter()
49
51
  WEB_URL = os.environ.get("WEB_URL")
50
52
  ADMIN_API_KEY = os.environ.get("ADMIN_API_KEY")
51
53
 
@@ -59,6 +61,112 @@ GA4_MEASUREMENT_ID = os.getenv("GA4_MEASUREMENT_ID")
59
61
  GA4_API_SECRET = os.getenv("GA4_API_SECRET")
60
62
 
61
63
  IS_CLOUD = os.getenv("COMPAIR_EDITION", "core").lower() == "cloud"
64
+ SINGLE_USER_SESSION_TTL = timedelta(days=365)
65
+
66
+
67
+ def _ensure_single_user(session: Session, settings: Settings) -> models.User:
68
+ """Create or fetch the singleton user used when authentication is disabled."""
69
+ changed = False
70
+ user = (
71
+ session.query(models.User)
72
+ .options(joinedload(models.User.groups))
73
+ .filter(models.User.username == settings.single_user_username)
74
+ .first()
75
+ )
76
+ if user is None:
77
+ now = datetime.now(timezone.utc)
78
+ user = models.User(
79
+ username=settings.single_user_username,
80
+ name=settings.single_user_name,
81
+ datetime_registered=now,
82
+ verification_token=None,
83
+ token_expiration=None,
84
+ )
85
+ user.set_password(secrets.token_urlsafe(16))
86
+ user.status = "active"
87
+ user.status_change_date = now
88
+ session.add(user)
89
+ session.flush()
90
+ admin = models.Administrator(user_id=user.user_id)
91
+ group = models.Group(
92
+ name=user.username,
93
+ datetime_created=now,
94
+ group_image=None,
95
+ category="Private",
96
+ description=f"Private workspace for {settings.single_user_name}",
97
+ visibility="private",
98
+ )
99
+ group.admins.append(admin)
100
+ user.groups = [group]
101
+ session.add_all([group, admin])
102
+ changed = True
103
+ else:
104
+ now = datetime.now(timezone.utc)
105
+ if user.status != "active":
106
+ user.status = "active"
107
+ user.status_change_date = now
108
+ changed = True
109
+ group = next((g for g in user.groups if g.name == user.username), None)
110
+ if group is None:
111
+ group = session.query(models.Group).filter(models.Group.name == user.username).first()
112
+ if group is None:
113
+ group = models.Group(
114
+ name=user.username,
115
+ datetime_created=now,
116
+ group_image=None,
117
+ category="Private",
118
+ description=f"Private workspace for {user.name}",
119
+ visibility="private",
120
+ )
121
+ session.add(group)
122
+ changed = True
123
+ if group not in user.groups:
124
+ user.groups.append(group)
125
+ changed = True
126
+ admin = session.query(models.Administrator).filter(models.Administrator.user_id == user.user_id).first()
127
+ if admin is None:
128
+ admin = models.Administrator(user_id=user.user_id)
129
+ session.add(admin)
130
+ changed = True
131
+ if admin not in group.admins:
132
+ group.admins.append(admin)
133
+ changed = True
134
+
135
+ if changed:
136
+ session.commit()
137
+ user = (
138
+ session.query(models.User)
139
+ .options(joinedload(models.User.groups))
140
+ .filter(models.User.username == settings.single_user_username)
141
+ .first()
142
+ )
143
+ if user is None:
144
+ raise RuntimeError("Failed to initialize the local Compair user.")
145
+ user.groups # ensure relationship is loaded before detaching
146
+ return user
147
+
148
+
149
+ def _ensure_single_user_session(session: Session, user: models.User) -> models.Session:
150
+ """Return a long-lived session token for the singleton user."""
151
+ now = datetime.now(timezone.utc)
152
+ existing = (
153
+ session.query(models.Session)
154
+ .filter(models.Session.user_id == user.user_id, models.Session.datetime_valid_until >= now)
155
+ .order_by(models.Session.datetime_valid_until.desc())
156
+ .first()
157
+ )
158
+ if existing:
159
+ return existing
160
+ token = secrets.token_urlsafe()
161
+ user_session = models.Session(
162
+ id=token,
163
+ user_id=user.user_id,
164
+ datetime_created=now,
165
+ datetime_valid_until=now + SINGLE_USER_SESSION_TTL,
166
+ )
167
+ session.add(user_session)
168
+ session.commit()
169
+ return user_session
62
170
 
63
171
 
64
172
  def require_cloud(feature: str) -> None:
@@ -90,7 +198,13 @@ def require_feature(flag: bool, feature: str) -> None:
90
198
  if not flag:
91
199
  raise HTTPException(status_code=501, detail=f"{feature} is only available in the Compair Cloud edition.")
92
200
 
93
- def get_current_user(auth_token: str = Header(...)):
201
+ def get_current_user(auth_token: str | None = Header(None)):
202
+ settings = get_settings_dependency()
203
+ if not settings.require_authentication:
204
+ with compair.Session() as session:
205
+ return _ensure_single_user(session, settings)
206
+ if not auth_token:
207
+ raise HTTPException(status_code=401, detail="Missing session token")
94
208
  with compair.Session() as session:
95
209
  user_session = session.query(models.Session).filter(models.Session.id == auth_token).first()
96
210
  if not user_session:
@@ -154,9 +268,20 @@ log_service_resource_metrics(service_name="backend") # or "frontend"
154
268
 
155
269
  @router.post("/login")
156
270
  def login(request: schema.LoginRequest) -> dict:
271
+ settings = get_settings_dependency()
157
272
  with compair.Session() as session:
273
+ if not settings.require_authentication:
274
+ user = _ensure_single_user(session, settings)
275
+ user_session = _ensure_single_user_session(session, user)
276
+ return {
277
+ "user_id": user.user_id,
278
+ "username": user.username,
279
+ "name": user.name,
280
+ "status": user.status,
281
+ "role": user.role,
282
+ "auth_token": user_session.id,
283
+ }
158
284
  user = session.query(models.User).filter(models.User.username == request.username).first()
159
- print("PW yo: {request.password}")
160
285
  if not user or not user.check_password(request.password):
161
286
  raise HTTPException(status_code=401, detail="Invalid credentials")
162
287
  if user.status == 'inactive':
@@ -530,8 +655,15 @@ def create_user(
530
655
 
531
656
 
532
657
  @router.get("/load_session")
533
- def load_session(auth_token: str) -> schema.Session | None:
658
+ def load_session(auth_token: str | None = None) -> schema.Session | None:
659
+ settings = get_settings_dependency()
660
+ if not settings.require_authentication:
661
+ with compair.Session() as session:
662
+ user = _ensure_single_user(session, settings)
663
+ return _ensure_single_user_session(session, user)
534
664
  with compair.Session() as session:
665
+ if not auth_token:
666
+ raise HTTPException(status_code=400, detail="auth_token is required when authentication is enabled.")
535
667
  user_session = session.query(models.Session).filter(models.Session.id == auth_token).first()
536
668
  if not user_session:
537
669
  raise HTTPException(status_code=404, detail="Session not found")
@@ -596,6 +728,9 @@ def update_session_duration(
596
728
  def delete_user(
597
729
  current_user: models.User = Depends(get_current_user)
598
730
  ):
731
+ settings = get_settings_dependency()
732
+ if not settings.require_authentication:
733
+ raise HTTPException(status_code=403, detail="Deleting the local user is not supported when authentication is disabled.")
599
734
  with compair.Session() as session:
600
735
  current_user.delete()
601
736
  session.commit()
@@ -1711,6 +1846,9 @@ def load_references(
1711
1846
 
1712
1847
  @router.get("/verify-email")
1713
1848
  def verify_email(token: str):
1849
+ settings = get_settings_dependency()
1850
+ if not settings.require_authentication:
1851
+ raise HTTPException(status_code=403, detail="Email verification is disabled when authentication is disabled.")
1714
1852
  with compair.Session() as session:
1715
1853
  print(token)
1716
1854
  user = session.query(models.User).filter(models.User.verification_token == token).first()
@@ -1769,6 +1907,9 @@ def sign_up(
1769
1907
  request: schema.SignUpRequest,
1770
1908
  analytics: Analytics = Depends(get_analytics),
1771
1909
  ) -> dict:
1910
+ settings = get_settings_dependency()
1911
+ if not settings.require_authentication:
1912
+ raise HTTPException(status_code=403, detail="Sign-up is disabled when authentication is disabled.")
1772
1913
  print('1')
1773
1914
  if not is_valid_email(request.username):
1774
1915
  raise HTTPException(status_code=400, detail="Invalid email address")
@@ -1802,6 +1943,9 @@ def sign_up(
1802
1943
 
1803
1944
  @router.post("/forgot-password")
1804
1945
  def forgot_password(request: schema.ForgotPasswordRequest) -> dict:
1946
+ settings = get_settings_dependency()
1947
+ if not settings.require_authentication:
1948
+ raise HTTPException(status_code=403, detail="Password resets are disabled when authentication is disabled.")
1805
1949
  print('1')
1806
1950
  with compair.Session() as session:
1807
1951
  print('2')
@@ -1834,6 +1978,9 @@ def forgot_password(request: schema.ForgotPasswordRequest) -> dict:
1834
1978
 
1835
1979
  @router.post("/reset-password")
1836
1980
  def reset_password(request: schema.ResetPasswordRequest) -> dict:
1981
+ settings = get_settings_dependency()
1982
+ if not settings.require_authentication:
1983
+ raise HTTPException(status_code=403, detail="Password resets are disabled when authentication is disabled.")
1837
1984
  with compair.Session() as session:
1838
1985
  print('1')
1839
1986
  print(request.token)
@@ -3352,6 +3499,34 @@ def submit_deactivate_request(
3352
3499
  return {"message": f"We’ve received your request and will delete your account and data shortly. If you change your mind, reach out within 24 hours at {EMAIL_USER}."}
3353
3500
 
3354
3501
 
3502
+ CORE_PATHS: set[str] = {
3503
+ "/login",
3504
+ "/load_session",
3505
+ "/load_groups",
3506
+ "/load_group",
3507
+ "/create_group",
3508
+ "/join_group",
3509
+ "/load_group_users",
3510
+ "/delete_group",
3511
+ "/load_documents",
3512
+ "/load_document",
3513
+ "/load_document_by_id",
3514
+ "/create_doc",
3515
+ "/process_doc",
3516
+ "/status/{task_id}",
3517
+ "/upload/ocr-file",
3518
+ "/ocr-file-result/{task_id}",
3519
+ "/load_chunks",
3520
+ "/load_references",
3521
+ "/load_feedback",
3522
+ "/documents/{document_id}/feedback",
3523
+ }
3524
+
3525
+ for route in router.routes:
3526
+ if isinstance(route, APIRoute) and route.path in CORE_PATHS:
3527
+ core_router.routes.append(route)
3528
+
3529
+
3355
3530
  def create_fastapi_app():
3356
3531
  """Backwards-compatible app factory for running this module directly."""
3357
3532
  from fastapi import FastAPI
@@ -28,9 +28,15 @@ def create_app(settings: Settings | None = None) -> FastAPI:
28
28
 
29
29
  app = FastAPI(title="Compair API", version=resolved_settings.version)
30
30
 
31
- from ..api import router as legacy_router
31
+ from ..api import core_router, router as legacy_router
32
32
 
33
- app.include_router(legacy_router)
33
+ if edition == "cloud":
34
+ app.include_router(legacy_router)
35
+ else:
36
+ if resolved_settings.include_legacy_routes:
37
+ app.include_router(legacy_router)
38
+ else:
39
+ app.include_router(core_router)
34
40
  app.include_router(capabilities_router)
35
41
 
36
42
  # Share the resolved settings with request handlers
@@ -11,10 +11,13 @@ router = APIRouter(tags=["meta"])
11
11
  @router.get("/capabilities")
12
12
  def capabilities(settings: Settings = Depends(get_settings)) -> dict[str, object]:
13
13
  edition = settings.edition.lower()
14
+ require_auth = settings.require_authentication
14
15
  return {
15
16
  "auth": {
16
17
  "device_flow": edition == "cloud",
17
- "password_login": True,
18
+ "password_login": require_auth,
19
+ "required": require_auth,
20
+ "single_user": not require_auth,
18
21
  },
19
22
  "inputs": {
20
23
  "text": True,
@@ -35,4 +38,5 @@ def capabilities(settings: Settings = Depends(get_settings)) -> dict[str, object
35
38
  },
36
39
  "server": "Compair Cloud" if edition == "cloud" else "Compair Core",
37
40
  "version": settings.version,
41
+ "legacy_routes": settings.include_legacy_routes,
38
42
  }
@@ -16,6 +16,10 @@ class Settings(BaseSettings):
16
16
  billing_enabled: bool = False
17
17
  integrations_enabled: bool = False
18
18
  premium_models: bool = False
19
+ require_authentication: bool = False
20
+ single_user_username: str = "compair-local@example.com"
21
+ single_user_name: str = "Compair Local User"
22
+ include_legacy_routes: bool = False
19
23
 
20
24
  # Core/local storage defaults
21
25
  local_upload_dir: str = "~/.compair-core/data/uploads"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: compair-core
3
- Version: 0.3.10
3
+ Version: 0.3.12
4
4
  Summary: Open-source foundation of the Compair collaboration platform.
5
5
  Author: RocketResearch, Inc.
6
6
  License: MIT
@@ -89,6 +89,9 @@ Key environment variables for the core edition:
89
89
  - `COMPAIR_SQLITE_DIR` / `COMPAIR_SQLITE_NAME` – override the default local SQLite path (falls back to `./compair_data` if `/data` is not writable).
90
90
  - `COMPAIR_LOCAL_MODEL_URL` – endpoint for your local embeddings/feedback service (defaults to `http://local-model:9000`).
91
91
  - `COMPAIR_EMAIL_BACKEND` – the core mailer logs emails to stdout; cloud overrides this with transactional delivery.
92
+ - `COMPAIR_REQUIRE_AUTHENTICATION` (`true`) – set to `false` to run the API in single-user mode without login or account management. When disabled, Compair auto-provisions a local user, group, and long-lived session token so you can upload documents immediately.
93
+ - `COMPAIR_SINGLE_USER_USERNAME` / `COMPAIR_SINGLE_USER_NAME` – override the email-style username and display name that are used for the auto-provisioned local user in single-user mode.
94
+ - `COMPAIR_INCLUDE_LEGACY_ROUTES` (`false`) – opt-in to the full legacy API surface (used by the hosted product) when running the core edition. Leave unset to expose only the streamlined single-user endpoints in Swagger.
92
95
 
93
96
  See `compair_core/server/settings.py` for the full settings surface.
94
97
 
@@ -1,5 +1,5 @@
1
1
  compair_core/__init__.py,sha256=ktPgTk1QCd7PF-CUzfcd49JvkEut68SEefx2qcL5M5s,122
2
- compair_core/api.py,sha256=sNROj9Bp80KInxZwoekW0NSkIO-oTZmU7PJvJe7yY5I,129241
2
+ compair_core/api.py,sha256=XkTj7YrZMtTceNoe4G7hq9IMNETFXcjM1v-dF7HlynA,135954
3
3
  compair_core/compair/__init__.py,sha256=V2mqe6UQEvY4U8XL8T-TtCRNDWVUMNeCLZ8nsYQLvr4,2494
4
4
  compair_core/compair/celery_app.py,sha256=OM_Saza9yC9Q0kz_WXctfswrKkG7ruT52Zl5E4guiT0,640
5
5
  compair_core/compair/default_groups.py,sha256=dbacrFkSjqEQZ_uoFU5gYhgIoP_3lmvz6LJNHCJvxlw,498
@@ -17,9 +17,9 @@ compair_core/compair_email/email_core.py,sha256=da7JxTo5ude55mB7UNLlpNp8xenYwoPa
17
17
  compair_core/compair_email/templates.py,sha256=JVlLdJEcpu14mVKRAYRIPIw2JGy70kG70mfjXgby-To,206
18
18
  compair_core/compair_email/templates_core.py,sha256=1XzBXGjmM6gApSXq382fxCRiVa5J-iskC3r9QQvmV1U,967
19
19
  compair_core/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
- compair_core/server/app.py,sha256=8Kbq--xxFQ8zUD6dfm2aQ2rM58LQGwb4bUpzFUju-v4,3415
20
+ compair_core/server/app.py,sha256=4FY4Pur1fJUkQAwBApLhPQsnF6BRNZjUQ-X7bJCv2mI,3625
21
21
  compair_core/server/deps.py,sha256=0X-Z5JQGeXwbMooWIOC2kXVmsiJIvgUtqkK2PmDjKpI,1557
22
- compair_core/server/settings.py,sha256=XutlHezdP6YjxsavE4ILnReSOBvZgeg-Wukqxl3vdxE,1491
22
+ compair_core/server/settings.py,sha256=mWE5vgIx3jxm6LzyeH6QL1tCwxQ6bsZIQffVowqtkFQ,1681
23
23
  compair_core/server/local_model/__init__.py,sha256=YlzDgorgAjGou9d_W29Xp3TVu08e4t9x8csFxn8cgSE,50
24
24
  compair_core/server/local_model/app.py,sha256=2bOLjgAyKnFcng2lmDVMB0G6WgnnVGjHyj2X8TeJjnU,1626
25
25
  compair_core/server/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -30,9 +30,9 @@ compair_core/server/providers/noop_analytics.py,sha256=OKw23SObxBlQzFdB0xEBg5qD1
30
30
  compair_core/server/providers/noop_billing.py,sha256=V18Cpl1D1reM3xhgw-lShGliVpYO8IsiAPWOAIR34jM,1358
31
31
  compair_core/server/providers/noop_ocr.py,sha256=fMaJrivDef38-ECgIuTXUBCIm_avgvZf3nQ3UTdFPNI,341
32
32
  compair_core/server/routers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
- compair_core/server/routers/capabilities.py,sha256=FZBLgmlIw6tWZiuihpzRmZ9AvbrY1jolVs7zqnhNPRU,1150
34
- compair_core-0.3.10.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
35
- compair_core-0.3.10.dist-info/METADATA,sha256=Uxz10AhV2Qr4tZzomNLX4tbVJPLg0bgFCCoBq_D3U7c,4639
36
- compair_core-0.3.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
37
- compair_core-0.3.10.dist-info/top_level.txt,sha256=1dpwoLSY2DWQUVGS05Tq0MuFXg8sabYzg4V2deLzzuo,13
38
- compair_core-0.3.10.dist-info/RECORD,,
33
+ compair_core/server/routers/capabilities.py,sha256=2U9lEzyQRRYftprrvEeM5Lif_5rhiRGqZhIsvYZBaE4,1349
34
+ compair_core-0.3.12.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
35
+ compair_core-0.3.12.dist-info/METADATA,sha256=L__UeN0VYvCRMSPEZIDdBebUczdsDRP1CtRJ7XT_0bo,5315
36
+ compair_core-0.3.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
37
+ compair_core-0.3.12.dist-info/top_level.txt,sha256=1dpwoLSY2DWQUVGS05Tq0MuFXg8sabYzg4V2deLzzuo,13
38
+ compair_core-0.3.12.dist-info/RECORD,,