compair-core 0.3.10__py3-none-any.whl → 0.3.11__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.
compair_core/api.py CHANGED
@@ -59,6 +59,112 @@ GA4_MEASUREMENT_ID = os.getenv("GA4_MEASUREMENT_ID")
59
59
  GA4_API_SECRET = os.getenv("GA4_API_SECRET")
60
60
 
61
61
  IS_CLOUD = os.getenv("COMPAIR_EDITION", "core").lower() == "cloud"
62
+ SINGLE_USER_SESSION_TTL = timedelta(days=365)
63
+
64
+
65
+ def _ensure_single_user(session: Session, settings: Settings) -> models.User:
66
+ """Create or fetch the singleton user used when authentication is disabled."""
67
+ changed = False
68
+ user = (
69
+ session.query(models.User)
70
+ .options(joinedload(models.User.groups))
71
+ .filter(models.User.username == settings.single_user_username)
72
+ .first()
73
+ )
74
+ if user is None:
75
+ now = datetime.now(timezone.utc)
76
+ user = models.User(
77
+ username=settings.single_user_username,
78
+ name=settings.single_user_name,
79
+ datetime_registered=now,
80
+ verification_token=None,
81
+ token_expiration=None,
82
+ )
83
+ user.set_password(secrets.token_urlsafe(16))
84
+ user.status = "active"
85
+ user.status_change_date = now
86
+ session.add(user)
87
+ session.flush()
88
+ admin = models.Administrator(user_id=user.user_id)
89
+ group = models.Group(
90
+ name=user.username,
91
+ datetime_created=now,
92
+ group_image=None,
93
+ category="Private",
94
+ description=f"Private workspace for {settings.single_user_name}",
95
+ visibility="private",
96
+ )
97
+ group.admins.append(admin)
98
+ user.groups = [group]
99
+ session.add_all([group, admin])
100
+ changed = True
101
+ else:
102
+ now = datetime.now(timezone.utc)
103
+ if user.status != "active":
104
+ user.status = "active"
105
+ user.status_change_date = now
106
+ changed = True
107
+ group = next((g for g in user.groups if g.name == user.username), None)
108
+ if group is None:
109
+ group = session.query(models.Group).filter(models.Group.name == user.username).first()
110
+ if group is None:
111
+ group = models.Group(
112
+ name=user.username,
113
+ datetime_created=now,
114
+ group_image=None,
115
+ category="Private",
116
+ description=f"Private workspace for {user.name}",
117
+ visibility="private",
118
+ )
119
+ session.add(group)
120
+ changed = True
121
+ if group not in user.groups:
122
+ user.groups.append(group)
123
+ changed = True
124
+ admin = session.query(models.Administrator).filter(models.Administrator.user_id == user.user_id).first()
125
+ if admin is None:
126
+ admin = models.Administrator(user_id=user.user_id)
127
+ session.add(admin)
128
+ changed = True
129
+ if admin not in group.admins:
130
+ group.admins.append(admin)
131
+ changed = True
132
+
133
+ if changed:
134
+ session.commit()
135
+ user = (
136
+ session.query(models.User)
137
+ .options(joinedload(models.User.groups))
138
+ .filter(models.User.username == settings.single_user_username)
139
+ .first()
140
+ )
141
+ if user is None:
142
+ raise RuntimeError("Failed to initialize the local Compair user.")
143
+ user.groups # ensure relationship is loaded before detaching
144
+ return user
145
+
146
+
147
+ def _ensure_single_user_session(session: Session, user: models.User) -> models.Session:
148
+ """Return a long-lived session token for the singleton user."""
149
+ now = datetime.now(timezone.utc)
150
+ existing = (
151
+ session.query(models.Session)
152
+ .filter(models.Session.user_id == user.user_id, models.Session.datetime_valid_until >= now)
153
+ .order_by(models.Session.datetime_valid_until.desc())
154
+ .first()
155
+ )
156
+ if existing:
157
+ return existing
158
+ token = secrets.token_urlsafe()
159
+ user_session = models.Session(
160
+ id=token,
161
+ user_id=user.user_id,
162
+ datetime_created=now,
163
+ datetime_valid_until=now + SINGLE_USER_SESSION_TTL,
164
+ )
165
+ session.add(user_session)
166
+ session.commit()
167
+ return user_session
62
168
 
63
169
 
64
170
  def require_cloud(feature: str) -> None:
@@ -90,7 +196,13 @@ def require_feature(flag: bool, feature: str) -> None:
90
196
  if not flag:
91
197
  raise HTTPException(status_code=501, detail=f"{feature} is only available in the Compair Cloud edition.")
92
198
 
93
- def get_current_user(auth_token: str = Header(...)):
199
+ def get_current_user(auth_token: str | None = Header(None)):
200
+ settings = get_settings_dependency()
201
+ if not settings.require_authentication:
202
+ with compair.Session() as session:
203
+ return _ensure_single_user(session, settings)
204
+ if not auth_token:
205
+ raise HTTPException(status_code=401, detail="Missing session token")
94
206
  with compair.Session() as session:
95
207
  user_session = session.query(models.Session).filter(models.Session.id == auth_token).first()
96
208
  if not user_session:
@@ -154,9 +266,20 @@ log_service_resource_metrics(service_name="backend") # or "frontend"
154
266
 
155
267
  @router.post("/login")
156
268
  def login(request: schema.LoginRequest) -> dict:
269
+ settings = get_settings_dependency()
157
270
  with compair.Session() as session:
