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.
- {compair_core-0.3.9 → compair_core-0.3.11}/PKG-INFO +4 -1
- {compair_core-0.3.9 → compair_core-0.3.11}/README.md +2 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/api.py +162 -10
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair/__init__.py +27 -35
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair/utils.py +1 -1
- compair_core-0.3.11/compair_core/compair_email/templates_core.py +32 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/routers/capabilities.py +4 -1
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/settings.py +4 -1
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core.egg-info/PKG-INFO +4 -1
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core.egg-info/requires.txt +1 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/pyproject.toml +2 -1
- compair_core-0.3.9/compair_core/compair_email/templates_core.py +0 -13
- {compair_core-0.3.9 → compair_core-0.3.11}/LICENSE +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/__init__.py +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair/celery_app.py +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair/default_groups.py +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair/embeddings.py +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair/feedback.py +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair/logger.py +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair/main.py +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair/models.py +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair/schema.py +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair/tasks.py +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair_email/__init__.py +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair_email/email.py +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair_email/email_core.py +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/compair_email/templates.py +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/__init__.py +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/app.py +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/deps.py +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/local_model/__init__.py +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/local_model/app.py +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/providers/__init__.py +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/providers/console_mailer.py +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/providers/contracts.py +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/providers/local_storage.py +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/providers/noop_analytics.py +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/providers/noop_billing.py +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/providers/noop_ocr.py +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core/server/routers/__init__.py +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core.egg-info/SOURCES.txt +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core.egg-info/dependency_links.txt +0 -0
- {compair_core-0.3.9 → compair_core-0.3.11}/compair_core.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|
-
|
|
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
|
-
|
|
3246
|
-
document_id =
|
|
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
|
|
12
|
-
from
|
|
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
|
-
|
|
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
|
|
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
|
-
)
|
|
25
|
+
)
|
|
31
26
|
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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"]
|
|
@@ -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":
|
|
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.
|
|
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
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "compair-core"
|
|
7
|
-
version = "0.3.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|