compair-core 0.3.1__py3-none-any.whl → 0.3.2__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/__init__.py +87 -0
- compair/celery_app.py +22 -0
- compair/default_groups.py +14 -0
- compair/embeddings.py +66 -0
- compair/feedback.py +79 -0
- compair/logger.py +29 -0
- compair/main.py +240 -0
- compair/models.py +355 -0
- compair/schema.py +146 -0
- compair/tasks.py +94 -0
- compair/utils.py +61 -0
- {compair_core-0.3.1.dist-info → compair_core-0.3.2.dist-info}/METADATA +5 -3
- compair_core-0.3.2.dist-info/RECORD +36 -0
- compair_core-0.3.2.dist-info/top_level.txt +3 -0
- compair_email/__init__.py +0 -0
- compair_email/email.py +6 -0
- compair_email/email_core.py +15 -0
- compair_email/templates.py +6 -0
- compair_email/templates_core.py +13 -0
- server/__init__.py +0 -0
- server/app.py +90 -0
- server/deps.py +67 -0
- server/local_model/__init__.py +1 -0
- server/local_model/app.py +62 -0
- server/providers/__init__.py +0 -0
- server/providers/console_mailer.py +9 -0
- server/providers/contracts.py +66 -0
- server/providers/local_storage.py +28 -0
- server/providers/noop_analytics.py +7 -0
- server/providers/noop_billing.py +30 -0
- server/providers/noop_ocr.py +10 -0
- server/routers/__init__.py +0 -0
- server/routers/capabilities.py +38 -0
- server/settings.py +51 -0
- compair_core-0.3.1.dist-info/RECORD +0 -5
- compair_core-0.3.1.dist-info/top_level.txt +0 -1
- {compair_core-0.3.1.dist-info → compair_core-0.3.2.dist-info}/WHEEL +0 -0
- {compair_core-0.3.1.dist-info → compair_core-0.3.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
compair/__init__.py,sha256=OUXUSHK8JTlWMgCpzodE1B6Xc7glkCcRMHZQi02sRCM,2594
|
|
2
|
+
compair/celery_app.py,sha256=OM_Saza9yC9Q0kz_WXctfswrKkG7ruT52Zl5E4guiT0,640
|
|
3
|
+
compair/default_groups.py,sha256=dbacrFkSjqEQZ_uoFU5gYhgIoP_3lmvz6LJNHCJvxlw,498
|
|
4
|
+
compair/embeddings.py,sha256=ELpMYWBw3nCjxTx9vLrDiHYptzlzj_7JW6jZAf2Iql4,2341
|
|
5
|
+
compair/feedback.py,sha256=jgDxYKo5PzW2p-uLf5ETmlL-mDAfqzJazO1NZDK-Z-g,2755
|
|
6
|
+
compair/logger.py,sha256=mB9gV3FfC0qE_G9NBErpEAJhyGxDxEVKqWKu3n8hOkc,802
|
|
7
|
+
compair/main.py,sha256=uc3q5TY3TZdC9UsqnbHX741rplNoveK0BwYOoCi0cCA,8073
|
|
8
|
+
compair/models.py,sha256=J24eVQSPdiz13VdrKe8QsZh-nte8OZySf4LQwp4wPuU,13891
|
|
9
|
+
compair/schema.py,sha256=TxQpDQ96J_tIj-Y_1C_x2eUYn9n_LOG6XiNLCX1-GYY,2902
|
|
10
|
+
compair/tasks.py,sha256=_wvwjeHMyH7dD9zbMyGDfwMp9KqRxR1901LX5rSsLbo,3590
|
|
11
|
+
compair/utils.py,sha256=5Cw3FrOsCc5jfO4R5Ghg_TXvh3DwHICl2PdwLZDm3XY,1581
|
|
12
|
+
compair_core-0.3.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
13
|
+
compair_email/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
compair_email/email.py,sha256=bedTrcfmi4K2kB0B2bSbryhq5k_4C-QNtb8wZjvzKAs,238
|
|
15
|
+
compair_email/email_core.py,sha256=da7JxTo5ude55mB7UNLlpNp8xenYwoPaqyTunxjU7to,316
|
|
16
|
+
compair_email/templates.py,sha256=JVlLdJEcpu14mVKRAYRIPIw2JGy70kG70mfjXgby-To,206
|
|
17
|
+
compair_email/templates_core.py,sha256=1jPbXo36TjzNDCgYwL_4FyT5SsAZe5r6ufJe6W94eeE,424
|
|
18
|
+
server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
server/app.py,sha256=XjdNPBX7978BWjWYS_66ekfrDuDsTWAEfndemaMFQF4,3413
|
|
20
|
+
server/deps.py,sha256=0X-Z5JQGeXwbMooWIOC2kXVmsiJIvgUtqkK2PmDjKpI,1557
|
|
21
|
+
server/settings.py,sha256=eqHrrv62h-l2VTDtiKPVfB5BV_ysA4W2qfX7Nxn7SNo,1476
|
|
22
|
+
server/local_model/__init__.py,sha256=YlzDgorgAjGou9d_W29Xp3TVu08e4t9x8csFxn8cgSE,50
|
|
23
|
+
server/local_model/app.py,sha256=2bOLjgAyKnFcng2lmDVMB0G6WgnnVGjHyj2X8TeJjnU,1626
|
|
24
|
+
server/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
|
+
server/providers/console_mailer.py,sha256=7ady954yOPlT8t3spkDvdMdO3BTwosJUj1cpVNOwj8U,308
|
|
26
|
+
server/providers/contracts.py,sha256=pYA_2AaPHw089O_UP2JtWRHbIiVkoNGhLuesuNATowU,1858
|
|
27
|
+
server/providers/local_storage.py,sha256=UNCDHejeWlDlPVDRdwsN13toNRi_qHiaX-kbmzxf0OA,1054
|
|
28
|
+
server/providers/noop_analytics.py,sha256=OKw23SObxBlQzFdB0xEBg5qD1Fcq_bcx6OOb4S3Mbd0,194
|
|
29
|
+
server/providers/noop_billing.py,sha256=V18Cpl1D1reM3xhgw-lShGliVpYO8IsiAPWOAIR34jM,1358
|
|
30
|
+
server/providers/noop_ocr.py,sha256=fMaJrivDef38-ECgIuTXUBCIm_avgvZf3nQ3UTdFPNI,341
|
|
31
|
+
server/routers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
|
+
server/routers/capabilities.py,sha256=FZBLgmlIw6tWZiuihpzRmZ9AvbrY1jolVs7zqnhNPRU,1150
|
|
33
|
+
compair_core-0.3.2.dist-info/METADATA,sha256=AQeN3eKdTcJGKJZKF4UAaKL7Mje41ONRutMhXW6d58s,4533
|
|
34
|
+
compair_core-0.3.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
35
|
+
compair_core-0.3.2.dist-info/top_level.txt,sha256=-UjF8_GOz0G56FNiZDZnMSkEpnzBX__7qNrKpKLOqNA,29
|
|
36
|
+
compair_core-0.3.2.dist-info/RECORD,,
|
|
File without changes
|
compair_email/email.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from redmail import EmailSender
|
|
4
|
+
|
|
5
|
+
EMAIL_HOST = f"{os.environ.get('EMAIL_HOST')}"
|
|
6
|
+
EMAIL_USER = f"{os.environ.get('EMAIL_USER')}"
|
|
7
|
+
EMAIL_PW = f"{os.environ.get('EMAIL_PW')}"
|
|
8
|
+
|
|
9
|
+
emailer = EmailSender(
|
|
10
|
+
host=EMAIL_HOST,
|
|
11
|
+
port=587,
|
|
12
|
+
username=EMAIL_USER,
|
|
13
|
+
password=EMAIL_PW,
|
|
14
|
+
use_starttls=True
|
|
15
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
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()
|
server/__init__.py
ADDED
|
File without changes
|
server/app.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""FastAPI app factory supporting Core and Cloud editions."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from fastapi import FastAPI
|
|
5
|
+
|
|
6
|
+
from .deps import (
|
|
7
|
+
get_analytics,
|
|
8
|
+
get_billing,
|
|
9
|
+
get_mailer,
|
|
10
|
+
get_ocr,
|
|
11
|
+
get_settings_dependency,
|
|
12
|
+
get_storage,
|
|
13
|
+
)
|
|
14
|
+
from .providers.local_storage import LocalStorage
|
|
15
|
+
from .routers.capabilities import router as capabilities_router
|
|
16
|
+
from .settings import Settings, get_settings
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _normalize_edition(value: str) -> str:
|
|
20
|
+
return (value or "core").lower()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def create_app(settings: Settings | None = None) -> FastAPI:
|
|
24
|
+
"""Instantiate the FastAPI application with edition-specific wiring."""
|
|
25
|
+
|
|
26
|
+
resolved_settings = settings or get_settings()
|
|
27
|
+
edition = _normalize_edition(resolved_settings.edition)
|
|
28
|
+
|
|
29
|
+
app = FastAPI(title="Compair API", version=resolved_settings.version)
|
|
30
|
+
|
|
31
|
+
from api import router as legacy_router
|
|
32
|
+
|
|
33
|
+
app.include_router(legacy_router)
|
|
34
|
+
app.include_router(capabilities_router)
|
|
35
|
+
|
|
36
|
+
# Share the resolved settings with request handlers
|
|
37
|
+
app.dependency_overrides[get_settings_dependency] = lambda: resolved_settings
|
|
38
|
+
|
|
39
|
+
if edition == "cloud":
|
|
40
|
+
try:
|
|
41
|
+
from compair_cloud.analytics.ga4 import GA4Analytics
|
|
42
|
+
from compair_cloud.billing.stripe_provider import StripeBilling
|
|
43
|
+
from compair_cloud.mailer.transactional import TransactionalMailer
|
|
44
|
+
from compair_cloud.ocr.claude_ocr import ClaudeOCR
|
|
45
|
+
from compair_cloud.storage.r2_storage import R2Storage
|
|
46
|
+
except ImportError as exc: # pragma: no cover - only triggered in misconfigured builds
|
|
47
|
+
raise RuntimeError(
|
|
48
|
+
"Cloud edition requires the private 'compair_cloud' package to be installed."
|
|
49
|
+
) from exc
|
|
50
|
+
|
|
51
|
+
storage_provider = R2Storage(
|
|
52
|
+
bucket=resolved_settings.r2_bucket,
|
|
53
|
+
cdn_base=resolved_settings.r2_cdn_base,
|
|
54
|
+
access_key=resolved_settings.r2_access_key,
|
|
55
|
+
secret_key=resolved_settings.r2_secret_key,
|
|
56
|
+
endpoint_url=resolved_settings.r2_endpoint_url,
|
|
57
|
+
)
|
|
58
|
+
billing_provider = StripeBilling(
|
|
59
|
+
stripe_key=resolved_settings.stripe_key,
|
|
60
|
+
endpoint_secret=resolved_settings.stripe_endpoint_secret,
|
|
61
|
+
)
|
|
62
|
+
ocr_provider = ClaudeOCR()
|
|
63
|
+
mailer_provider = TransactionalMailer()
|
|
64
|
+
|
|
65
|
+
analytics_provider = None
|
|
66
|
+
if resolved_settings.ga4_measurement_id and resolved_settings.ga4_api_secret:
|
|
67
|
+
analytics_provider = GA4Analytics(
|
|
68
|
+
measurement_id=resolved_settings.ga4_measurement_id,
|
|
69
|
+
api_secret=resolved_settings.ga4_api_secret,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
app.dependency_overrides[get_storage] = lambda sp=storage_provider: sp
|
|
73
|
+
app.dependency_overrides[get_billing] = lambda bp=billing_provider: bp
|
|
74
|
+
app.dependency_overrides[get_ocr] = lambda op=ocr_provider: op
|
|
75
|
+
if analytics_provider is not None:
|
|
76
|
+
app.dependency_overrides[get_analytics] = lambda ap=analytics_provider: ap
|
|
77
|
+
app.dependency_overrides[get_mailer] = lambda mp=mailer_provider: mp
|
|
78
|
+
|
|
79
|
+
else:
|
|
80
|
+
storage_provider = LocalStorage(
|
|
81
|
+
base_dir=resolved_settings.local_upload_dir,
|
|
82
|
+
base_url=resolved_settings.local_upload_base_url,
|
|
83
|
+
)
|
|
84
|
+
app.dependency_overrides[get_storage] = lambda sp=storage_provider: sp
|
|
85
|
+
|
|
86
|
+
return app
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# Uvicorn compatibility: allow ``uvicorn server.app:app``
|
|
90
|
+
app = create_app()
|
server/deps.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Dependency entry points for features that differ by edition."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
|
|
6
|
+
from fastapi import Depends
|
|
7
|
+
|
|
8
|
+
from .providers.console_mailer import ConsoleMailer
|
|
9
|
+
from .providers.contracts import Analytics, BillingProvider, Mailer, OCRProvider, StorageProvider
|
|
10
|
+
from .providers.local_storage import LocalStorage
|
|
11
|
+
from .providers.noop_analytics import NoopAnalytics
|
|
12
|
+
from .providers.noop_billing import NoopBilling
|
|
13
|
+
from .providers.noop_ocr import NoopOCR
|
|
14
|
+
from .settings import Settings, get_settings
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@lru_cache
|
|
18
|
+
def _local_storage_factory(base_dir: str, base_url: str) -> LocalStorage:
|
|
19
|
+
return LocalStorage(base_dir=base_dir, base_url=base_url)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@lru_cache
|
|
23
|
+
def _noop_billing() -> NoopBilling:
|
|
24
|
+
return NoopBilling()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@lru_cache
|
|
28
|
+
def _noop_ocr() -> NoopOCR:
|
|
29
|
+
return NoopOCR()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@lru_cache
|
|
33
|
+
def _console_mailer() -> ConsoleMailer:
|
|
34
|
+
return ConsoleMailer()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@lru_cache
|
|
38
|
+
def _noop_analytics() -> NoopAnalytics:
|
|
39
|
+
return NoopAnalytics()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_settings_dependency() -> Settings:
|
|
43
|
+
"""Expose settings as a FastAPI dependency."""
|
|
44
|
+
return get_settings()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_storage(
|
|
49
|
+
settings: Settings = Depends(get_settings_dependency),
|
|
50
|
+
) -> StorageProvider:
|
|
51
|
+
return _local_storage_factory(settings.local_upload_dir, settings.local_upload_base_url)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_billing() -> BillingProvider:
|
|
55
|
+
return _noop_billing()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_ocr() -> OCRProvider:
|
|
59
|
+
return _noop_ocr()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_mailer() -> Mailer:
|
|
63
|
+
return _console_mailer()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_analytics() -> Analytics:
|
|
67
|
+
return _noop_analytics()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Local model endpoints for the Core edition."""
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Minimal FastAPI application serving local embedding and generation endpoints."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
from fastapi import FastAPI
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
app = FastAPI(title="Compair Local Model", version="0.1.0")
|
|
11
|
+
|
|
12
|
+
EMBED_DIMENSION = 384
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _hash_embedding(text: str, dimension: int = EMBED_DIMENSION) -> List[float]:
|
|
16
|
+
if not text:
|
|
17
|
+
text = " "
|
|
18
|
+
digest = hashlib.sha256(text.encode("utf-8", "ignore")).digest()
|
|
19
|
+
vector: List[float] = []
|
|
20
|
+
while len(vector) < dimension:
|
|
21
|
+
for byte in digest:
|
|
22
|
+
vector.append((byte / 255.0) * 2 - 1)
|
|
23
|
+
if len(vector) == dimension:
|
|
24
|
+
break
|
|
25
|
+
digest = hashlib.sha256(digest).digest()
|
|
26
|
+
return vector
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class EmbedRequest(BaseModel):
|
|
30
|
+
text: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class EmbedResponse(BaseModel):
|
|
34
|
+
embedding: List[float]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class GenerateRequest(BaseModel):
|
|
38
|
+
system: str | None = None
|
|
39
|
+
prompt: str
|
|
40
|
+
verbosity: str | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class GenerateResponse(BaseModel):
|
|
44
|
+
text: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.post("/embed", response_model=EmbedResponse)
|
|
48
|
+
def embed(request: EmbedRequest) -> EmbedResponse:
|
|
49
|
+
return EmbedResponse(embedding=_hash_embedding(request.text))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.post("/generate", response_model=GenerateResponse)
|
|
53
|
+
def generate(request: GenerateRequest) -> GenerateResponse:
|
|
54
|
+
prompt = request.prompt.strip()
|
|
55
|
+
if not prompt:
|
|
56
|
+
return GenerateResponse(text="NONE")
|
|
57
|
+
|
|
58
|
+
first_sentence = prompt.split("\n", 1)[0][:200]
|
|
59
|
+
verbosity = request.verbosity or "default"
|
|
60
|
+
return GenerateResponse(
|
|
61
|
+
text=f"[local-{verbosity}] Key takeaway: {first_sentence}"
|
|
62
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Console mailer used in Core builds to avoid delivering real email."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Iterable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ConsoleMailer:
|
|
8
|
+
def send(self, subject: str, sender: str, receivers: Iterable[str], html: str) -> None:
|
|
9
|
+
print(f"[MAIL] {subject} -> {list(receivers)}")
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Provider protocol definitions shared across editions."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import BinaryIO, Iterable, Mapping, Protocol
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(slots=True)
|
|
9
|
+
class BillingSession:
|
|
10
|
+
"""Represents the result of creating a checkout session."""
|
|
11
|
+
|
|
12
|
+
id: str
|
|
13
|
+
url: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class StorageProvider(Protocol):
|
|
17
|
+
def put_file(self, key: str, fileobj: BinaryIO, content_type: str) -> str: ...
|
|
18
|
+
|
|
19
|
+
def get_file(self, key: str) -> tuple[BinaryIO, str]: ...
|
|
20
|
+
|
|
21
|
+
def build_url(self, key: str) -> str: ...
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BillingProvider(Protocol):
|
|
25
|
+
def ensure_customer(self, *, user_email: str, user_id: str) -> str: ...
|
|
26
|
+
|
|
27
|
+
def create_checkout_session(
|
|
28
|
+
self,
|
|
29
|
+
*,
|
|
30
|
+
customer_id: str,
|
|
31
|
+
price_id: str,
|
|
32
|
+
qty: int,
|
|
33
|
+
success_url: str,
|
|
34
|
+
cancel_url: str,
|
|
35
|
+
metadata: Mapping[str, str] | None = None,
|
|
36
|
+
) -> BillingSession: ...
|
|
37
|
+
|
|
38
|
+
def retrieve_session(self, session_id: str) -> BillingSession: ...
|
|
39
|
+
|
|
40
|
+
def get_checkout_url(self, session_id: str) -> str: ...
|
|
41
|
+
|
|
42
|
+
def create_customer_portal(self, *, customer_id: str, return_url: str) -> str: ...
|
|
43
|
+
|
|
44
|
+
def create_coupon(self, amount: int) -> str: ...
|
|
45
|
+
|
|
46
|
+
def apply_coupon(self, *, customer_id: str, coupon_id: str) -> None: ...
|
|
47
|
+
|
|
48
|
+
def construct_event(self, payload: bytes, signature: str | None) -> Mapping[str, object]: ...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class OCRProvider(Protocol):
|
|
52
|
+
def submit(
|
|
53
|
+
self, *, user_id: str, filename: str, data: bytes, document_id: str | None
|
|
54
|
+
) -> str: ...
|
|
55
|
+
|
|
56
|
+
def status(self, task_id: str) -> Mapping[str, object]: ...
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Mailer(Protocol):
|
|
60
|
+
def send(self, subject: str, sender: str, receivers: Iterable[str], html: str) -> None: ...
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Analytics(Protocol):
|
|
64
|
+
def track(
|
|
65
|
+
self, event_name: str, user_id: str, params: Mapping[str, object] | None = None
|
|
66
|
+
) -> None: ...
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Local filesystem storage provider used in the Core edition."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import BinaryIO
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LocalStorage:
|
|
9
|
+
def __init__(self, base_dir: str = "/data/uploads", base_url: str = "/uploads") -> None:
|
|
10
|
+
self.base_dir = Path(base_dir)
|
|
11
|
+
self.base_url = base_url.rstrip("/") or "/uploads"
|
|
12
|
+
self.base_dir.mkdir(parents=True, exist_ok=True)
|
|
13
|
+
|
|
14
|
+
def put_file(self, key: str, fileobj: BinaryIO, content_type: str) -> str:
|
|
15
|
+
destination = self.base_dir / key
|
|
16
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
with destination.open("wb") as dest:
|
|
18
|
+
dest.write(fileobj.read())
|
|
19
|
+
return self.build_url(key)
|
|
20
|
+
|
|
21
|
+
def get_file(self, key: str) -> tuple[BinaryIO, str]:
|
|
22
|
+
path = self.base_dir / key
|
|
23
|
+
if not path.exists():
|
|
24
|
+
raise FileNotFoundError(key)
|
|
25
|
+
return path.open("rb"), "application/octet-stream"
|
|
26
|
+
|
|
27
|
+
def build_url(self, key: str) -> str:
|
|
28
|
+
return f"{self.base_url}/{key}".replace("//", "/")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Billing provider placeholder for the Core edition."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .contracts import BillingSession
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class NoopBilling:
|
|
8
|
+
def ensure_customer(self, **_: object) -> str:
|
|
9
|
+
raise NotImplementedError("Billing is not available in the Core edition.")
|
|
10
|
+
|
|
11
|
+
def create_checkout_session(self, **_: object) -> BillingSession:
|
|
12
|
+
raise NotImplementedError("Billing is not available in the Core edition.")
|
|
13
|
+
|
|
14
|
+
def retrieve_session(self, *_: object, **__: object) -> BillingSession:
|
|
15
|
+
raise NotImplementedError("Billing is not available in the Core edition.")
|
|
16
|
+
|
|
17
|
+
def get_checkout_url(self, *_: object, **__: object) -> str:
|
|
18
|
+
raise NotImplementedError("Billing is not available in the Core edition.")
|
|
19
|
+
|
|
20
|
+
def create_customer_portal(self, *_: object, **__: object) -> str:
|
|
21
|
+
raise NotImplementedError("Billing is not available in the Core edition.")
|
|
22
|
+
|
|
23
|
+
def create_coupon(self, *_: object, **__: object) -> str:
|
|
24
|
+
raise NotImplementedError("Billing is not available in the Core edition.")
|
|
25
|
+
|
|
26
|
+
def apply_coupon(self, *_: object, **__: object) -> None:
|
|
27
|
+
raise NotImplementedError("Billing is not available in the Core edition.")
|
|
28
|
+
|
|
29
|
+
def construct_event(self, *_: object, **__: object) -> dict[str, object]:
|
|
30
|
+
raise NotImplementedError("Billing is not available in the Core edition.")
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""OCR provider placeholder for the Core edition."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class NoopOCR:
|
|
6
|
+
def submit(self, **_: object) -> str:
|
|
7
|
+
raise NotImplementedError("OCR is not available in the Core edition.")
|
|
8
|
+
|
|
9
|
+
def status(self, task_id: str) -> dict[str, object]:
|
|
10
|
+
return {"status": "unknown", "task_id": task_id}
|
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Meta endpoints that describe edition capabilities for the CLI."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, Depends
|
|
5
|
+
|
|
6
|
+
from ..settings import Settings, get_settings
|
|
7
|
+
|
|
8
|
+
router = APIRouter(tags=["meta"])
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@router.get("/capabilities")
|
|
12
|
+
def capabilities(settings: Settings = Depends(get_settings)) -> dict[str, object]:
|
|
13
|
+
edition = settings.edition.lower()
|
|
14
|
+
return {
|
|
15
|
+
"auth": {
|
|
16
|
+
"device_flow": edition == "cloud",
|
|
17
|
+
"password_login": True,
|
|
18
|
+
},
|
|
19
|
+
"inputs": {
|
|
20
|
+
"text": True,
|
|
21
|
+
"ocr": settings.ocr_enabled,
|
|
22
|
+
"repos": True,
|
|
23
|
+
},
|
|
24
|
+
"models": {
|
|
25
|
+
"premium": settings.premium_models,
|
|
26
|
+
"open": True,
|
|
27
|
+
},
|
|
28
|
+
"integrations": {
|
|
29
|
+
"slack": settings.integrations_enabled,
|
|
30
|
+
"github": settings.integrations_enabled,
|
|
31
|
+
},
|
|
32
|
+
"limits": {
|
|
33
|
+
"docs": None if edition == "core" else 100,
|
|
34
|
+
"feedback_per_day": None if edition == "core" else 50,
|
|
35
|
+
},
|
|
36
|
+
"server": "Compair Cloud" if edition == "cloud" else "Compair Core",
|
|
37
|
+
"version": settings.version,
|
|
38
|
+
}
|
server/settings.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Application settings and feature flag definitions."""
|
|
2
|
+
from functools import lru_cache
|
|
3
|
+
|
|
4
|
+
from pydantic_settings import BaseSettings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Settings(BaseSettings):
|
|
8
|
+
"""Configuration injected via COMPAIR_ environment variables."""
|
|
9
|
+
|
|
10
|
+
# Edition metadata
|
|
11
|
+
edition: str = "core" # core | cloud
|
|
12
|
+
version: str = "dev"
|
|
13
|
+
|
|
14
|
+
# Feature gates
|
|
15
|
+
ocr_enabled: bool = False
|
|
16
|
+
billing_enabled: bool = False
|
|
17
|
+
integrations_enabled: bool = False
|
|
18
|
+
premium_models: bool = False
|
|
19
|
+
|
|
20
|
+
# Core/local storage defaults
|
|
21
|
+
local_upload_dir: str = "/data/uploads"
|
|
22
|
+
local_upload_base_url: str = "/uploads"
|
|
23
|
+
|
|
24
|
+
# Cloud storage (R2/S3-compatible)
|
|
25
|
+
r2_bucket: str | None = None
|
|
26
|
+
r2_cdn_base: str | None = None
|
|
27
|
+
r2_access_key: str | None = None
|
|
28
|
+
r2_secret_key: str | None = None
|
|
29
|
+
r2_endpoint_url: str | None = None
|
|
30
|
+
|
|
31
|
+
# Optional cloud secrets
|
|
32
|
+
stripe_key: str | None = None
|
|
33
|
+
stripe_endpoint_secret: str | None = None
|
|
34
|
+
stripe_success_url: str = "https://compair.sh/home"
|
|
35
|
+
stripe_cancel_url: str = "https://compair.sh/home"
|
|
36
|
+
ga4_measurement_id: str | None = None
|
|
37
|
+
ga4_api_secret: str | None = None
|
|
38
|
+
|
|
39
|
+
# Local model endpoints
|
|
40
|
+
local_model_url: str = "http://local-model:9000"
|
|
41
|
+
local_embedding_route: str = "/embed"
|
|
42
|
+
local_generation_route: str = "/generate"
|
|
43
|
+
|
|
44
|
+
class Config:
|
|
45
|
+
env_prefix = "COMPAIR_"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@lru_cache
|
|
49
|
+
def get_settings() -> Settings:
|
|
50
|
+
"""Cached settings instance for dependency injection."""
|
|
51
|
+
return Settings()
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
compair_core-0.3.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
2
|
-
compair_core-0.3.1.dist-info/METADATA,sha256=I7eh0b6GwaAqI3XASxC-qYVQDVVLCpdTXWadD3CZ7hI,4466
|
|
3
|
-
compair_core-0.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
4
|
-
compair_core-0.3.1.dist-info/top_level.txt,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
5
|
-
compair_core-0.3.1.dist-info/RECORD,,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
File without changes
|
|
File without changes
|