271
+ if not settings.require_authentication:
272
+ user = _ensure_single_user(session, settings)
273
+ user_session = _ensure_single_user_session(session, user)
274
+ return {
275
+ "user_id": user.user_id,
276
+ "username": user.username,
277
+ "name": user.name,
278
+ "status": user.status,
279
+ "role": user.role,
280
+ "auth_token": user_session.id,
281
+ }
158
282
  user = session.query(models.User).filter(models.User.username == request.username).first()
159
- print("PW yo: {request.password}")
160
283
  if not user or not user.check_password(request.password):
161
284
  raise HTTPException(status_code=401, detail="Invalid credentials")
162
285
  if user.status == 'inactive':
@@ -530,8 +653,15 @@ def create_user(
530
653
 
531
654
 
532
655
  @router.get("/load_session")
533
- def load_session(auth_token: str) -> schema.Session | None:
656
+ def load_session(auth_token: str | None = None) -> schema.Session | None:
657
+ settings = get_settings_dependency()
658
+ if not settings.require_authentication:
659
+ with compair.Session() as session:
660
+ user = _ensure_single_user(session, settings)
661
+ return _ensure_single_user_session(session, user)
534
662
  with compair.Session() as session:
663
+ if not auth_token:
664
+ raise HTTPException(status_code=400, detail="auth_token is required when authentication is enabled.")
535
665
  user_session = session.query(models.Session).filter(models.Session.id == auth_token).first()
536
666
  if not user_session:
537
667
  raise HTTPException(status_code=404, detail="Session not found")
@@ -596,6 +726,9 @@ def update_session_duration(
596
726
  def delete_user(
597
727
  current_user: models.User = Depends(get_current_user)
598
728
  ):
729
+ settings = get_settings_dependency()
730
+ if not settings.require_authentication:
731
+ raise HTTPException(status_code=403, detail="Deleting the local user is not supported when authentication is disabled.")
599
732
  with compair.Session() as session:
600
733
  current_user.delete()
601
734
  session.commit()
@@ -1711,6 +1844,9 @@ def load_references(
1711
1844
 
1712
1845
  @router.get("/verify-email")
1713
1846
  def verify_email(token: str):
1847
+ settings = get_settings_dependency()
1848
+ if not settings.require_authentication:
1849
+ raise HTTPException(status_code=403, detail="Email verification is disabled when authentication is disabled.")
1714
1850
  with compair.Session() as session:
1715
1851
  print(token)
1716
1852
  user = session.query(models.User).filter(models.User.verification_token == token).first()
@@ -1769,6 +1905,9 @@ def sign_up(
1769
1905
  request: schema.SignUpRequest,
1770
1906
  analytics: Analytics = Depends(get_analytics),
1771
1907
  ) -> dict:
1908
+ settings = get_settings_dependency()
1909
+ if not settings.require_authentication:
1910
+ raise HTTPException(status_code=403, detail="Sign-up is disabled when authentication is disabled.")
1772
1911
  print('1')
1773
1912
  if not is_valid_email(request.username):
1774
1913
  raise HTTPException(status_code=400, detail="Invalid email address")
@@ -1802,6 +1941,9 @@ def sign_up(
1802
1941
 
1803
1942
  @router.post("/forgot-password")
1804
1943
  def forgot_password(request: schema.ForgotPasswordRequest) -> dict:
1944
+ settings = get_settings_dependency()
1945
+ if not settings.require_authentication:
1946
+ raise HTTPException(status_code=403, detail="Password resets are disabled when authentication is disabled.")
1805
1947
  print('1')
1806
1948
  with compair.Session() as session:
1807
1949
  print('2')
@@ -1834,6 +1976,9 @@ def forgot_password(request: schema.ForgotPasswordRequest) -> dict:
1834
1976
 
1835
1977
  @router.post("/reset-password")
1836
1978
  def reset_password(request: schema.ResetPasswordRequest) -> dict:
1979
+ settings = get_settings_dependency()
1980
+ if not settings.require_authentication:
1981
+ raise HTTPException(status_code=403, detail="Password resets are disabled when authentication is disabled.")
1837
1982
  with compair.Session() as session:
1838
1983
  print('1')
1839
1984
  print(request.token)
@@ -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,
@@ -16,6 +16,9 @@ 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"
19
22
 
20
23
  # Core/local storage defaults
21
24
  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.11
4
4
  Summary: Open-source foundation of the Compair collaboration platform.
5
5
  Author: RocketResearch, Inc.
6
6
  License: MIT
@@ -89,6 +89,8 @@ 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.
92
94
 
93
95
  See `compair_core/server/settings.py` for the full settings surface.
94
96
 
@@ -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=acxam2asb8f9Jr6nML1avjlaLVkWAmHVvyI_Jzvzu40,135265
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
@@ -19,7 +19,7 @@ compair_core/compair_email/templates_core.py,sha256=1XzBXGjmM6gApSXq382fxCRiVa5J
19
19
  compair_core/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  compair_core/server/app.py,sha256=8Kbq--xxFQ8zUD6dfm2aQ2rM58LQGwb4bUpzFUju-v4,3415
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=9xq6vm51kxpgoDTWG5IFjRY5u7fVZtwKA_ch-0mlWR4,1641
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=pMnNjnDeHKCDVSVrF_1xhTXUclvyH3kYFksnGQRXKqY,1292
34
+ compair_core-0.3.11.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
35
+ compair_core-0.3.11.dist-info/METADATA,sha256=LKa90nl50pe8RG8GbC3SgtG4I5K2RlxNeaWZfV1oPr4,5092
36
+ compair_core-0.3.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
37
+ compair_core-0.3.11.dist-info/top_level.txt,sha256=1dpwoLSY2DWQUVGS05Tq0MuFXg8sabYzg4V2deLzzuo,13
38
+ compair_core-0.3.11.dist-info/RECORD,,