compair-core 0.3.9__tar.gz → 0.3.11__tar.gz

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.

Files changed (44) hide show
  1. {compair_core-0.3.9 → compair_core-0.3.11}/PKG-INFO +4 -1
  2. {compair_core-0.3.9 → compair_core-0.3.11}/README.md +2 -0
  3. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/api.py +162 -10
  4. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair/__init__.py +27 -35
  5. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair/utils.py +1 -1
  6. compair_core-0.3.11/compair_core/compair_email/templates_core.py +32 -0
  7. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/routers/capabilities.py +4 -1
  8. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/settings.py +4 -1
  9. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core.egg-info/PKG-INFO +4 -1
  10. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core.egg-info/requires.txt +1 -0
  11. {compair_core-0.3.9 → compair_core-0.3.11}/pyproject.toml +2 -1
  12. compair_core-0.3.9/compair_core/compair_email/templates_core.py +0 -13
  13. {compair_core-0.3.9 → compair_core-0.3.11}/LICENSE +0 -0
  14. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/__init__.py +0 -0
  15. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair/celery_app.py +0 -0
  16. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair/default_groups.py +0 -0
  17. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair/embeddings.py +0 -0
  18. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair/feedback.py +0 -0
  19. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair/logger.py +0 -0
  20. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair/main.py +0 -0
  21. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair/models.py +0 -0
  22. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair/schema.py +0 -0
  23. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair/tasks.py +0 -0
  24. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair_email/__init__.py +0 -0
  25. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair_email/email.py +0 -0
  26. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair_email/email_core.py +0 -0
  27. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair_email/templates.py +0 -0
  28. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/__init__.py +0 -0
  29. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/app.py +0 -0
  30. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/deps.py +0 -0
  31. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/local_model/__init__.py +0 -0
  32. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/local_model/app.py +0 -0
  33. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/providers/__init__.py +0 -0
  34. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/providers/console_mailer.py +0 -0
  35. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/providers/contracts.py +0 -0
  36. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/providers/local_storage.py +0 -0
  37. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/providers/noop_analytics.py +0 -0
  38. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/providers/noop_billing.py +0 -0
  39. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/providers/noop_ocr.py +0 -0
  40. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/routers/__init__.py +0 -0
  41. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core.egg-info/SOURCES.txt +0 -0
  42. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core.egg-info/dependency_links.txt +0 -0
  43. {compair_core-0.3.9 → compair_core-0.3.11}/compair_core.egg-info/top_level.txt +0 -0
  44. {compair_core-0.3.9 → compair_core-0.3.11}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: compair-core
3
- Version: 0.3.9
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
@@ -23,6 +23,7 @@ Requires-Dist: redis>=5.0
23
23
  Requires-Dist: psutil>=5.9
24
24
  Requires-Dist: python-Levenshtein>=0.23
25
25
  Requires-Dist: redmail>=0.6
26
+ Requires-Dist: python-multipart>=0.0.20
26
27
  Provides-Extra: dev
27
28
  Requires-Dist: build>=1.0; extra == "dev"
28
29
  Requires-Dist: twine>=5.0; extra == "dev"
@@ -88,6 +89,8 @@ Key environment variables for the core edition:
88
89
  - `COMPAIR_SQLITE_DIR` / `COMPAIR_SQLITE_NAME` – override the default local SQLite path (falls back to `./compair_data` if `/data` is not writable).
89
90
  - `COMPAIR_LOCAL_MODEL_URL` – endpoint for your local embeddings/feedback service (defaults to `http://local-model:9000`).
90
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.
91
94
 
92
95
  See `compair_core/server/settings.py` for the full settings surface.
93
96
 
@@ -54,6 +54,8 @@ Key environment variables for the core edition:
54
54
  - `COMPAIR_SQLITE_DIR` / `COMPAIR_SQLITE_NAME` – override the default local SQLite path (falls back to `./compair_data` if `/data` is not writable).
55
55
  - `COMPAIR_LOCAL_MODEL_URL` – endpoint for your local embeddings/feedback service (defaults to `http://local-model:9000`).
56
56
  - `COMPAIR_EMAIL_BACKEND` – the core mailer logs emails to stdout; cloud overrides this with transactional delivery.
57
+ - `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.
58
+ - `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.
57
59
 
58
60
  See `compair_core/server/settings.py` for the full settings surface.
59
61
 
@@ -36,10 +36,13 @@ from .compair_email.templates import (
36
36
  )
37
37
  from .compair.tasks import process_document_task as process_document_celery, send_feature_announcement_task, send_deactivate_request_email, send_help_request_email
38
38
 
39
- import redis
39
+ try:
40
+ import redis # type: ignore
41
+ except ImportError: # pragma: no cover - optional dependency
42
+ redis = None
40
43
 
