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 +148 -3
- compair_core/server/routers/capabilities.py +4 -1
- compair_core/server/settings.py +3 -0
- {compair_core-0.3.10.dist-info → compair_core-0.3.11.dist-info}/METADATA +3 -1
- {compair_core-0.3.10.dist-info → compair_core-0.3.11.dist-info}/RECORD +8 -8
- {compair_core-0.3.10.dist-info → compair_core-0.3.11.dist-info}/WHEEL +0 -0
- {compair_core-0.3.10.dist-info → compair_core-0.3.11.dist-info}/licenses/LICENSE +0 -0
- {compair_core-0.3.10.dist-info → compair_core-0.3.11.dist-info}/top_level.txt +0 -0
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":
|
|
18
|
+
"password_login": require_auth,
|
|
19
|
+
"required": require_auth,
|
|
20
|
+
"single_user": not require_auth,
|
|
18
21
|
},
|
|
19
22
|
"inputs": {
|
|
20
23
|
"text": True,
|
compair_core/server/settings.py
CHANGED
|
@@ -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.
|
|
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=
|
|
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=
|
|
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=
|
|
34
|
-
compair_core-0.3.
|
|
35
|
-
compair_core-0.3.
|
|
36
|
-
compair_core-0.3.
|
|
37
|
-
compair_core-0.3.
|
|
38
|
-
compair_core-0.3.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|