schift-cli 0.1.0__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.
Files changed (40) hide show
  1. schift_cli/__init__.py +1 -0
  2. schift_cli/client.py +119 -0
  3. schift_cli/commands/__init__.py +0 -0
  4. schift_cli/commands/auth.py +68 -0
  5. schift_cli/commands/bench.py +65 -0
  6. schift_cli/commands/catalog.py +74 -0
  7. schift_cli/commands/db.py +96 -0
  8. schift_cli/commands/embed.py +104 -0
  9. schift_cli/commands/migrate.py +127 -0
  10. schift_cli/commands/query.py +66 -0
  11. schift_cli/commands/skill.py +110 -0
  12. schift_cli/commands/usage.py +50 -0
  13. schift_cli/config.py +58 -0
  14. schift_cli/data/schift-best-practices/AGENTS.md +77 -0
  15. schift_cli/data/schift-best-practices/CLAUDE.md +77 -0
  16. schift_cli/data/schift-best-practices/SKILL.md +89 -0
  17. schift_cli/data/schift-best-practices/references/bucket-organization.md +126 -0
  18. schift_cli/data/schift-best-practices/references/bucket-upload.md +116 -0
  19. schift_cli/data/schift-best-practices/references/chatbot-widget.md +238 -0
  20. schift_cli/data/schift-best-practices/references/cost-batching.md +179 -0
  21. schift_cli/data/schift-best-practices/references/cost-storage-tiers.md +183 -0
  22. schift_cli/data/schift-best-practices/references/deploy-cloudrun.md +140 -0
  23. schift_cli/data/schift-best-practices/references/embed-batch-processing.md +86 -0
  24. schift_cli/data/schift-best-practices/references/embed-error-handling.md +155 -0
  25. schift_cli/data/schift-best-practices/references/embed-multimodal.md +100 -0
  26. schift_cli/data/schift-best-practices/references/embed-task-types.md +135 -0
  27. schift_cli/data/schift-best-practices/references/rag-chunking.md +173 -0
  28. schift_cli/data/schift-best-practices/references/rag-workflow-builder.md +205 -0
  29. schift_cli/data/schift-best-practices/references/sdk-async-patterns.md +103 -0
  30. schift_cli/data/schift-best-practices/references/sdk-auth-patterns.md +76 -0
  31. schift_cli/data/schift-best-practices/references/search-collection-design.md +229 -0
  32. schift_cli/data/schift-best-practices/references/search-hybrid.md +163 -0
  33. schift_cli/data/schift-best-practices/references/search-similarity-tuning.md +134 -0
  34. schift_cli/display.py +85 -0
  35. schift_cli/main.py +39 -0
  36. schift_cli-0.1.0.dist-info/METADATA +12 -0
  37. schift_cli-0.1.0.dist-info/RECORD +40 -0
  38. schift_cli-0.1.0.dist-info/WHEEL +5 -0
  39. schift_cli-0.1.0.dist-info/entry_points.txt +2 -0
  40. schift_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,135 @@