41
44
  redis_url = os.environ.get("REDIS_URL")
42
- redis_client = redis.Redis.from_url(redis_url)
45
+ redis_client = redis.Redis.from_url(redis_url) if (redis and redis_url) else None
43
46
  #from compair.main import process_document
44
47
 
45
48
  router = APIRouter()
@@ -56,6 +59,112 @@ GA4_MEASUREMENT_ID = os.getenv("GA4_MEASUREMENT_ID")
56
59
  GA4_API_SECRET = os.getenv("GA4_API_SECRET")
57
60
 
58
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
59
168
 
60
169
 
61
170
  def require_cloud(feature: str) -> None:
@@ -80,13 +189,20 @@ HAS_ACTIVITY = hasattr(models, "Activity")
80
189
  HAS_REFERRALS = hasattr(models.User, "referral_code")
81
190
  HAS_BILLING = hasattr(models.User, "stripe_customer_id")
82
191
  HAS_TRIALS = hasattr(models.User, "trial_expiration_date")
192
+ HAS_REDIS = redis_client is not None
83
193
 
84
194
 
85
195
  def require_feature(flag: bool, feature: str) -> None:
86
196
  if not flag:
87
197
  raise HTTPException(status_code=501, detail=f"{feature} is only available in the Compair Cloud edition.")
88
198
 
89
- 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")
90
206
  with compair.Session() as session:
91
207
  user_session = session.query(models.Session).filter(models.Session.id == auth_token).first()
92
208
  if not user_session:
@@ -150,9 +266,20 @@ log_service_resource_metrics(service_name="backend") # or "frontend"
150
266
 
151
267
  @router.post("/login")
152
268
  def login(request: schema.LoginRequest) -> dict:
269
+ settings = get_settings_dependency()
153
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
+ }
154
282
  user = session.query(models.User).filter(models.User.username == request.username).first()
155
- print("PW yo: {request.password}")
156
283
  if not user or not user.check_password(request.password):
157
284
  raise HTTPException(status_code=401, detail="Invalid credentials")
158
285
  if user.status == 'inactive':
@@ -526,8 +653,15 @@ def create_user(
526
653
 
527
654
 
528
655
  @router.get("/load_session")
529
- 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)
530
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.")
531
665
  user_session = session.query(models.Session).filter(models.Session.id == auth_token).first()
532
666
  if not user_session:
533
667
  raise HTTPException(status_code=404, detail="Session not found")
