struai 0.1.0__tar.gz → 1.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- struai-1.0.1/.github/workflows/release.yml +139 -0
- struai-1.0.1/.pre-commit-config.yaml +13 -0
- struai-1.0.1/CHANGELOG.md +15 -0
- {struai-0.1.0 → struai-1.0.1}/PKG-INFO +4 -1
- {struai-0.1.0 → struai-1.0.1}/README.md +3 -0
- {struai-0.1.0 → struai-1.0.1}/js/README.md +7 -0
- {struai-0.1.0 → struai-1.0.1}/js/package.json +1 -1
- {struai-0.1.0 → struai-1.0.1}/js/src/index.ts +130 -2
- {struai-0.1.0 → struai-1.0.1}/pyproject.toml +13 -1
- {struai-0.1.0 → struai-1.0.1}/src/struai/__init__.py +29 -25
- {struai-0.1.0 → struai-1.0.1}/src/struai/_base.py +17 -3
- {struai-0.1.0 → struai-1.0.1}/src/struai/_client.py +4 -2
- {struai-0.1.0 → struai-1.0.1}/src/struai/_version.py +1 -1
- {struai-0.1.0 → struai-1.0.1}/src/struai/models/__init__.py +3 -1
- {struai-0.1.0 → struai-1.0.1}/src/struai/models/common.py +1 -1
- struai-1.0.1/src/struai/models/entities.py +62 -0
- {struai-0.1.0 → struai-1.0.1}/src/struai/resources/projects.py +5 -5
- struai-0.1.0/.github/workflows/publish-npm.yml +0 -68
- struai-0.1.0/.github/workflows/publish-python.yml +0 -54
- struai-0.1.0/src/struai/models/entities.py +0 -37
- {struai-0.1.0 → struai-1.0.1}/.github/workflows/ci.yml +0 -0
- {struai-0.1.0 → struai-1.0.1}/.gitignore +0 -0
- {struai-0.1.0 → struai-1.0.1}/js/.gitignore +0 -0
- {struai-0.1.0 → struai-1.0.1}/js/tsconfig.json +0 -0
- {struai-0.1.0 → struai-1.0.1}/src/struai/_exceptions.py +0 -0
- {struai-0.1.0 → struai-1.0.1}/src/struai/models/drawings.py +0 -0
- {struai-0.1.0 → struai-1.0.1}/src/struai/models/projects.py +0 -0
- {struai-0.1.0 → struai-1.0.1}/src/struai/models/search.py +0 -0
- {struai-0.1.0 → struai-1.0.1}/src/struai/py.typed +0 -0
- {struai-0.1.0 → struai-1.0.1}/src/struai/resources/__init__.py +0 -0
- {struai-0.1.0 → struai-1.0.1}/src/struai/resources/drawings.py +0 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
publish_only:
|
|
7
|
+
description: 'Just publish current version (skip version bump)'
|
|
8
|
+
required: false
|
|
9
|
+
default: 'false'
|
|
10
|
+
type: choice
|
|
11
|
+
options:
|
|
12
|
+
- 'false'
|
|
13
|
+
- 'true'
|
|
14
|
+
push:
|
|
15
|
+
branches: [main]
|
|
16
|
+
|
|
17
|
+
permissions:
|
|
18
|
+
contents: write
|
|
19
|
+
id-token: write
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
release:
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
concurrency: release
|
|
25
|
+
if: github.repository_owner == 'bhoshaga' && inputs.publish_only != 'true'
|
|
26
|
+
outputs:
|
|
27
|
+
released: ${{ steps.version.outputs.release }}
|
|
28
|
+
version: ${{ steps.version.outputs.version }}
|
|
29
|
+
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v4
|
|
32
|
+
with:
|
|
33
|
+
fetch-depth: 0
|
|
34
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
35
|
+
|
|
36
|
+
- name: Set up Python
|
|
37
|
+
uses: actions/setup-python@v5
|
|
38
|
+
with:
|
|
39
|
+
python-version: '3.11'
|
|
40
|
+
|
|
41
|
+
- name: Set up Node.js
|
|
42
|
+
uses: actions/setup-node@v4
|
|
43
|
+
with:
|
|
44
|
+
node-version: '20'
|
|
45
|
+
|
|
46
|
+
- name: Install python-semantic-release
|
|
47
|
+
run: pip install python-semantic-release
|
|
48
|
+
|
|
49
|
+
- name: Get next version
|
|
50
|
+
id: version
|
|
51
|
+
run: |
|
|
52
|
+
# Get the next version without making changes
|
|
53
|
+
NEXT_VERSION=$(semantic-release version --print 2>/dev/null || echo "")
|
|
54
|
+
if [ -z "$NEXT_VERSION" ]; then
|
|
55
|
+
echo "No release needed"
|
|
56
|
+
echo "release=false" >> $GITHUB_OUTPUT
|
|
57
|
+
else
|
|
58
|
+
echo "Next version: $NEXT_VERSION"
|
|
59
|
+
echo "release=true" >> $GITHUB_OUTPUT
|
|
60
|
+
echo "version=$NEXT_VERSION" >> $GITHUB_OUTPUT
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
- name: Update npm package version
|
|
64
|
+
if: steps.version.outputs.release == 'true'
|
|
65
|
+
run: |
|
|
66
|
+
cd js
|
|
67
|
+
npm version ${{ steps.version.outputs.version }} --no-git-tag-version
|
|
68
|
+
|
|
69
|
+
- name: Python Semantic Release
|
|
70
|
+
if: steps.version.outputs.release == 'true'
|
|
71
|
+
env:
|
|
72
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
73
|
+
run: |
|
|
74
|
+
git config user.name "github-actions[bot]"
|
|
75
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
76
|
+
|
|
77
|
+
# Add the npm package.json change
|
|
78
|
+
git add js/package.json
|
|
79
|
+
|
|
80
|
+
# Run semantic-release (will update Python version, commit, tag, and create release)
|
|
81
|
+
semantic-release version
|
|
82
|
+
|
|
83
|
+
# Push changes and tags
|
|
84
|
+
git push origin main --follow-tags
|
|
85
|
+
|
|
86
|
+
publish-python:
|
|
87
|
+
needs: release
|
|
88
|
+
if: always() && (needs.release.outputs.released == 'true' || inputs.publish_only == 'true')
|
|
89
|
+
runs-on: ubuntu-latest
|
|
90
|
+
environment: pypi
|
|
91
|
+
permissions:
|
|
92
|
+
id-token: write
|
|
93
|
+
|
|
94
|
+
steps:
|
|
95
|
+
- uses: actions/checkout@v4
|
|
96
|
+
|
|
97
|
+
- name: Set up Python
|
|
98
|
+
uses: actions/setup-python@v5
|
|
99
|
+
with:
|
|
100
|
+
python-version: '3.11'
|
|
101
|
+
|
|
102
|
+
- name: Install build tools
|
|
103
|
+
run: pip install build
|
|
104
|
+
|
|
105
|
+
- name: Build package
|
|
106
|
+
run: python -m build
|
|
107
|
+
|
|
108
|
+
- name: Publish to PyPI
|
|
109
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
110
|
+
with:
|
|
111
|
+
skip-existing: true
|
|
112
|
+
|
|
113
|
+
publish-npm:
|
|
114
|
+
needs: release
|
|
115
|
+
if: always() && (needs.release.outputs.released == 'true' || inputs.publish_only == 'true')
|
|
116
|
+
runs-on: ubuntu-latest
|
|
117
|
+
defaults:
|
|
118
|
+
run:
|
|
119
|
+
working-directory: js
|
|
120
|
+
|
|
121
|
+
steps:
|
|
122
|
+
- uses: actions/checkout@v4
|
|
123
|
+
|
|
124
|
+
- name: Set up Node.js
|
|
125
|
+
uses: actions/setup-node@v4
|
|
126
|
+
with:
|
|
127
|
+
node-version: '20'
|
|
128
|
+
registry-url: 'https://registry.npmjs.org'
|
|
129
|
+
|
|
130
|
+
- name: Install dependencies
|
|
131
|
+
run: npm install
|
|
132
|
+
|
|
133
|
+
- name: Build
|
|
134
|
+
run: npm run build
|
|
135
|
+
|
|
136
|
+
- name: Publish to npm
|
|
137
|
+
run: npm publish --access public
|
|
138
|
+
env:
|
|
139
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
3
|
+
rev: v0.9.4
|
|
4
|
+
hooks:
|
|
5
|
+
- id: ruff
|
|
6
|
+
args: [--fix]
|
|
7
|
+
- id: ruff-format
|
|
8
|
+
|
|
9
|
+
- repo: https://github.com/compilerla/conventional-pre-commit
|
|
10
|
+
rev: v3.6.0
|
|
11
|
+
hooks:
|
|
12
|
+
- id: conventional-pre-commit
|
|
13
|
+
stages: [commit-msg]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# CHANGELOG
|
|
2
|
+
|
|
3
|
+
<!-- version list -->
|
|
4
|
+
|
|
5
|
+
## v1.0.1 (2026-02-04)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- **ci**: Include publish jobs directly in release workflow
|
|
10
|
+
([`02436eb`](https://github.com/bhoshaga/struai/commit/02436eb55c63cbaa54eac8ca456a54d30125fada))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## v1.0.0 (2026-02-04)
|
|
14
|
+
|
|
15
|
+
- Initial Release
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: struai
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: StruAI Drawing Analysis SDK - AI-powered construction drawing analysis
|
|
5
5
|
Project-URL: Homepage, https://struai.com
|
|
6
6
|
Project-URL: Documentation, https://docs.struai.com/python
|
|
@@ -46,6 +46,9 @@ pip install struai
|
|
|
46
46
|
from struai import StruAI
|
|
47
47
|
|
|
48
48
|
client = StruAI(api_key="sk-xxx") # or set STRUAI_API_KEY env var
|
|
49
|
+
|
|
50
|
+
# Optional: override base URL (http://localhost:8000 or http://localhost:8000/v1)
|
|
51
|
+
client = StruAI(api_key="sk-xxx", base_url="http://localhost:8000")
|
|
49
52
|
```
|
|
50
53
|
|
|
51
54
|
## Tier 1: Raw Detection ($0.02/page)
|
|
@@ -14,6 +14,9 @@ pip install struai
|
|
|
14
14
|
from struai import StruAI
|
|
15
15
|
|
|
16
16
|
client = StruAI(api_key="sk-xxx") # or set STRUAI_API_KEY env var
|
|
17
|
+
|
|
18
|
+
# Optional: override base URL (http://localhost:8000 or http://localhost:8000/v1)
|
|
19
|
+
client = StruAI(api_key="sk-xxx", base_url="http://localhost:8000")
|
|
17
20
|
```
|
|
18
21
|
|
|
19
22
|
## Tier 1: Raw Detection ($0.02/page)
|
|
@@ -16,6 +16,9 @@ npm install struai
|
|
|
16
16
|
import { StruAI } from 'struai';
|
|
17
17
|
|
|
18
18
|
const client = new StruAI({ apiKey: 'sk-xxx' });
|
|
19
|
+
|
|
20
|
+
// Optional: override base URL (http://localhost:8000 or http://localhost:8000/v1)
|
|
21
|
+
const local = new StruAI({ apiKey: 'sk-xxx', baseUrl: 'http://localhost:8000' });
|
|
19
22
|
```
|
|
20
23
|
|
|
21
24
|
## Tier 1: Raw Detection ($0.02/page)
|
|
@@ -59,6 +62,10 @@ for (const hit of results.results) {
|
|
|
59
62
|
// Natural language query
|
|
60
63
|
const answer = await project.query('What beams connect to column C3?');
|
|
61
64
|
console.log(answer.answer);
|
|
65
|
+
|
|
66
|
+
// Browse entities
|
|
67
|
+
const entities = await project.entities.list({ type: 'Component', limit: 50 });
|
|
68
|
+
const entity = await project.entities.get('ent_abc123');
|
|
62
69
|
```
|
|
63
70
|
|
|
64
71
|
## License
|
|
@@ -163,7 +163,71 @@ export interface QueryResponse {
|
|
|
163
163
|
confidence: number;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
|
|
166
|
+
export interface EntityLocation {
|
|
167
|
+
sheet_id: string;
|
|
168
|
+
sheet_title?: string;
|
|
169
|
+
page?: number | null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface EntityRelation {
|
|
173
|
+
uuid: string;
|
|
174
|
+
type: string;
|
|
175
|
+
fact: string;
|
|
176
|
+
source_id?: string;
|
|
177
|
+
source_label?: string;
|
|
178
|
+
target_id?: string;
|
|
179
|
+
target_label?: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface Fact {
|
|
183
|
+
id: string;
|
|
184
|
+
type: string;
|
|
185
|
+
fact: string;
|
|
186
|
+
source_id: string;
|
|
187
|
+
target_id: string;
|
|
188
|
+
source_label?: string;
|
|
189
|
+
target_label?: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface EntityListItem {
|
|
193
|
+
id: string;
|
|
194
|
+
type: string;
|
|
195
|
+
label: string;
|
|
196
|
+
description?: string;
|
|
197
|
+
sheet_id?: string;
|
|
198
|
+
bbox?: [number, number, number, number];
|
|
199
|
+
attributes?: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface Entity {
|
|
203
|
+
id: string;
|
|
204
|
+
type: string;
|
|
205
|
+
label: string;
|
|
206
|
+
description?: string;
|
|
207
|
+
outgoing: EntityRelation[];
|
|
208
|
+
incoming: EntityRelation[];
|
|
209
|
+
locations: EntityLocation[];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const DEFAULT_BASE_URL = 'https://api.stru.ai';
|
|
213
|
+
|
|
214
|
+
function normalizeBaseUrl(raw: string): string {
|
|
215
|
+
const trimmed = raw.replace(/\/$/, '');
|
|
216
|
+
try {
|
|
217
|
+
const parsed = new URL(trimmed);
|
|
218
|
+
const path = parsed.pathname.replace(/\/$/, '');
|
|
219
|
+
if (path === '' || path === '/') {
|
|
220
|
+
parsed.pathname = '/v1';
|
|
221
|
+
return parsed.toString().replace(/\/$/, '');
|
|
222
|
+
}
|
|
223
|
+
return trimmed;
|
|
224
|
+
} catch {
|
|
225
|
+
if (trimmed.endsWith('/v1')) {
|
|
226
|
+
return trimmed;
|
|
227
|
+
}
|
|
228
|
+
return `${trimmed}/v1`;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
167
231
|
|
|
168
232
|
class APIError extends Error {
|
|
169
233
|
constructor(
|
|
@@ -284,14 +348,78 @@ class Sheets {
|
|
|
284
348
|
}
|
|
285
349
|
}
|
|
286
350
|
|
|
351
|
+
class Entities {
|
|
352
|
+
constructor(
|
|
353
|
+
private client: StruAI,
|
|
354
|
+
private projectId: string
|
|
355
|
+
) {}
|
|
356
|
+
|
|
357
|
+
async list(options?: {
|
|
358
|
+
sheetId?: string;
|
|
359
|
+
type?: string;
|
|
360
|
+
limit?: number;
|
|
361
|
+
}): Promise<EntityListItem[]> {
|
|
362
|
+
const params = new URLSearchParams();
|
|
363
|
+
if (options?.limit) params.set('limit', options.limit.toString());
|
|
364
|
+
if (options?.sheetId) params.set('sheet_id', options.sheetId);
|
|
365
|
+
if (options?.type) params.set('type', options.type);
|
|
366
|
+
const query = params.toString();
|
|
367
|
+
|
|
368
|
+
const response = await this.client.request<{ entities: EntityListItem[] }>(
|
|
369
|
+
query
|
|
370
|
+
? `/projects/${this.projectId}/entities?${query}`
|
|
371
|
+
: `/projects/${this.projectId}/entities`
|
|
372
|
+
);
|
|
373
|
+
return response.entities;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async get(entityId: string): Promise<Entity> {
|
|
377
|
+
return this.client.request<Entity>(
|
|
378
|
+
`/projects/${this.projectId}/entities/${entityId}`
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
class Relationships {
|
|
384
|
+
constructor(
|
|
385
|
+
private client: StruAI,
|
|
386
|
+
private projectId: string
|
|
387
|
+
) {}
|
|
388
|
+
|
|
389
|
+
async list(options?: {
|
|
390
|
+
sourceId?: string;
|
|
391
|
+
targetId?: string;
|
|
392
|
+
type?: string;
|
|
393
|
+
limit?: number;
|
|
394
|
+
}): Promise<Fact[]> {
|
|
395
|
+
const params = new URLSearchParams();
|
|
396
|
+
if (options?.limit) params.set('limit', options.limit.toString());
|
|
397
|
+
if (options?.sourceId) params.set('source_id', options.sourceId);
|
|
398
|
+
if (options?.targetId) params.set('target_id', options.targetId);
|
|
399
|
+
if (options?.type) params.set('type', options.type);
|
|
400
|
+
const query = params.toString();
|
|
401
|
+
|
|
402
|
+
const response = await this.client.request<{ relationships: Fact[] }>(
|
|
403
|
+
query
|
|
404
|
+
? `/projects/${this.projectId}/relationships?${query}`
|
|
405
|
+
: `/projects/${this.projectId}/relationships`
|
|
406
|
+
);
|
|
407
|
+
return response.relationships;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
287
411
|
class ProjectInstance {
|
|
288
412
|
public readonly sheets: Sheets;
|
|
413
|
+
public readonly entities: Entities;
|
|
414
|
+
public readonly relationships: Relationships;
|
|
289
415
|
|
|
290
416
|
constructor(
|
|
291
417
|
private client: StruAI,
|
|
292
418
|
private project: Project
|
|
293
419
|
) {
|
|
294
420
|
this.sheets = new Sheets(client, project.id);
|
|
421
|
+
this.entities = new Entities(client, project.id);
|
|
422
|
+
this.relationships = new Relationships(client, project.id);
|
|
295
423
|
}
|
|
296
424
|
|
|
297
425
|
get id() {
|
|
@@ -378,7 +506,7 @@ export class StruAI {
|
|
|
378
506
|
|
|
379
507
|
constructor(options: StruAIOptions) {
|
|
380
508
|
this.apiKey = options.apiKey;
|
|
381
|
-
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL)
|
|
509
|
+
this.baseUrl = normalizeBaseUrl(options.baseUrl ?? DEFAULT_BASE_URL);
|
|
382
510
|
this.timeout = options.timeout ?? 60000;
|
|
383
511
|
|
|
384
512
|
this.drawings = new Drawings(this);
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "struai"
|
|
7
|
-
version = "0.1
|
|
7
|
+
version = "1.0.1"
|
|
8
8
|
description = "StruAI Drawing Analysis SDK - AI-powered construction drawing analysis"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -72,3 +72,15 @@ warn_unused_ignores = true
|
|
|
72
72
|
|
|
73
73
|
[tool.pytest.ini_options]
|
|
74
74
|
asyncio_mode = "auto"
|
|
75
|
+
|
|
76
|
+
[tool.semantic_release]
|
|
77
|
+
version_toml = ["pyproject.toml:project.version"]
|
|
78
|
+
version_variables = ["src/struai/_version.py:__version__"]
|
|
79
|
+
branch = "main"
|
|
80
|
+
build_command = "pip install build && python -m build"
|
|
81
|
+
commit_message = "chore(release): {version}\n\nAutomatically generated by python-semantic-release"
|
|
82
|
+
|
|
83
|
+
[tool.semantic_release.commit_parser_options]
|
|
84
|
+
allowed_tags = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "refactor", "style", "test"]
|
|
85
|
+
minor_tags = ["feat"]
|
|
86
|
+
patch_tags = ["fix", "perf"]
|
|
@@ -17,50 +17,52 @@ Example:
|
|
|
17
17
|
>>> results = project.search("W12x26 beam connections")
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
-
from .
|
|
21
|
-
from ._client import StruAI, AsyncStruAI
|
|
20
|
+
from ._client import AsyncStruAI, StruAI
|
|
22
21
|
from ._exceptions import (
|
|
23
|
-
StruAIError,
|
|
24
22
|
APIError,
|
|
25
23
|
AuthenticationError,
|
|
26
|
-
|
|
24
|
+
ConnectionError,
|
|
25
|
+
InternalServerError,
|
|
26
|
+
JobFailedError,
|
|
27
27
|
NotFoundError,
|
|
28
|
-
|
|
28
|
+
PermissionDeniedError,
|
|
29
29
|
RateLimitError,
|
|
30
|
-
|
|
30
|
+
StruAIError,
|
|
31
31
|
TimeoutError,
|
|
32
|
-
|
|
33
|
-
JobFailedError,
|
|
32
|
+
ValidationError,
|
|
34
33
|
)
|
|
34
|
+
from ._version import __version__
|
|
35
35
|
|
|
36
36
|
# Re-export commonly used models
|
|
37
37
|
from .models import (
|
|
38
|
-
|
|
39
|
-
Point,
|
|
38
|
+
Annotations,
|
|
40
39
|
BBox,
|
|
41
|
-
|
|
40
|
+
DetailTag,
|
|
42
41
|
Dimensions,
|
|
43
42
|
# Tier 1 - Drawings
|
|
44
43
|
DrawingResult,
|
|
45
|
-
|
|
44
|
+
# Entities
|
|
45
|
+
Entity,
|
|
46
|
+
EntityListItem,
|
|
47
|
+
EntityRelation,
|
|
48
|
+
Fact,
|
|
49
|
+
JobStatus,
|
|
46
50
|
Leader,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
RevisionTriangle,
|
|
50
|
-
RevisionCloud,
|
|
51
|
-
TitleBlock,
|
|
51
|
+
# Common
|
|
52
|
+
Point,
|
|
52
53
|
# Tier 2 - Projects
|
|
53
54
|
Project,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
QueryResponse,
|
|
56
|
+
RevisionCloud,
|
|
57
|
+
RevisionTriangle,
|
|
58
|
+
SearchHit,
|
|
57
59
|
# Search
|
|
58
60
|
SearchResponse,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
SectionTag,
|
|
62
|
+
Sheet,
|
|
63
|
+
SheetResult,
|
|
64
|
+
TextSpan,
|
|
65
|
+
TitleBlock,
|
|
64
66
|
)
|
|
65
67
|
|
|
66
68
|
__all__ = [
|
|
@@ -104,5 +106,7 @@ __all__ = [
|
|
|
104
106
|
"SearchHit",
|
|
105
107
|
"QueryResponse",
|
|
106
108
|
"Entity",
|
|
109
|
+
"EntityListItem",
|
|
110
|
+
"EntityRelation",
|
|
107
111
|
"Fact",
|
|
108
112
|
]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Base HTTP client with retry logic."""
|
|
2
2
|
import time
|
|
3
3
|
from typing import Any, Dict, Optional, Type, TypeVar, Union
|
|
4
|
+
from urllib.parse import urlparse
|
|
4
5
|
|
|
5
6
|
import httpx
|
|
6
7
|
from pydantic import BaseModel
|
|
@@ -20,11 +21,24 @@ from ._version import __version__
|
|
|
20
21
|
|
|
21
22
|
T = TypeVar("T", bound=BaseModel)
|
|
22
23
|
|
|
23
|
-
DEFAULT_BASE_URL = "https://api.stru.ai
|
|
24
|
+
DEFAULT_BASE_URL = "https://api.stru.ai"
|
|
24
25
|
DEFAULT_TIMEOUT = 60.0
|
|
25
26
|
DEFAULT_MAX_RETRIES = 2
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
def _normalize_base_url(base_url: str) -> str:
|
|
30
|
+
trimmed = base_url.rstrip("/")
|
|
31
|
+
parsed = urlparse(trimmed)
|
|
32
|
+
if parsed.scheme and parsed.netloc:
|
|
33
|
+
path = parsed.path.rstrip("/")
|
|
34
|
+
if path in ("", "/"):
|
|
35
|
+
return f"{trimmed}/v1"
|
|
36
|
+
return trimmed
|
|
37
|
+
if trimmed.endswith("/v1"):
|
|
38
|
+
return trimmed
|
|
39
|
+
return f"{trimmed}/v1"
|
|
40
|
+
|
|
41
|
+
|
|
28
42
|
class BaseClient:
|
|
29
43
|
"""Base HTTP client with retry logic."""
|
|
30
44
|
|
|
@@ -36,7 +50,7 @@ class BaseClient:
|
|
|
36
50
|
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
37
51
|
):
|
|
38
52
|
self.api_key = api_key
|
|
39
|
-
self.base_url = base_url
|
|
53
|
+
self.base_url = _normalize_base_url(base_url)
|
|
40
54
|
self.timeout = timeout
|
|
41
55
|
self.max_retries = max_retries
|
|
42
56
|
self._client: Optional[httpx.Client] = None
|
|
@@ -195,7 +209,7 @@ class AsyncBaseClient:
|
|
|
195
209
|
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
196
210
|
):
|
|
197
211
|
self.api_key = api_key
|
|
198
|
-
self.base_url = base_url
|
|
212
|
+
self.base_url = _normalize_base_url(base_url)
|
|
199
213
|
self.timeout = timeout
|
|
200
214
|
self.max_retries = max_retries
|
|
201
215
|
self._client: Optional[httpx.AsyncClient] = None
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""Main StruAI client classes."""
|
|
2
|
+
|
|
2
3
|
import os
|
|
3
4
|
from functools import cached_property
|
|
4
5
|
from typing import Optional
|
|
5
6
|
|
|
6
|
-
from ._base import
|
|
7
|
+
from ._base import DEFAULT_BASE_URL, DEFAULT_TIMEOUT, AsyncBaseClient, BaseClient
|
|
7
8
|
from ._exceptions import StruAIError
|
|
8
9
|
from .resources.drawings import AsyncDrawings, Drawings
|
|
9
10
|
from .resources.projects import AsyncProjects, Projects
|
|
@@ -14,7 +15,8 @@ class StruAI(BaseClient):
|
|
|
14
15
|
|
|
15
16
|
Args:
|
|
16
17
|
api_key: Your API key. Falls back to STRUAI_API_KEY env var.
|
|
17
|
-
base_url: API base URL. Defaults to
|
|
18
|
+
base_url: API base URL. Defaults to https://api.stru.ai.
|
|
19
|
+
If no path is provided, /v1 is appended automatically.
|
|
18
20
|
timeout: Request timeout in seconds. Default 60.
|
|
19
21
|
max_retries: Max retry attempts for failed requests. Default 2.
|
|
20
22
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""Version information."""
|
|
2
|
-
__version__ = "0.1
|
|
2
|
+
__version__ = "1.0.1"
|
|
@@ -10,7 +10,7 @@ from .drawings import (
|
|
|
10
10
|
SectionTag,
|
|
11
11
|
TitleBlock,
|
|
12
12
|
)
|
|
13
|
-
from .entities import Entity, EntityLocation, Fact
|
|
13
|
+
from .entities import Entity, EntityListItem, EntityLocation, EntityRelation, Fact
|
|
14
14
|
from .projects import JobStatus, JobStep, Project, Sheet, SheetResult
|
|
15
15
|
from .search import (
|
|
16
16
|
EntitySummary,
|
|
@@ -55,6 +55,8 @@ __all__ = [
|
|
|
55
55
|
"QuerySource",
|
|
56
56
|
# Entities
|
|
57
57
|
"Entity",
|
|
58
|
+
"EntityListItem",
|
|
58
59
|
"EntityLocation",
|
|
60
|
+
"EntityRelation",
|
|
59
61
|
"Fact",
|
|
60
62
|
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Entity and Relationship models."""
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from .common import BBox
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EntityLocation(BaseModel):
|
|
10
|
+
"""Where an entity appears."""
|
|
11
|
+
|
|
12
|
+
sheet_id: str
|
|
13
|
+
sheet_title: Optional[str] = None
|
|
14
|
+
page: Optional[int] = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EntityRelation(BaseModel):
|
|
18
|
+
"""Relationship entry on an entity detail response."""
|
|
19
|
+
|
|
20
|
+
uuid: str
|
|
21
|
+
type: str
|
|
22
|
+
fact: str
|
|
23
|
+
source_id: Optional[str] = None
|
|
24
|
+
source_label: Optional[str] = None
|
|
25
|
+
target_id: Optional[str] = None
|
|
26
|
+
target_label: Optional[str] = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Fact(BaseModel):
|
|
30
|
+
"""Relationship between entities (list endpoint)."""
|
|
31
|
+
|
|
32
|
+
id: str
|
|
33
|
+
type: str
|
|
34
|
+
fact: str
|
|
35
|
+
source_id: str
|
|
36
|
+
target_id: str
|
|
37
|
+
source_label: Optional[str] = None
|
|
38
|
+
target_label: Optional[str] = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class EntityListItem(BaseModel):
|
|
42
|
+
"""Entity summary from list endpoint."""
|
|
43
|
+
|
|
44
|
+
id: str
|
|
45
|
+
type: str
|
|
46
|
+
label: str
|
|
47
|
+
description: Optional[str] = None
|
|
48
|
+
sheet_id: Optional[str] = None
|
|
49
|
+
bbox: Optional[BBox] = None
|
|
50
|
+
attributes: Optional[str] = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Entity(BaseModel):
|
|
54
|
+
"""Full entity with relationships."""
|
|
55
|
+
|
|
56
|
+
id: str
|
|
57
|
+
type: str
|
|
58
|
+
label: str
|
|
59
|
+
description: Optional[str] = None
|
|
60
|
+
outgoing: List[EntityRelation] = []
|
|
61
|
+
incoming: List[EntityRelation] = []
|
|
62
|
+
locations: List[EntityLocation] = []
|
|
@@ -6,7 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
from typing import TYPE_CHECKING, BinaryIO, Dict, List, Optional, Union
|
|
7
7
|
|
|
8
8
|
from .._exceptions import JobFailedError, TimeoutError
|
|
9
|
-
from ..models.entities import Entity, Fact
|
|
9
|
+
from ..models.entities import Entity, EntityListItem, Fact
|
|
10
10
|
from ..models.projects import JobStatus, Project, Sheet, SheetResult
|
|
11
11
|
from ..models.search import QueryResponse, SearchResponse
|
|
12
12
|
|
|
@@ -266,7 +266,7 @@ class Entities:
|
|
|
266
266
|
sheet_id: Optional[str] = None,
|
|
267
267
|
type: Optional[str] = None,
|
|
268
268
|
limit: int = 100,
|
|
269
|
-
) -> List[
|
|
269
|
+
) -> List[EntityListItem]:
|
|
270
270
|
"""List entities in project."""
|
|
271
271
|
params: Dict[str, Union[str, int]] = {"limit": limit}
|
|
272
272
|
if sheet_id:
|
|
@@ -278,7 +278,7 @@ class Entities:
|
|
|
278
278
|
f"/projects/{self._project_id}/entities",
|
|
279
279
|
params=params,
|
|
280
280
|
)
|
|
281
|
-
return [
|
|
281
|
+
return [EntityListItem.model_validate(e) for e in response["entities"]]
|
|
282
282
|
|
|
283
283
|
def get(self, entity_id: str) -> Entity:
|
|
284
284
|
"""Get entity with all relationships."""
|
|
@@ -300,7 +300,7 @@ class AsyncEntities:
|
|
|
300
300
|
sheet_id: Optional[str] = None,
|
|
301
301
|
type: Optional[str] = None,
|
|
302
302
|
limit: int = 100,
|
|
303
|
-
) -> List[
|
|
303
|
+
) -> List[EntityListItem]:
|
|
304
304
|
"""List entities in project."""
|
|
305
305
|
params: Dict[str, Union[str, int]] = {"limit": limit}
|
|
306
306
|
if sheet_id:
|
|
@@ -312,7 +312,7 @@ class AsyncEntities:
|
|
|
312
312
|
f"/projects/{self._project_id}/entities",
|
|
313
313
|
params=params,
|
|
314
314
|
)
|
|
315
|
-
return [
|
|
315
|
+
return [EntityListItem.model_validate(e) for e in response["entities"]]
|
|
316
316
|
|
|
317
317
|
async def get(self, entity_id: str) -> Entity:
|
|
318
318
|
"""Get entity with all relationships."""
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
name: Publish npm Package
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
workflow_dispatch:
|
|
5
|
-
push:
|
|
6
|
-
branches: [main]
|
|
7
|
-
paths:
|
|
8
|
-
- 'js/**'
|
|
9
|
-
- '.github/workflows/publish-npm.yml'
|
|
10
|
-
release:
|
|
11
|
-
types: [published]
|
|
12
|
-
|
|
13
|
-
jobs:
|
|
14
|
-
build:
|
|
15
|
-
runs-on: ubuntu-latest
|
|
16
|
-
defaults:
|
|
17
|
-
run:
|
|
18
|
-
working-directory: js
|
|
19
|
-
steps:
|
|
20
|
-
- uses: actions/checkout@v4
|
|
21
|
-
|
|
22
|
-
- name: Set up Node.js
|
|
23
|
-
uses: actions/setup-node@v4
|
|
24
|
-
with:
|
|
25
|
-
node-version: '20'
|
|
26
|
-
registry-url: 'https://registry.npmjs.org'
|
|
27
|
-
|
|
28
|
-
- name: Install dependencies
|
|
29
|
-
run: npm install
|
|
30
|
-
|
|
31
|
-
- name: Build
|
|
32
|
-
run: npm run build
|
|
33
|
-
|
|
34
|
-
- name: Upload artifacts
|
|
35
|
-
uses: actions/upload-artifact@v4
|
|
36
|
-
with:
|
|
37
|
-
name: npm-dist
|
|
38
|
-
path: js/dist/
|
|
39
|
-
|
|
40
|
-
publish:
|
|
41
|
-
needs: build
|
|
42
|
-
runs-on: ubuntu-latest
|
|
43
|
-
if: github.event_name == 'release' || github.ref == 'refs/heads/main'
|
|
44
|
-
defaults:
|
|
45
|
-
run:
|
|
46
|
-
working-directory: js
|
|
47
|
-
steps:
|
|
48
|
-
- uses: actions/checkout@v4
|
|
49
|
-
|
|
50
|
-
- name: Set up Node.js
|
|
51
|
-
uses: actions/setup-node@v4
|
|
52
|
-
with:
|
|
53
|
-
node-version: '20'
|
|
54
|
-
registry-url: 'https://registry.npmjs.org'
|
|
55
|
-
|
|
56
|
-
- name: Install dependencies
|
|
57
|
-
run: npm install
|
|
58
|
-
|
|
59
|
-
- name: Download artifacts
|
|
60
|
-
uses: actions/download-artifact@v4
|
|
61
|
-
with:
|
|
62
|
-
name: npm-dist
|
|
63
|
-
path: js/dist/
|
|
64
|
-
|
|
65
|
-
- name: Publish to npm
|
|
66
|
-
run: npm publish --access public
|
|
67
|
-
env:
|
|
68
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
name: Publish Python Package
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
workflow_dispatch:
|
|
5
|
-
push:
|
|
6
|
-
branches: [main]
|
|
7
|
-
paths:
|
|
8
|
-
- 'src/**'
|
|
9
|
-
- 'pyproject.toml'
|
|
10
|
-
- '.github/workflows/publish-python.yml'
|
|
11
|
-
release:
|
|
12
|
-
types: [published]
|
|
13
|
-
|
|
14
|
-
jobs:
|
|
15
|
-
build:
|
|
16
|
-
runs-on: ubuntu-latest
|
|
17
|
-
steps:
|
|
18
|
-
- uses: actions/checkout@v4
|
|
19
|
-
|
|
20
|
-
- name: Set up Python
|
|
21
|
-
uses: actions/setup-python@v5
|
|
22
|
-
with:
|
|
23
|
-
python-version: '3.11'
|
|
24
|
-
|
|
25
|
-
- name: Install build tools
|
|
26
|
-
run: pip install build twine
|
|
27
|
-
|
|
28
|
-
- name: Build package
|
|
29
|
-
run: python -m build
|
|
30
|
-
|
|
31
|
-
- name: Upload artifacts
|
|
32
|
-
uses: actions/upload-artifact@v4
|
|
33
|
-
with:
|
|
34
|
-
name: python-dist
|
|
35
|
-
path: dist/
|
|
36
|
-
|
|
37
|
-
publish:
|
|
38
|
-
needs: build
|
|
39
|
-
runs-on: ubuntu-latest
|
|
40
|
-
if: github.event_name == 'release' || github.ref == 'refs/heads/main'
|
|
41
|
-
environment: pypi
|
|
42
|
-
permissions:
|
|
43
|
-
id-token: write
|
|
44
|
-
steps:
|
|
45
|
-
- name: Download artifacts
|
|
46
|
-
uses: actions/download-artifact@v4
|
|
47
|
-
with:
|
|
48
|
-
name: python-dist
|
|
49
|
-
path: dist/
|
|
50
|
-
|
|
51
|
-
- name: Publish to PyPI
|
|
52
|
-
uses: pypa/gh-action-pypi-publish@release/v1
|
|
53
|
-
with:
|
|
54
|
-
skip-existing: true
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
"""Entity and Relationship models."""
|
|
2
|
-
from typing import List, Optional
|
|
3
|
-
|
|
4
|
-
from pydantic import BaseModel
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class EntityLocation(BaseModel):
|
|
8
|
-
"""Where an entity appears."""
|
|
9
|
-
|
|
10
|
-
sheet_id: str
|
|
11
|
-
sheet_title: Optional[str] = None
|
|
12
|
-
page: int
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class Fact(BaseModel):
|
|
16
|
-
"""Relationship between entities."""
|
|
17
|
-
|
|
18
|
-
id: str
|
|
19
|
-
fact: str
|
|
20
|
-
edge_type: str
|
|
21
|
-
source_id: str
|
|
22
|
-
source_label: Optional[str] = None
|
|
23
|
-
target_id: str
|
|
24
|
-
target_label: Optional[str] = None
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class Entity(BaseModel):
|
|
28
|
-
"""Full entity with relationships."""
|
|
29
|
-
|
|
30
|
-
id: str
|
|
31
|
-
type: str
|
|
32
|
-
label: str
|
|
33
|
-
description: Optional[str] = None
|
|
34
|
-
group_id: str
|
|
35
|
-
outgoing_facts: List[Fact] = []
|
|
36
|
-
incoming_facts: List[Fact] = []
|
|
37
|
-
locations: List[EntityLocation] = []
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|