1
+ ---
2
+ title: Specify task_type for optimal embedding quality
3
+ impact: HIGH
4
+ impactDescription: Using the default task_type for every use case can silently degrade retrieval accuracy by 10-30%. Schift optimizes embeddings differently depending on whether the vector will be used for search, classification, clustering, or other tasks.
5
+ tags: [embedding, task-type, retrieval, classification, quality]
6
+ ---
7
+
8
+ ## Specify task_type for optimal embedding quality
9
+
10
+ Schift supports seven task types that control how the embedding model is fine-tuned for your specific use case. The default (`retrieval`) is a safe starting point, but explicitly setting the correct task type produces better embeddings and downstream results.
11
+
12
+ | task_type | When to use |
13
+ |---|---|
14
+ | `retrieval` | Asymmetric search — query against document corpus |
15
+ | `similarity` | Symmetric similarity — comparing two pieces of text |
16
+ | `classification` | Labeling or categorizing text |
17
+ | `clustering` | Grouping documents by topic |
18
+ | `QA` | Question-answering — question against answer pairs |
19
+ | `fact_verification` | Claim against evidence matching |
20
+ | `code_retrieval` | Natural-language query against code snippets |
21
+
22
+ **Incorrect** — using default `task_type` for every case reduces embedding quality:
23
+
24
+ ```python
25
+ # Python - DON'T do this
26
+ from schift import Schift
27
+
28
+ client = Schift(api_key="sch_...")
29
+
30
+ # Indexing documents for search — no task_type set
31
+ doc_embedding = client.embed("The Eiffel Tower is in Paris.")
32
+
33
+ # Classifying customer feedback — no task_type set
34
+ label_embedding = client.embed("This product is terrible!")
35
+
36
+ # Searching code — no task_type set
37
+ code_query = client.embed("function that sorts a list")
38
+ ```
39
+
40
+ ```typescript
41
+ // TypeScript - DON'T do this
42
+ import { Schift } from '@schift-io/sdk';
43
+
44
+ const client = new Schift({ apiKey: 'sch_...' });
45
+
46
+ // All three use cases use the same default — suboptimal for each
47
+ const docEmbedding = await client.embed('The Eiffel Tower is in Paris.');
48
+ const labelEmbedding = await client.embed('This product is terrible!');
49
+ const codeQuery = await client.embed('function that sorts a list');
50
+ ```
51
+
52
+ **Correct** — match `task_type` to your use case:
53
+
54
+ ```python
55
+ # Python - DO this
56
+ from schift import Schift
57
+
58
+ client = Schift(api_key="sch_...")
59
+
60
+ # Asymmetric document search: index documents as "retrieval"
61
+ doc_embedding = client.embed(
62
+ "The Eiffel Tower is in Paris.",
63
+ task_type="retrieval",
64
+ )
65
+
66
+ # Classifying customer feedback
67
+ label_embedding = client.embed(
68
+ "This product is terrible!",
69
+ task_type="classification",
70
+ )
71
+
72
+ # Natural-language query against a code repository
73
+ code_query = client.embed(
74
+ "function that sorts a list",
75
+ task_type="code_retrieval",
76
+ )
77
+
78
+ # QA: embed question against pre-embedded answers
79
+ question_vec = client.embed(
80
+ "What year was the Eiffel Tower built?",
81
+ task_type="QA",
82
+ )
83
+
84
+ # Fact verification: claim against supporting evidence
85
+ claim_vec = client.embed(
86
+ "The Eiffel Tower was built in 1889.",
87
+ task_type="fact_verification",
88
+ )
89
+
90
+ # Batch embed with task_type
91
+ docs = ["Doc A content...", "Doc B content...", "Doc C content..."]
92
+ results = client.embed_batch(docs, task_type="retrieval")
93
+ ```
94
+
95
+ ```typescript
96
+ // TypeScript - DO this
97
+ import { Schift } from '@schift-io/sdk';
98
+
99
+ const client = new Schift({ apiKey: 'sch_...' });
100
+
101
+ // Asymmetric document search
102
+ const docEmbedding = await client.embed(
103
+ 'The Eiffel Tower is in Paris.',
104
+ { taskType: 'retrieval' },
105
+ );
106
+
107
+ // Classification
108
+ const labelEmbedding = await client.embed(
109
+ 'This product is terrible!',
110
+ { taskType: 'classification' },
111
+ );
112
+
113
+ // Code search
114
+ const codeQuery = await client.embed(
115
+ 'function that sorts a list',
116
+ { taskType: 'code_retrieval' },
117
+ );
118
+
119
+ // QA
120
+ const questionVec = await client.embed(
121
+ 'What year was the Eiffel Tower built?',
122
+ { taskType: 'QA' },
123
+ );
124
+
125
+ // Batch with task type
126
+ const docs = ['Doc A content...', 'Doc B content...', 'Doc C content...'];
127
+ const results = await client.embedBatch(docs, { taskType: 'retrieval' });
128
+ ```
129
+
130
+ Use the same `task_type` when indexing documents and when embedding queries — mixing task types in the same collection degrades search quality. For collections that serve multiple use cases, create separate collections per task type.
131
+
132
+ ## Reference
133
+
134
+ - https://docs.schift.io/api/embed#task-type
135
+ - https://docs.schift.io/concepts/task-types
@@ -0,0 +1,173 @@
1
+ ---
2
+ title: Let Schift Handle Chunking via Bucket Upload
3
+ impact: HIGH
4
+ impactDescription: Custom chunking loses document structure (headings, tables, lists), produces uneven chunk quality, and requires maintaining your own chunking logic. Schift's bucket upload handles OCR, structure-aware chunking, and embedding automatically.
5
+ tags:
6
+ - rag
7
+ - chunking
8
+ - bucket
9
+ - document-structure
10
+ - preprocessing
11
+ ---
12
+
13
+ ## Let Schift Handle Chunking via Bucket Upload
14
+
15
+ Chunking is one of the most impactful and underestimated decisions in a RAG pipeline. Naively splitting text by character count or regex destroys semantic units: headings get separated from their content, table rows become orphaned fragments, and list items lose their parent context. The result is chunks that embed poorly and retrieve incorrectly.
16
+
17
+ Schift's bucket upload pipeline runs document parsing, OCR, and structure-aware chunking on your behalf. It understands heading hierarchies, tables, code blocks, and lists — producing semantically coherent chunks that embed and retrieve at higher quality without any custom code.
18
+
19
+ ### Incorrect
20
+
21
+ Custom regex or fixed-size chunking that destroys document structure:
22
+
23
+ ```python
24
+ # Python — naive fixed-size chunking, loses all structure
25
+ import re
26
+ from schift import Schift
27
+
28
+ client = Schift(api_key="sch_...")
29
+
30
+ with open("technical_report.pdf", "rb") as f:
31
+ raw_text = extract_text_from_pdf(f) # flat string, all structure lost
32
+
33
+ # Split every 500 characters regardless of sentence or section boundaries
34
+ chunks = [raw_text[i:i+500] for i in range(0, len(raw_text), 500)]
35
+
36
+ # Problems:
37
+ # - Heading "## Performance Results" ends up in a different chunk than the table below it
38
+ # - Table rows split mid-row: "| 98.2% | la" / "tency 12ms |"
39
+ # - Code blocks broken at arbitrary positions
40
+ # - No metadata: no page number, no section, no source
41
+
42
+ for i, chunk in enumerate(chunks):
43
+ client.embed_and_insert(collection_id, chunk, metadata={"index": i})
44
+ ```
45
+
46
+ ```typescript
47
+ // TypeScript — naive regex split, loses paragraph context
48
+ import { Schift } from '@schift-io/sdk';
49
+
50
+ const client = new Schift({ apiKey: 'sch_...' });
51
+
52
+ const rawText = await extractText('technical_report.pdf'); // flat string
53
+
54
+ // Split on double newlines — misses tables, code blocks, lists
55
+ const chunks = rawText.split(/\n\n+/).filter(c => c.trim().length > 0);
56
+
57
+ // No overlap, no metadata, no awareness of document hierarchy
58
+ for (const chunk of chunks) {
59
+ await client.embedAndInsert(collectionId, chunk);
60
+ }
61
+ ```
62
+
63
+ This approach drops all structural signals that models rely on to understand context. A chunk reading `"98.2% 12ms 4.1GB"` is meaningless without the table header that provides column names.
64
+
65
+ ### Correct
66
+
67
+ Upload directly to a Schift bucket and let the pipeline handle chunking:
68
+
69
+ ```python
70
+ # Python — bucket upload: structure-aware chunking, no manual work
71
+ from schift import Schift
72
+
73
+ client = Schift(api_key="sch_...")
74
+
75
+ # Upload the raw file — Schift handles OCR, parsing, chunking, embedding
76
+ with open("technical_report.pdf", "rb") as f:
77
+ result = client.bucket.upload(
78
+ bucket_id=bucket_id,
79
+ file=f,
80
+ filename="technical_report.pdf",
81
+ metadata={
82
+ "source": "internal-research",
83
+ "version": "2026-Q1",
84
+ "department": "engineering",
85
+ }
86
+ )
87
+
88
+ print(result.chunk_count) # e.g. 142 semantically coherent chunks
89
+ print(result.document_id) # reference for deletion or re-indexing
90
+
91
+ # Schift preserves:
92
+ # - Heading hierarchy (H1 > H2 > H3 as metadata fields)
93
+ # - Table structure (header row context attached to each data row chunk)
94
+ # - Code blocks (kept intact, tagged as code)
95
+ # - List items (grouped under their parent heading)
96
+ ```
97
+
98
+ ```typescript
99
+ // TypeScript — bucket upload: let Schift do the heavy lifting
100
+ import { Schift } from '@schift-io/sdk';
101
+ import { readFileSync } from 'fs';
102
+
103
+ const client = new Schift({ apiKey: 'sch_...' });
104
+
105
+ const fileBuffer = readFileSync('technical_report.pdf');
106
+
107
+ const result = await client.bucket.upload(bucketId, fileBuffer, {
108
+ filename: 'technical_report.pdf',
109
+ metadata: {
110
+ source: 'internal-research',
111
+ version: '2026-Q1',
112
+ department: 'engineering',
113
+ },
114
+ });
115
+
116
+ console.log(result.chunkCount); // semantically coherent chunks
117
+ console.log(result.documentId); // for future reference or deletion
118
+ ```
119
+
120
+ If you must chunk manually (e.g., streaming data with no file format), follow these rules:
121
+
122
+ ```python
123
+ # Python — manual chunking fallback: overlap + boundaries + metadata
124
+ def chunk_text_safely(text: str, source: str, page: int = 0) -> list[dict]:
125
+ paragraphs = text.split("\n\n")
126
+ chunks = []
127
+ window = []
128
+ window_size = 0
129
+ target_size = 400 # tokens (approximate)
130
+ overlap_size = 60 # 10–20% overlap to preserve cross-boundary context
131
+
132
+ for para in paragraphs:
133
+ words = para.split()
134
+ window.extend(words)
135
+ window_size += len(words)
136
+
137
+ if window_size >= target_size:
138
+ chunk_text = " ".join(window)
139
+ chunks.append({
140
+ "text": chunk_text,
141
+ "metadata": {
142
+ "source": source,
143
+ "page": page,
144
+ "char_count": len(chunk_text),
145
+ }
146
+ })
147
+ # Retain overlap to preserve context across chunk boundary
148
+ window = window[-overlap_size:]
149
+ window_size = len(window)
150
+
151
+ if window: # flush remainder
152
+ chunks.append({
153
+ "text": " ".join(window),
154
+ "metadata": {"source": source, "page": page}
155
+ })
156
+
157
+ return chunks
158
+ ```
159
+
160
+ **Manual chunking checklist (if unavoidable):**
161
+
162
+ | Rule | Why |
163
+ |------|-----|
164
+ | 10–20% overlap between chunks | Prevents context loss at boundaries |
165
+ | Respect paragraph/sentence boundaries | Keeps semantic units intact |
166
+ | Include source, page, section in metadata | Enables filtered search and attribution |
167
+ | Target 300–500 tokens per chunk | Balances precision and recall |
168
+ | Never split mid-sentence | Broken sentences embed poorly |
169
+
170
+ ## Reference
171
+
172
+ - https://docs.schift.io/buckets/upload
173
+ - https://docs.schift.io/buckets/chunking
@@ -0,0 +1,205 @@
1
+ ---
2
+ title: Use WorkflowBuilder for Reproducible RAG Pipelines
3
+ impact: HIGH
4
+ impactDescription: Ad-hoc inline RAG code is hard to test, version, or share. WorkflowBuilder defines RAG as a versioned DAG — reproducible across environments, observable via run history, and shareable as templates.
5
+ tags:
6
+ - rag
7
+ - workflow
8
+ - dag
9
+ - workflowbuilder
10
+ - pipeline
11
+ - observability
12
+ ---
13
+
14
+ ## Use WorkflowBuilder for Reproducible RAG Pipelines
15
+
16
+ Embedding retrieval + reranking + LLM generation is a multi-step pipeline. When this logic lives as scattered inline code, it becomes impossible to test individual steps in isolation, reproduce a specific run for debugging, or share the pipeline as a reusable template across projects.
17
+
18
+ Schift's workflow engine models RAG pipelines as directed acyclic graphs (DAGs). Each step is a typed node (retrieve, rerank, generate, filter, transform). The `WorkflowBuilder` API in TypeScript (and its Python equivalent) lets you define, version, and register these pipelines as first-class resources. You run them by ID, and every run is logged — input, intermediate results, output — for debugging and evaluation.
19
+
20
+ ### Incorrect
21
+
22
+ Inline search + LLM call logic scattered across the application:
23
+
24
+ ```typescript
25
+ // TypeScript — ad-hoc RAG scattered across application code
26
+ import { Schift } from '@schift-io/sdk';
27
+ import OpenAI from 'openai';
28
+
29
+ const client = new Schift({ apiKey: 'sch_...' });
30
+ const openai = new OpenAI();
31
+
32
+ // This logic is duplicated in routes/chat.ts, routes/search.ts, scripts/batch.ts
33
+ // No versioning, no run history, impossible to A/B test
34
+ async function answerQuestion(question: string) {
35
+ // Step 1: retrieve — hardcoded params, no way to tune per environment
36
+ const results = await client.search(collectionId, question, { topK: 5 });
37
+
38
+ // Step 2: no reranking step — order determined by vector score alone
39
+ const context = results.map(r => r.text).join('\n\n');
40
+
41
+ // Step 3: generate — prompt template hardcoded here, not versioned
42
+ const completion = await openai.chat.completions.create({
43
+ model: 'gpt-4o',
44
+ messages: [
45
+ { role: 'system', content: 'Answer based on context.' },
46
+ { role: 'user', content: `Context:\n${context}\n\nQuestion: ${question}` },
47
+ ],
48
+ });
49
+
50
+ return completion.choices[0].message.content;
51
+ // No logging of which chunks were retrieved, no run ID, no eval surface
52
+ }
53
+ ```
54
+
55
+ ```python
56
+ # Python — same problem: logic copy-pasted across scripts, no observability
57
+ from schift import Schift
58
+ from openai import OpenAI
59
+
60
+ schift = Schift(api_key="sch_...")
61
+ openai = OpenAI()
62
+
63
+ def answer(question: str) -> str:
64
+ results = schift.search(collection_id, question, top_k=5)
65
+ context = "\n\n".join(r.text for r in results)
66
+ resp = openai.chat.completions.create(
67
+ model="gpt-4o",
68
+ messages=[
69
+ {"role": "system", "content": "Answer based on context."},
70
+ {"role": "user", "content": f"Context:\n{context}\n\nQ: {question}"},
71
+ ]
72
+ )
73
+ return resp.choices[0].message.content
74
+ # No run ID, no intermediate state, cannot replay or debug
75
+ ```
76
+
77
+ When this breaks in production, you cannot inspect which chunks were retrieved, which reranking score caused a bad answer, or compare two versions of the pipeline.
78
+
79
+ ### Correct
80
+
81
+ Define the pipeline once as a versioned workflow, run it by ID:
82
+
83
+ ```typescript
84
+ // TypeScript — WorkflowBuilder: define once, run anywhere, fully observable
85
+ import { Schift, WorkflowBuilder } from '@schift-io/sdk';
86
+
87
+ const client = new Schift({ apiKey: 'sch_...' });
88
+
89
+ // Define the DAG — do this once during setup (e.g., in a migration script)
90
+ const graph = new WorkflowBuilder()
91
+ .addNode({
92
+ id: 'retrieve',
93
+ type: 'retrieve',
94
+ config: {
95
+ collectionId: 'col_docs_prod',
96
+ topK: 10,
97
+ scoreThreshold: 0.70,
98
+ taskType: 'qa',
99
+ },
100
+ })
101
+ .addNode({
102
+ id: 'rerank',
103
+ type: 'rerank',
104
+ config: {
105
+ model: 'schift-reranker-v1',
106
+ topN: 3, // keep only the top 3 after reranking
107
+ },
108
+ })
109
+ .addNode({
110
+ id: 'generate',
111
+ type: 'generate',
112
+ config: {
113
+ model: 'gpt-4o',
114
+ systemPrompt: 'Answer the question using only the provided context. If unsure, say so.',
115
+ maxTokens: 512,
116
+ },
117
+ })
118
+ .addEdge('retrieve', 'rerank') // retrieve → rerank
119
+ .addEdge('rerank', 'generate') // rerank → generate
120
+ .build();
121
+
122
+ // Register the workflow — returns a versioned workflow ID
123
+ const workflow = await client.workflow.create('docs-qa-v1', graph);
124
+ console.log(workflow.id); // wf_abc123 — store this in your config
125
+
126
+ // Run the workflow — every run gets a run ID
127
+ const run = await client.workflow.run(workflow.id, {
128
+ query: 'What are the latency benchmarks for the v2 model?',
129
+ });
130
+
131
+ console.log(run.runId); // run_xyz789 — for debugging
132
+ console.log(run.output.answer); // final generated answer
133
+ console.log(run.steps.retrieve.chunks); // intermediate: which chunks retrieved
134
+ console.log(run.steps.rerank.chunks); // intermediate: which chunks survived reranking
135
+ ```
136
+
137
+ ```python
138
+ # Python — same workflow pattern
139
+ from schift import Schift
140
+
141
+ client = Schift(api_key="sch_...")
142
+
143
+ # Build the graph as a dict (Python SDK uses graph spec format)
144
+ graph = {
145
+ "nodes": [
146
+ {
147
+ "id": "retrieve",
148
+ "type": "retrieve",
149
+ "config": {
150
+ "collection_id": "col_docs_prod",
151
+ "top_k": 10,
152
+ "score_threshold": 0.70,
153
+ "task_type": "qa",
154
+ },
155
+ },
156
+ {
157
+ "id": "rerank",
158
+ "type": "rerank",
159
+ "config": {"model": "schift-reranker-v1", "top_n": 3},
160
+ },
161
+ {
162
+ "id": "generate",
163
+ "type": "generate",
164
+ "config": {
165
+ "model": "gpt-4o",
166
+ "system_prompt": "Answer using only the provided context.",
167
+ "max_tokens": 512,
168
+ },
169
+ },
170
+ ],
171
+ "edges": [
172
+ {"from": "retrieve", "to": "rerank"},
173
+ {"from": "rerank", "to": "generate"},
174
+ ],
175
+ }
176
+
177
+ # Register once
178
+ workflow = client.workflow.create("docs-qa-v1", graph)
179
+
180
+ # Run — returns structured run result with full step history
181
+ run = client.workflow.run(workflow.id, {"query": "What are the latency benchmarks?"})
182
+
183
+ print(run.run_id) # for debugging
184
+ print(run.output["answer"]) # final answer
185
+ print(run.steps["retrieve"]) # which chunks were retrieved
186
+ print(run.steps["rerank"]) # which chunks survived reranking
187
+ ```
188
+
189
+ **Why WorkflowBuilder over inline code:**
190
+
191
+ | Concern | Inline Code | WorkflowBuilder |
192
+ |---------|-------------|-----------------|
193
+ | Versioning | Manual, error-prone | Built-in (v1, v2, ...) |
194
+ | Observability | None | Full run history with step-level traces |
195
+ | Testing | Full integration test required | Each node testable in isolation |
196
+ | Sharing | Copy-paste | Export/import workflow templates |
197
+ | A/B testing | Requires code branching | Run two workflow IDs, compare run metrics |
198
+ | Prompt updates | Redeploy required | Update workflow node config, no code change |
199
+
200
+ Store the workflow ID in your application config (environment variable or database). If you need to update the pipeline — change `topK`, swap the reranker, update the system prompt — create a new workflow version and update the config, with no application code changes.
201
+
202
+ ## Reference
203
+
204
+ - https://docs.schift.io/workflows/builder
205
+ - https://docs.schift.io/workflows/run-history
@@ -0,0 +1,103 @@
1
+ ---
2
+ title: SDK Async Patterns
3
+ impact: MEDIUM
4
+ impactDescription: Calling the synchronous Schift client inside an async web framework blocks the event loop, serializing all requests and eliminating the concurrency benefit of async frameworks like FastAPI or Express.
5
+ tags: [sdk, async, fastapi, performance, python, typescript]
6
+ ---
7
+
8
+ ## Use the Async Client in Async Applications
9
+
10
+ The Schift Python SDK ships two client classes: `Schift` (synchronous, thread-blocking) and `AsyncSchift` (async/await, non-blocking). The TypeScript SDK is async-only and always returns Promises.
11
+
12
+ Using the synchronous Python client inside an `async def` handler does not raise an error but blocks the entire event loop for the duration of the network call. Under load, this effectively single-threads your server.
13
+
14
+ ### Incorrect
15
+
16
+ ```python
17
+ # Python - synchronous client blocks the event loop inside an async handler
18
+ import os
19
+ from fastapi import FastAPI
20
+ from schift import Schift # synchronous client
21
+
22
+ app = FastAPI()
23
+ client = Schift() # uses SCHIFT_API_KEY env var
24
+
25
+ @app.get("/search")
26
+ async def search(q: str, bucket_id: str):
27
+ # client.bucket.search is a blocking I/O call
28
+ # This stalls the event loop while waiting for the HTTP response
29
+ results = client.bucket.search(bucket_id, q)
30
+ return results
31
+ ```
32
+
33
+ ```typescript
34
+ // TypeScript - no sync client exists, but forgetting await is a common mistake
35
+ import { Schift } from '@schift-io/sdk';
36
+
37
+ const client = new Schift();
38
+
39
+ app.get('/search', (req, res) => {
40
+ // Missing await - returns a Promise, not the actual results
41
+ const results = client.bucket.search(req.query.bucketId, req.query.q);
42
+ res.json(results); // serializes the Promise object, not the data
43
+ });
44
+ ```
45
+
46
+ ### Correct
47
+
48
+ ```python
49
+ # Python - use AsyncSchift with async def handlers
50
+ import os
51
+ from fastapi import FastAPI
52
+ from schift import AsyncSchift # async client
53
+
54
+ app = FastAPI()
55
+ client = AsyncSchift() # reads SCHIFT_API_KEY automatically
56
+
57
+ @app.get("/search")
58
+ async def search(q: str, bucket_id: str):
59
+ results = await client.bucket.search(bucket_id, q)
60
+ return results
61
+
62
+ # Parallel calls: use asyncio.gather to fan out multiple searches concurrently
63
+ import asyncio
64
+
65
+ @app.get("/multi-search")
66
+ async def multi_search(q: str, bucket_ids: list[str]):
67
+ tasks = [client.bucket.search(bid, q) for bid in bucket_ids]
68
+ results = await asyncio.gather(*tasks)
69
+ return results
70
+ ```
71
+
72
+ ```typescript
73
+ // TypeScript - always await; use Promise.all for concurrent calls
74
+ import { Schift } from '@schift-io/sdk';
75
+ import express from 'express';
76
+
77
+ const app = express();
78
+ const client = new Schift(); // reads SCHIFT_API_KEY automatically
79
+
80
+ app.get('/search', async (req, res) => {
81
+ const results = await client.bucket.search(
82
+ req.query.bucketId as string,
83
+ req.query.q as string
84
+ );
85
+ res.json(results);
86
+ });
87
+
88
+ // Parallel calls
89
+ app.get('/multi-search', async (req, res) => {
90
+ const bucketIds = (req.query.bucketIds as string).split(',');
91
+ const results = await Promise.all(
92
+ bucketIds.map(id => client.bucket.search(id, req.query.q as string))
93
+ );
94
+ res.json(results);
95
+ });
96
+ ```
97
+
98
+ For background jobs or scripts where blocking is acceptable, the synchronous `Schift` client is fine and has simpler usage. Reserve `AsyncSchift` for code running inside an event loop.
99
+
100
+ ## Reference
101
+
102
+ - https://docs.schift.io/sdk/python#async
103
+ - https://docs.schift.io/sdk/typescript
@@ -0,0 +1,76 @@
1
+ ---
2
+ title: SDK Authentication Patterns
3
+ impact: CRITICAL
4
+ impactDescription: Hardcoded API keys in source code are the most common cause of credential leaks. A leaked `sch_` key can drain your quota and expose your data before you notice.
5
+ tags: [sdk, auth, security, api-key]
6
+ ---
7
+
8
+ ## Never Hardcode API Keys
9
+
10
+ API keys must never appear as string literals in source code. Keys committed to version control or embedded in build artifacts are exposed to anyone with repository access, container image access, or build log access. Rotate immediately if a key is ever exposed.
11
+
12
+ Schift API keys follow the format `sch_` followed by a random string. Treat them like passwords.
13
+
14
+ ### Incorrect
15
+
16
+ ```python
17
+ # Python - key is visible in source and will leak via git history
18
+ from schift import Schift
19
+
20
+ client = Schift(api_key="sch_abc123xyz789...") # NEVER do this
21
+ ```
22
+
23
+ ```typescript
24
+ // TypeScript - same problem; bundlers may embed this in client-side code
25
+ import { Schift } from '@schift-io/sdk';
26
+
27
+ const client = new Schift({ apiKey: 'sch_abc123xyz789...' }); // NEVER do this
28
+ ```
29
+
30
+ Both examples embed a literal key that will appear in `git log`, CI logs, Docker image layers, and any bundle output.
31
+
32
+ ### Correct
33
+
34
+ ```python
35
+ # Python - read from environment variable explicitly
36
+ import os
37
+ from schift import Schift
38
+
39
+ client = Schift(api_key=os.environ["SCHIFT_API_KEY"])
40
+
41
+ # Or rely on the SDK's automatic env-var lookup (recommended for simplicity)
42
+ # The SDK reads SCHIFT_API_KEY automatically when api_key is omitted
43
+ client = Schift()
44
+ ```
45
+
46
+ ```typescript
47
+ // TypeScript - read from process.env
48
+ import { Schift } from '@schift-io/sdk';
49
+
50
+ const client = new Schift({ apiKey: process.env.SCHIFT_API_KEY });
51
+
52
+ // Or rely on SDK auto-detection (reads SCHIFT_API_KEY if apiKey is omitted)
53
+ const client = new Schift();
54
+ ```
55
+
56
+ Set the key in your environment:
57
+
58
+ ```bash
59
+ # Local development (.envrc, never committed)
60
+ export SCHIFT_API_KEY=sch_dev_...
61
+
62
+ # CI/CD - set as a masked secret variable in your platform
63
+ # Docker / Kubernetes - inject via --env-file or a Secret resource
64
+ ```
65
+
66
+ Additional best practices:
67
+ - Use **project-scoped keys** (not org-wide admin keys) so a leaked key has minimal blast radius
68
+ - Create **separate keys** for `dev`, `staging`, and `prod` environments
69
+ - **Rotate keys regularly** from the Schift dashboard; old keys can be revoked without downtime
70
+ - Add `SCHIFT_API_KEY` to `.gitignore`-adjacent `.envrc` or use a secrets manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault)
71
+
72
+ ## Reference
73
+
74
+ - https://docs.schift.io/authentication
75
+ - https://docs.schift.io/sdk/python#authentication
76
+ - https://docs.schift.io/sdk/typescript#authentication