@@ -592,6 +726,9 @@ def update_session_duration(
592
726
  def delete_user(
593
727
  current_user: models.User = Depends(get_current_user)
594
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.")
595
732
  with compair.Session() as session:
596
733
  current_user.delete()
597
734
  session.commit()
@@ -1707,6 +1844,9 @@ def load_references(
1707
1844
 
1708
1845
  @router.get("/verify-email")
1709
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.")
1710
1850
  with compair.Session() as session:
1711
1851
  print(token)
1712
1852
  user = session.query(models.User).filter(models.User.verification_token == token).first()
@@ -1765,6 +1905,9 @@ def sign_up(
1765
1905
  request: schema.SignUpRequest,
1766
1906
  analytics: Analytics = Depends(get_analytics),
1767
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.")
1768
1911
  print('1')
1769
1912
  if not is_valid_email(request.username):
1770
1913
  raise HTTPException(status_code=400, detail="Invalid email address")
@@ -1798,6 +1941,9 @@ def sign_up(
1798
1941
 
1799
1942
  @router.post("/forgot-password")
1800
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.")
1801
1947
  print('1')
1802
1948
  with compair.Session() as session:
1803
1949
  print('2')
@@ -1830,6 +1976,9 @@ def forgot_password(request: schema.ForgotPasswordRequest) -> dict:
1830
1976
 
1831
1977
  @router.post("/reset-password")
1832
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.")
1833
1982
  with compair.Session() as session:
1834
1983
  print('1')
1835
1984
  print(request.token)
@@ -3228,11 +3377,12 @@ def generate_download_token(
3228
3377
  else:
3229
3378
  raise HTTPException(status_code=403, detail="Not authorized to download this file.")
3230
3379
 
3380
+ if not HAS_REDIS:
3381
+ raise HTTPException(status_code=501, detail="Secure download links require Redis, which is unavailable in the core edition.")
3382
+
3231
3383
  token = secrets.token_urlsafe(32)
3232
3384
  key = f"download_token:{token}"
3233
3385
  redis_client.setex(key, 300, document_id)
3234
- print('Setting redis kv')
3235
- print(key, document_id)
3236
3386
  return {"download_url": f"/documents/download/{token}"}
3237
3387
 
3238
3388
 
@@ -3241,10 +3391,12 @@ def download_document_with_token(
3241
3391
  token: str,
3242
3392
  storage: StorageProvider = Depends(get_storage),
3243
3393
  ):
3394
+ if not HAS_REDIS:
3395
+ raise HTTPException(status_code=501, detail="Secure download links require Redis, which is unavailable in the core edition.")
3396
+
3244
3397
  key = f"download_token:{token}"
3245
- print(f'Retrieving redis kv with key {key}')
3246
- document_id = redis_client.get(key).decode('utf-8') if redis_client.get(key) else None
3247
- print(f'Value {document_id}')
3398
+ value = redis_client.get(key) if redis_client else None
3399
+ document_id = value.decode('utf-8') if value else None
3248
3400
  if not document_id:
3249
3401
  raise HTTPException(status_code=403, detail="Invalid or expired token")
3250
3402
  redis_client.delete(key)
@@ -1,25 +1,20 @@
1
1
  from __future__ import annotations
2
2
 
3
- import importlib
4
- import logging
5
3
  import os
6
- import sys
7
-
8
4
  from sqlalchemy import Engine, create_engine
9
5
  from sqlalchemy.orm import sessionmaker
10
6
 
11
- from ..compair import embeddings, feedback, logger, main, models, tasks, utils
12
- from ..compair.default_groups import initialize_default_groups
7
+ from . import embeddings, feedback, logger, main, models, tasks, utils
8
+ from .default_groups import initialize_default_groups
13
9
 
14
10
  edition = os.getenv("COMPAIR_EDITION", "core").lower()
15
11
 
16
- _cloud_available = False
12
+ initialize_database_override = None
13
+
17
14
  if edition == "cloud":
18
15
  try: # Import cloud overrides if the private package is installed
19
- from ..compair_cloud import (
16
+ from compair_cloud import ( # type: ignore
20
17
  bootstrap as cloud_bootstrap,
21
- celery_app as cloud_celery_app,
22
- default_groups as cloud_default_groups,
23
18
  embeddings as cloud_embeddings,
24
19
  feedback as cloud_feedback,
25
20
  logger as cloud_logger,
@@ -27,27 +22,18 @@ if edition == "cloud":
27
22
  models as cloud_models,
28
23
  tasks as cloud_tasks,
29
24
  utils as cloud_utils,
30
- ) # type: ignore
25
+ )
31
26
 
32
- _cloud_available = True
27
+ embeddings = cloud_embeddings
28
+ feedback = cloud_feedback
29
+ logger = cloud_logger
30
+ main = cloud_main
31
+ models = cloud_models
32
+ tasks = cloud_tasks
33
+ utils = cloud_utils
34
+ initialize_database_override = getattr(cloud_bootstrap, "initialize_database", None)
33
35
  except ImportError:
34
- _cloud_available = False
35
-
36
- if _cloud_available:
37
- from ..compair_cloud.default_groups import initialize_default_groups # type: ignore
38
- embeddings = cloud_embeddings
39
- feedback = cloud_feedback
40
- logger = cloud_logger
41
- main = cloud_main
42
- models = cloud_models
43
- tasks = cloud_tasks
44
- utils = cloud_utils
45
- initialize_database_override = getattr(cloud_bootstrap, "initialize_database", None)
46
- else:
47
- from . import embeddings, feedback, logger, main, models, tasks, utils
48
- from .default_groups import initialize_default_groups
49
- initialize_database_override = None
50
-
36
+ pass
51
37
 
52
38
 
53
39
  def _handle_engine() -> Engine:
@@ -74,15 +60,21 @@ def _handle_engine() -> Engine:
74
60
  return create_engine(f"sqlite:///{sqlite_path}", connect_args={"check_same_thread": False})
75
61
 
76
62
 
77
- engine = _handle_engine()
78
-
79
-
80
63
  def initialize_database() -> None:
81
64
  models.Base.metadata.create_all(engine)
82
65
  if initialize_database_override:
83
66
  initialize_database_override(engine)
84
67
 
85
68
 
86
- __all__ = ["embeddings", "feedback", "main", "models", "utils"]
87
- sys.modules.setdefault(__name__ + ".server", importlib.import_module("server"))
88
- sys.modules.setdefault(__name__ + ".compair_email", importlib.import_module("compair_email"))
69
+ def _initialize_defaults() -> None:
70
+ with Session() as session:
71
+ initialize_default_groups(session)
72
+
73
+
74
+ engine = _handle_engine()
75
+ Session = sessionmaker(engine)
76
+ embedder = embeddings.Embedder()
77
+ reviewer = feedback.Reviewer()
78
+ _initialize_defaults()
79
+
80
+ __all__ = ["embeddings", "feedback", "main", "models", "utils", "Session"]
@@ -5,7 +5,7 @@ from datetime import datetime, timedelta, timezone
5
5
 
6
6
  from sqlalchemy.orm import Session
7
7
 
8
- from compair.models import Activity
8
+ from .models import Activity
9
9
 
10
10
 
11
11
  def chunk_text(text: str) -> list[str]:
@@ -0,0 +1,32 @@
1
+ """Minimal email templates for the core edition."""
2
+
3
+ ACCOUNT_VERIFY_TEMPLATE = """
4
+ <p>Hi {{user_name}},</p>
5
+ <p>Please verify your Compair account by clicking the link below:</p>
6
+ <p><a href="{{verify_link}}">Verify my account</a></p>
7
+ <p>Thanks!</p>
8
+ """.strip()
9
+
10
+ PASSWORD_RESET_TEMPLATE = """
11
+ <p>We received a request to reset your password.</p>
12
+ <p>Your password reset code is: <strong>{{reset_code}}</strong></p>
13
+ """.strip()
14
+
15
+ GROUP_INVITATION_TEMPLATE = """
16
+ <p>{{inviter_name}} invited you to join the group {{group_name}}.</p>
17
+ <p><a href="{{invitation_link}}">Accept invitation</a></p>
18
+ """.strip()
19
+
20
+ GROUP_JOIN_TEMPLATE = """
21
+ <p>{{user_name}} has joined your group.</p>
22
+ """.strip()
23
+
24
+ INDIVIDUAL_INVITATION_TEMPLATE = """
25
+ <p>{{inviter_name}} invited you to Compair.</p>
26
+ <p><a href="{{referral_link}}">Join now</a></p>
27
+ """.strip()
28
+
29
+ REFERRAL_CREDIT_TEMPLATE = """
30
+ <p>Hi {{user_name}},</p>
31
+ <p>Great news! You now have {{referral_credits}} referral credits.</p>
32
+ """.strip()
@@ -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,9 +16,12 @@ 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
- local_upload_dir: str = "/data/uploads"
24
+ local_upload_dir: str = "~/.compair-core/data/uploads"
22
25
  local_upload_base_url: str = "/uploads"
23
26
 
24
27
  # Cloud storage (R2/S3-compatible)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: compair-core
3
- Version: 0.3.9
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
@@ -23,6 +23,7 @@ Requires-Dist: redis>=5.0
23
23
  Requires-Dist: psutil>=5.9
24
24
  Requires-Dist: python-Levenshtein>=0.23
25
25
  Requires-Dist: redmail>=0.6
26
+ Requires-Dist: python-multipart>=0.0.20
26
27
  Provides-Extra: dev
27
28
  Requires-Dist: build>=1.0; extra == "dev"
28
29
  Requires-Dist: twine>=5.0; extra == "dev"
@@ -88,6 +89,8 @@ Key environment variables for the core edition:
88
89
  - `COMPAIR_SQLITE_DIR` / `COMPAIR_SQLITE_NAME` – override the default local SQLite path (falls back to `./compair_data` if `/data` is not writable).
89
90
  - `COMPAIR_LOCAL_MODEL_URL` – endpoint for your local embeddings/feedback service (defaults to `http://local-model:9000`).
90
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.
91
94
 
92
95
  See `compair_core/server/settings.py` for the full settings surface.
93
96
 
@@ -11,6 +11,7 @@ redis>=5.0
11
11
  psutil>=5.9
12
12
  python-Levenshtein>=0.23
13
13
  redmail>=0.6
14
+ python-multipart>=0.0.20
14
15
 
15
16
  [dev]
16
17
  build>=1.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "compair-core"
7
- version = "0.3.9"
7
+ version = "0.3.11"
8
8
  description = "Open-source foundation of the Compair collaboration platform."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -26,6 +26,7 @@ dependencies = [
26
26
  "psutil>=5.9",
27
27
  "python-Levenshtein>=0.23",
28
28
  "redmail>=0.6",
29
+ "python-multipart>=0.0.20",
29
30
  ]
30
31
 
31
32
  [project.optional-dependencies]
@@ -1,13 +0,0 @@
1
- """Minimal email templates for the core edition."""
2
-
3
- ACCOUNT_VERIFY_TEMPLATE = """
4
- <p>Hi {{user_name}},</p>
5
- <p>Please verify your Compair account by clicking the link below:</p>
6
- <p><a href="{{verify_link}}">Verify my account</a></p>
7
- <p>Thanks!</p>
8
- """.strip()
9
-
10
- PASSWORD_RESET_TEMPLATE = """
11
- <p>We received a request to reset your password.</p>
12
- <p>Your password reset code is: <strong>{{reset_code}}</strong></p>
13
- """.strip()
File without changes
File without changes