compair-core 0.3.1__py3-none-any.whl → 0.3.3__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/models.py ADDED
@@ -0,0 +1,355 @@
1
+ from __future__ import annotations
2
+
3
+ import binascii
4
+ import hashlib
5
+ import os
6
+ import secrets
7
+ from datetime import datetime, timezone
8
+ from uuid import uuid4
9
+
10
+ from pgvector.sqlalchemy import Vector
11
+ from sqlalchemy import (
12
+ Boolean,
13
+ Column,
14
+ DateTime,
15
+ ForeignKey,
16
+ Identity,
17
+ Integer,
18
+ String,
19
+ Table,
20
+ Text,
21
+ )
22
+ from sqlalchemy.orm import (
23
+ DeclarativeBase,
24
+ Mapped,
25
+ MappedAsDataclass,
26
+ mapped_column,
27
+ relationship,
28
+ )
29
+
30
+
31
+ class Base(DeclarativeBase, MappedAsDataclass):
32
+ pass
33
+
34
+
35
+ class BaseObject(Base):
36
+ __abstract__ = True
37
+
38
+
39
+ class User(Base):
40
+ __tablename__ = "user"
41
+ __table_args__ = {"schema": "public"}
42
+
43
+ user_id: Mapped[str] = mapped_column(String(36), primary_key=True, init=False, default=lambda: str(uuid4()))
44
+ username: Mapped[str] = mapped_column(String(128))
45
+ name: Mapped[str] = mapped_column(String(256))
46
+ role: Mapped[str | None] = mapped_column(String(128), nullable=True)
47
+ profile_image: Mapped[str | None] = mapped_column(String, nullable=True)
48
+ verification_token: Mapped[str | None] = mapped_column(String, nullable=True)
49
+ reset_token: Mapped[str | None] = mapped_column(String, nullable=True)
50
+ token_expiration: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
51
+ datetime_registered: Mapped[datetime]
52
+ status_change_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
53
+ password_hash: Mapped[str]
54
+ password_salt: Mapped[str]
55
+
56
+ status: Mapped[str] = mapped_column(String(16), default="inactive")
57
+ include_own_documents_in_feedback: Mapped[bool] = mapped_column(Boolean, default=False)
58
+ default_publish: Mapped[bool] = mapped_column(Boolean, default=True)
59
+ preferred_feedback_length: Mapped[str] = mapped_column(String(16), default="Brief")
60
+ hide_affiliations: Mapped[bool] = mapped_column(Boolean, default=False)
61
+
62
+ groups = relationship("Group", secondary="user_to_group", back_populates="users")
63
+ documents = relationship(
64
+ "Document",
65
+ back_populates="user",
66
+ cascade="all, delete",
67
+ passive_deletes=True,
68
+ )
69
+ notes = relationship(
70
+ "Note",
71
+ back_populates="author",
72
+ cascade="all, delete",
73
+ passive_deletes=True,
74
+ )
75
+
76
+ def __init__(
77
+ self,
78
+ username: str,
79
+ name: str,
80
+ datetime_registered: datetime,
81
+ verification_token: str | None,
82
+ token_expiration: datetime | None,
83
+ ):
84
+ super().__init__()
85
+ self.username = username
86
+ self.name = name
87
+ self.datetime_registered = datetime_registered
88
+ self.verification_token = verification_token
89
+ self.token_expiration = token_expiration
90
+ self.status = "inactive"
91
+ self.status_change_date = datetime.now(timezone.utc)
92
+
93
+ def set_password(self, password: str) -> str:
94
+ salt = os.urandom(64)
95
+ self.password_salt = binascii.hexlify(salt).decode("utf-8")
96
+ hash_bytes = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 100000)
97
+ self.password_hash = binascii.hexlify(hash_bytes).decode("utf-8")
98
+ return self.password_hash
99
+
100
+ def check_password(self, password: str) -> bool:
101
+ if not self.password_salt or not self.password_hash:
102
+ return False
103
+ salt = binascii.unhexlify(self.password_salt.encode("utf-8"))
104
+ hash_bytes = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 100000)
105
+ hash_hex = binascii.hexlify(hash_bytes).decode("utf-8")
106
+ return secrets.compare_digest(self.password_hash, hash_hex)
107
+
108
+
109
+ class Session(Base):
110
+ __tablename__ = "session"
111
+ __table_args__ = {"schema": "public"}
112
+
113
+ id: Mapped[str] = mapped_column(String(128), primary_key=True, init=True)
114
+ user_id: Mapped[str] = mapped_column(ForeignKey("public.user.user_id", ondelete="CASCADE"), index=True)
115
+ datetime_created: Mapped[datetime]
116
+ datetime_valid_until: Mapped[datetime]
117
+
118
+
119
+ class Group(BaseObject):
120
+ __tablename__ = "group"
121
+ __table_args__ = {"schema": "public"}
122
+
123
+ group_id: Mapped[str] = mapped_column(String(36), primary_key=True, init=False, default=lambda: str(uuid4()))
124
+ name: Mapped[str] = mapped_column(String(256))
125
+ datetime_created: Mapped[datetime]
126
+ group_image: Mapped[str | None] = mapped_column(String, nullable=True)
127
+ category: Mapped[str] = mapped_column(String(256), default="Other")
128
+ description: Mapped[str] = mapped_column(Text, default="")
129
+ visibility: Mapped[str] = mapped_column(String(32), default="public")
130
+
131
+ users = relationship("User", secondary="user_to_group", back_populates="groups")
132
+ admins = relationship("Administrator", secondary="admin_to_group", back_populates="groups")
133
+ documents = relationship("Document", secondary="document_to_group", back_populates="groups")
134
+ notes = relationship("Note", secondary="note_to_group", back_populates="groups")
135
+
136
+ __mapper_args__ = {"primary_key": [group_id]}
137
+
138
+ @property
139
+ def document_count(self) -> int:
140
+ return len(self.documents)
141
+
142
+ @property
143
+ def user_count(self) -> int:
144
+ return len(self.users)
145
+
146
+ @property
147
+ def first_three_user_profile_images(self) -> list[str | None]:
148
+ return [user.profile_image for user in self.users[:3]]
149
+
150
+
151
+ class Administrator(Base):
152
+ __tablename__ = "administrator"
153
+ __table_args__ = {"schema": "public"}
154
+
155
+ admin_id: Mapped[str] = mapped_column(String(36), primary_key=True, init=False, default=lambda: str(uuid4()))
156
+ user_id: Mapped[str] = mapped_column(ForeignKey("public.user.user_id", ondelete="CASCADE"), index=True)
157
+
158
+ user = relationship("User")
159
+ groups = relationship("Group", secondary="admin_to_group", back_populates="admins")
160
+
161
+
162
+ class JoinRequest(Base):
163
+ __tablename__ = "join_request"
164
+ __table_args__ = {"schema": "public"}
165
+
166
+ request_id: Mapped[int] = mapped_column(Identity(), primary_key=True, autoincrement=True, init=False)
167
+ user_id: Mapped[str] = mapped_column(ForeignKey("public.user.user_id", ondelete="CASCADE"))
168
+ group_id: Mapped[str] = mapped_column(ForeignKey("public.group.group_id", ondelete="CASCADE"))
169
+ datetime_requested: Mapped[datetime] = mapped_column(default=datetime.now(timezone.utc), init=False)
170
+
171
+ user = relationship("User")
172
+ group = relationship("Group")
173
+
174
+
175
+ class GroupInvitation(Base):
176
+ __tablename__ = "group_invitation"
177
+ __table_args__ = {"schema": "public"}
178
+
179
+ invitation_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True, autoincrement=True, init=False)
180
+ group_id: Mapped[str] = mapped_column(ForeignKey("public.group.group_id", ondelete="CASCADE"))
181
+ inviter_id: Mapped[str] = mapped_column(ForeignKey("public.user.user_id", ondelete="CASCADE"))
182
+ token: Mapped[str] = mapped_column(String(64), unique=True, nullable=False)
183
+ email: Mapped[str | None] = mapped_column(String(256), nullable=True)
184
+ datetime_expiration: Mapped[datetime]
185
+ datetime_created: Mapped[datetime] = mapped_column(default=datetime.now(timezone.utc), init=False)
186
+ status: Mapped[str] = mapped_column(String(32), default="pending")
187
+
188
+ group = relationship("Group")
189
+ inviter = relationship("User")
190
+
191
+
192
+ class Document(BaseObject):
193
+ __tablename__ = "document"
194
+ __table_args__ = {"schema": "public"}
195
+
196
+ document_id: Mapped[str] = mapped_column(String(36), primary_key=True, init=False, default=lambda: str(uuid4()))
197
+ user_id: Mapped[str] = mapped_column(ForeignKey("public.user.user_id", ondelete="CASCADE"), index=True)
198
+ author_id: Mapped[str]
199
+ title: Mapped[str]
200
+ content: Mapped[str] = mapped_column(Text)
201
+ doc_type: Mapped[str]
202
+ datetime_created: Mapped[datetime]
203
+ datetime_modified: Mapped[datetime]
204
+ file_key: Mapped[str | None] = mapped_column(String, nullable=True, default=None)
205
+ image_key: Mapped[str | None] = mapped_column(String, nullable=True, default=None)
206
+ is_published: Mapped[bool] = mapped_column(Boolean, default=False)
207
+ embedding = mapped_column(Vector(1536))
208
+
209
+ user = relationship("User", back_populates="documents")
210
+ groups = relationship("Group", secondary="document_to_group", back_populates="documents")
211
+ chunks = relationship(
212
+ "Chunk",
213
+ back_populates="document",
214
+ cascade="all, delete",
215
+ passive_deletes=True,
216
+ )
217
+ references = relationship(
218
+ "Reference",
219
+ back_populates="document",
220
+ cascade="all, delete",
221
+ passive_deletes=True,
222
+ )
223
+ notes = relationship(
224
+ "Note",
225
+ back_populates="document",
226
+ cascade="all, delete",
227
+ passive_deletes=True,
228
+ )
229
+
230
+
231
+ class Note(Base):
232
+ __tablename__ = "note"
233
+ __table_args__ = {"schema": "public"}
234
+
235
+ note_id: Mapped[str] = mapped_column(String(36), primary_key=True, init=False, default=lambda: str(uuid4()))
236
+ document_id: Mapped[str] = mapped_column(ForeignKey("public.document.document_id", ondelete="CASCADE"), index=True)
237
+ author_id: Mapped[str] = mapped_column(ForeignKey("public.user.user_id", ondelete="CASCADE"), index=True)
238
+ group_id: Mapped[str | None] = mapped_column(ForeignKey("public.group.group_id", ondelete="CASCADE"), index=True, nullable=True)
239
+ content: Mapped[str] = mapped_column(Text)
240
+ datetime_created: Mapped[datetime] = mapped_column(default=datetime.now(timezone.utc))
241
+ embedding = mapped_column(Vector(1536))
242
+
243
+ document = relationship("Document", back_populates="notes")
244
+ author = relationship("User", back_populates="notes")
245
+ groups = relationship("Group", back_populates="notes")
246
+ chunks = relationship(
247
+ "Chunk",
248
+ back_populates="note",
249
+ cascade="all, delete",
250
+ passive_deletes=True,
251
+ )
252
+ references = relationship(
253
+ "Reference",
254
+ back_populates="note",
255
+ cascade="all, delete",
256
+ passive_deletes=True,
257
+ )
258
+
259
+
260
+ class Chunk(Base):
261
+ __tablename__ = "chunk"
262
+ __table_args__ = {"schema": "public"}
263
+
264
+ chunk_id: Mapped[str] = mapped_column(String(36), primary_key=True, init=False, default=lambda: str(uuid4()))
265
+ hash: Mapped[str] = mapped_column(String(32))
266
+ content: Mapped[str] = mapped_column(Text)
267
+ document_id: Mapped[str | None] = mapped_column(ForeignKey("public.document.document_id", ondelete="CASCADE"), index=True, nullable=True)
268
+ note_id: Mapped[str | None] = mapped_column(ForeignKey("public.note.note_id", ondelete="CASCADE"), index=True, nullable=True)
269
+ chunk_type: Mapped[str] = mapped_column(String(16), default="document")
270
+ embedding = mapped_column(Vector(1536))
271
+
272
+ document = relationship("Document", back_populates="chunks")
273
+ note = relationship("Note", back_populates="chunks")
274
+ references = relationship(
275
+ "Reference",
276
+ back_populates="chunk",
277
+ cascade="all, delete",
278
+ passive_deletes=True,
279
+ )
280
+ feedbacks = relationship(
281
+ "Feedback",
282
+ back_populates="chunk",
283
+ cascade="all, delete",
284
+ passive_deletes=True,
285
+ )
286
+
287
+
288
+ class Reference(Base):
289
+ __tablename__ = "reference"
290
+ __table_args__ = {"schema": "public"}
291
+
292
+ reference_id: Mapped[str] = mapped_column(String(36), primary_key=True, init=False, default=lambda: str(uuid4()))
293
+ source_chunk_id: Mapped[str] = mapped_column(ForeignKey("public.chunk.chunk_id", ondelete="CASCADE"), index=True)
294
+ reference_document_id: Mapped[str | None] = mapped_column(ForeignKey("public.document.document_id", ondelete="CASCADE"), index=True, nullable=True)
295
+ reference_note_id: Mapped[str | None] = mapped_column(ForeignKey("public.note.note_id", ondelete="CASCADE"), index=True, nullable=True)
296
+ reference_type: Mapped[str] = mapped_column(String(16), default="document")
297
+
298
+ chunk = relationship("Chunk", back_populates="references")
299
+ document = relationship("Document", back_populates="references")
300
+ note = relationship("Note", back_populates="references")
301
+
302
+
303
+ class Feedback(Base):
304
+ __tablename__ = "feedback"
305
+ __table_args__ = {"schema": "public"}
306
+
307
+ feedback_id: Mapped[str] = mapped_column(String(36), primary_key=True, init=False, default=lambda: str(uuid4()))
308
+ source_chunk_id: Mapped[str] = mapped_column(ForeignKey("public.chunk.chunk_id", ondelete="CASCADE"), index=True)
309
+ feedback: Mapped[str] = mapped_column(Text)
310
+ model: Mapped[str] = mapped_column(Text)
311
+ timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=datetime.now(timezone.utc))
312
+ user_feedback: Mapped[str | None] = mapped_column(String(16), nullable=True, default=None)
313
+ is_hidden: Mapped[bool] = mapped_column(Boolean, default=False)
314
+
315
+ chunk = relationship("Chunk", back_populates="feedbacks")
316
+
317
+
318
+ user_to_group_table = Table(
319
+ "user_to_group",
320
+ Base.metadata,
321
+ Column("user_id", ForeignKey("public.user.user_id", ondelete="CASCADE"), primary_key=True),
322
+ Column("group_id", ForeignKey("public.group.group_id", ondelete="CASCADE"), primary_key=True),
323
+ )
324
+
325
+
326
+ admin_to_group_table = Table(
327
+ "admin_to_group",
328
+ Base.metadata,
329
+ Column("admin_id", ForeignKey("public.administrator.admin_id", ondelete="CASCADE"), primary_key=True),
330
+ Column("group_id", ForeignKey("public.group.group_id", ondelete="CASCADE"), primary_key=True),
331
+ )
332
+
333
+
334
+ document_to_group_table = Table(
335
+ "document_to_group",
336
+ Base.metadata,
337
+ Column("document_id", ForeignKey("public.document.document_id", ondelete="CASCADE"), primary_key=True),
338
+ Column("group_id", ForeignKey("public.group.group_id", ondelete="CASCADE"), primary_key=True),
339
+ )
340
+
341
+ note_to_group_table = Table(
342
+ "note_to_group",
343
+ Base.metadata,
344
+ Column("note_id", ForeignKey("public.note.note_id", ondelete="CASCADE"), primary_key=True),
345
+ Column("group_id", ForeignKey("public.group.group_id", ondelete="CASCADE"), primary_key=True),
346
+ )
347
+
348
+
349
+ try:
350
+ from compair_cloud.models import extend_models # type: ignore
351
+ except (ImportError, ModuleNotFoundError):
352
+ extend_models = None
353
+
354
+ if extend_models:
355
+ extend_models(Base, globals())
compair/schema.py ADDED
@@ -0,0 +1,146 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import Optional
5
+
6
+ from pydantic import BaseModel
7
+
8
+
9
+ class GroupForm:
10
+ name: str
11
+ user_id: Optional[str] = None
12
+ group_id: Optional[str] = None
13
+ datetime_created: Optional[datetime] = None
14
+ category: Optional[str] = None
15
+ description: Optional[str] = None
16
+ visibility: Optional[str] = None
17
+
18
+
19
+ class Group(BaseModel):
20
+ name: str
21
+ user_id: Optional[str] = None
22
+ group_id: Optional[str] = None
23
+ datetime_created: Optional[datetime] = None
24
+ group_image: Optional[str] = None
25
+ category: Optional[str] = None
26
+ description: Optional[str] = None
27
+ visibility: Optional[str] = None
28
+
29
+ model_config = {"from_attributes": True}
30
+
31
+
32
+ class User(BaseModel):
33
+ user_id: str
34
+ username: str
35
+ name: str
36
+ datetime_registered: datetime
37
+ status: str
38
+ groups: Optional[list[Group]] = None
39
+ profile_image: Optional[str] = None
40
+ role: Optional[str] = None
41
+
42
+ model_config = {"from_attributes": True}
43
+
44
+
45
+ class UpdateUserRequest(BaseModel):
46
+ user_id: str
47
+ name: Optional[str] = None
48
+ role: Optional[str] = None
49
+ group_ids: Optional[list[str]] = None
50
+
51
+
52
+ class Session(BaseModel):
53
+ id: str
54
+ user_id: str
55
+ datetime_created: datetime
56
+ datetime_valid_until: datetime
57
+
58
+
59
+ class Document(BaseModel):
60
+ document_id: str
61
+ user_id: str
62
+ author_id: str
63
+ groups: list[Group]
64
+ user: User
65
+ title: str
66
+ content: str
67
+ doc_type: str
68
+ datetime_created: datetime
69
+ datetime_modified: datetime
70
+ is_published: bool
71
+ file_key: Optional[str] = None
72
+ image_key: Optional[str] = None
73
+
74
+ model_config = {"from_attributes": True}
75
+
76
+
77
+ class Chunk(BaseModel):
78
+ chunk_id: str
79
+ hash: str
80
+ document_id: str
81
+ content: str
82
+
83
+
84
+ class Reference(BaseModel):
85
+ reference_id: str
86
+ source_chunk_id: str
87
+ reference_document_id: str
88
+ document: Document
89
+ document_author: str
90
+
91
+
92
+ class Feedback(BaseModel):
93
+ feedback_id: str
94
+ source_chunk_id: str
95
+ feedback: str
96
+ user_feedback: str | None = None
97
+
98
+
99
+ class LoginRequest(BaseModel):
100
+ username: str
101
+ password: str
102
+
103
+
104
+ class SignUpRequest(BaseModel):
105
+ username: str
106
+ name: str
107
+ password: str
108
+ groups: list[Group] | None
109
+ referral_code: str | None = None
110
+
111
+
112
+ class ForgotPasswordRequest(BaseModel):
113
+ email: str
114
+
115
+
116
+ class ResetPasswordRequest(BaseModel):
117
+ token: str
118
+ new_password: str
119
+
120
+
121
+ class Note(BaseModel):
122
+ note_id: str
123
+ document_id: str
124
+ author_id: str
125
+ group_id: str | None = None
126
+ content: str
127
+ datetime_created: datetime
128
+ author: User | None = None
129
+ group: Group | None = None
130
+
131
+
132
+ class InviteToGroupRequest(BaseModel):
133
+ admin_id: str
134
+ group_id: str
135
+ email: str
136
+
137
+
138
+ class InviteMemberRequest(BaseModel):
139
+ admin_id: str
140
+ group_id: str
141
+ username: str
142
+
143
+
144
+ class RemoveMemberRequest(BaseModel):
145
+ group_id: str
146
+ user_id: str
compair/tasks.py ADDED
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import Mapping
5
+
6
+ try:
7
+ from compair_cloud.tasks import ( # type: ignore
8
+ process_document_task,
9
+ process_text_task,
10
+ check_trial_expirations,
11
+ expire_group_invitations,
12
+ send_trial_warnings,
13
+ send_feature_announcement_task,
14
+ send_deactivate_request_email,
15
+ send_help_request_email,
16
+ send_daily_usage_report,
17
+ )
18
+ except (ImportError, ModuleNotFoundError):
19
+ from sqlalchemy.orm import joinedload
20
+
21
+ def _lazy_components():
22
+ from . import Session as SessionMaker
23
+ from .embeddings import Embedder
24
+ from .feedback import Reviewer
25
+ from .logger import log_event
26
+ from .main import process_document
27
+ from .models import Document, User
28
+
29
+ return SessionMaker, Embedder, Reviewer, log_event, process_document, Document, User
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ def process_document_task(
34
+ user_id: str,
35
+ doc_id: str,
36
+ doc_text: str,
37
+ generate_feedback: bool = True,
38
+ ) -> Mapping[str, list[str]]:
39
+ SessionMaker, Embedder, Reviewer, log_event, process_document, Document, User = _lazy_components()
40
+ with SessionMaker() as session:
41
+ user = session.query(User).filter(User.user_id == user_id).first()
42
+ if not user:
43
+ logger.warning("User not found for document processing", extra={"user_id": user_id})
44
+ return {"chunk_task_ids": []}
45
+
46
+ doc = (
47
+ session.query(Document)
48
+ .options(joinedload(Document.groups))
49
+ .filter(Document.document_id == doc_id)
50
+ .first()
51
+ )
52
+ if not doc:
53
+ logger.warning("Document not found for processing", extra={"document_id": doc_id})
54
+ return {"chunk_task_ids": []}
55
+
56
+ doc.content = doc_text
57
+ session.add(doc)
58
+
59
+ embedder = Embedder()
60
+ reviewer = Reviewer()
61
+
62
+ process_document(user, session, embedder, reviewer, doc, generate_feedback=generate_feedback)
63
+
64
+ log_event(
65
+ "core_document_processed",
66
+ user_id=user_id,
67
+ document_id=doc_id,
68
+ feedback_requested=generate_feedback,
69
+ )
70
+
71
+ return {"chunk_task_ids": []}
72
+
73
+ def process_text_task(*args, **kwargs): # pragma: no cover
74
+ raise RuntimeError("process_text_task is only available in the Compair Cloud edition.")
75
+
76
+ def check_trial_expirations(): # pragma: no cover
77
+ raise RuntimeError("check_trial_expirations is only available in the Compair Cloud edition.")
78
+
79
+ def expire_group_invitations(): # pragma: no cover
80
+ raise RuntimeError("expire_group_invitations is only available in the Compair Cloud edition.")
81
+
82
+ def send_trial_warnings(): # pragma: no cover
83
+ raise RuntimeError("send_trial_warnings is only available in the Compair Cloud edition.")
84
+
85
+ def send_feature_announcement_task(): # pragma: no cover
86
+ raise RuntimeError("send_feature_announcement_task is only available in the Compair Cloud edition.")
87
+
88
+ def send_deactivate_request_email(*args, **kwargs): # pragma: no cover
89
+ raise RuntimeError("send_deactivate_request_email is only available in the Compair Cloud edition.")
90
+
91
+ def send_help_request_email(*args, **kwargs): # pragma: no cover
92
+ raise RuntimeError("send_help_request_email is only available in the Compair Cloud edition.")
93
+
94
+ def send_daily_usage_report(): # pragma: no cover
95
+ raise RuntimeError("send_daily_usage_report is only available in the Compair Cloud edition.")
96
+
97
+ def process_file_with_ocr_task(*args, **kwargs): # pragma: no cover
98
+ raise RuntimeError("OCR processing is only available in the Compair Cloud edition.")
compair/utils.py ADDED
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ import secrets
4
+ from datetime import datetime, timedelta, timezone
5
+ from typing import Any
6
+
7
+ from sqlalchemy.orm import Session
8
+
9
+ try:
10
+ from compair_cloud.utils import log_activity as cloud_log_activity # type: ignore
11
+ except (ImportError, ModuleNotFoundError):
12
+ cloud_log_activity = None
13
+
14
+
15
+ def chunk_text(text: str) -> list[str]:
16
+ chunks = text.split("\n\n")
17
+ chunks = [c.strip() for c in chunks]
18
+ return [c for c in chunks if c]
19
+
20
+
21
+ def generate_verification_token() -> tuple[str, datetime]:
22
+ token = secrets.token_urlsafe(32)
23
+ expiration = datetime.now(timezone.utc) + timedelta(hours=24)
24
+ return token, expiration
25
+
26
+
27
+ def log_activity(
28
+ session: Session,
29
+ user_id: str,
30
+ group_id: str,
31
+ action: str,
32
+ object_id: str,
33
+ object_name: str,
34
+ object_type: str,
35
+ ) -> None:
36
+ if cloud_log_activity:
37
+ cloud_log_activity(
38
+ session=session,
39
+ user_id=user_id,
40
+ group_id=group_id,
41
+ action=action,
42
+ object_id=object_id,
43
+ object_name=object_name,
44
+ object_type=object_type,
45
+ )
46
+
47
+
48
+ def aggregate_usage_by_user() -> dict[str, Any]:
49
+ if cloud_log_activity:
50
+ from compair_cloud.utils import aggregate_usage_by_user as cloud_usage # type: ignore
51
+
52
+ return cloud_usage()
53
+ return {}
54
+
55
+
56
+ def aggregate_service_resources() -> dict[str, Any]:
57
+ if cloud_log_activity:
58
+ from compair_cloud.utils import aggregate_service_resources as cloud_resources # type: ignore
59
+
60
+ return cloud_resources()
61
+ return {}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: compair-core
3
- Version: 0.3.1
3
+ Version: 0.3.3
4
4
  Summary: Open-source foundation of the Compair collaboration platform.
5
5
  Author: RocketResearch, Inc.
6
6
  License: MIT
@@ -96,8 +96,8 @@ See `compair_core/server/settings.py` for the full settings surface.
96
96
  ```bash
97
97
  python -m venv .venv
98
98
  source .venv/bin/activate
99
- pip install -e .[dev]
100
- uvicorn compair_core.server.app:create_app --factory --reload
99
+ pip install -e ".[dev]"
100
+ uvicorn compair.server.app:create_app --factory --reload
101
101
  ```
102
102
 
103
103
  The API will be available at http://127.0.0.1:8000 and supports the Swagger UI at `/docs`.
@@ -106,6 +106,8 @@ The API will be available at http://127.0.0.1:8000 and supports the Swagger UI a
106
106
 
107
107
  Core currently ships with a syntax sanity check (`python -m compileall ...`). You can add pytest or other tooling as needed.
108
108
 
109
+ Release and packaging steps are documented in `docs/maintainers.md`.
110
+
109
111
  ## Reporting Issues
110
112
 
111
113
  Please open GitHub issues or PRs against this repository. If you are a Compair Cloud customer, reach out through your support channel for issues related to premium features.