diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0d157c4..5b96d79 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,7 +49,6 @@ jobs: cache: "poetry" - run: "SKIP_BUILD_FRONTEND=true poetry install" - run: "poetry run python yacv_server/logo.py" - - run: "cp assets/fox.glb assets/logo_build/fox.glb" - uses: "actions/upload-artifact@v4" with: name: "logo" diff --git a/assets/.gitignore b/assets/.gitignore new file mode 100644 index 0000000..a7dbd25 --- /dev/null +++ b/assets/.gitignore @@ -0,0 +1 @@ +/logo_build/ \ No newline at end of file diff --git a/assets/logo_build/base.glb b/assets/logo_build/base.glb deleted file mode 100644 index 41f834f..0000000 Binary files a/assets/logo_build/base.glb and /dev/null differ diff --git a/assets/logo_build/img.jpg.glb b/assets/logo_build/img.jpg.glb deleted file mode 100644 index 62948c9..0000000 Binary files a/assets/logo_build/img.jpg.glb and /dev/null differ diff --git a/assets/logo_build/location.glb b/assets/logo_build/location.glb deleted file mode 100644 index 9f2e846..0000000 Binary files a/assets/logo_build/location.glb and /dev/null differ diff --git a/frontend/App.vue b/frontend/App.vue index 4852c14..38eb0b9 100644 --- a/frontend/App.vue +++ b/frontend/App.vue @@ -32,8 +32,18 @@ const disableTap = ref(false); const setDisableTap = (val: boolean) => disableTap.value = val; provide('disableTap', {disableTap, setDisableTap}); -async function onModelLoadRequest(model: NetworkUpdateEvent) { - sceneDocument.value = await SceneMgr.loadModel(sceneUrl, sceneDocument.value, model.name, model.url); +async function onModelLoadRequest(event: NetworkUpdateEvent) { + // Load a new batch of models to optimize rendering time + let doc = sceneDocument.value; + for (let model of event.models) { + let isLast = event.models[event.models.length - 1].url == model.url; + if (!model.isRemove) { + doc = await SceneMgr.loadModel(sceneUrl, doc, model.name, model.url, isLast, isLast); + } else { + doc = await SceneMgr.removeModel(sceneUrl, doc, model.name, isLast); + } + } + sceneDocument.value = doc triggerRef(sceneDocument); // Why not triggered automatically? } diff --git a/frontend/misc/distances.ts b/frontend/misc/distances.ts index 0bc2a90..60eef3b 100644 --- a/frontend/misc/distances.ts +++ b/frontend/misc/distances.ts @@ -26,7 +26,6 @@ function getCenterAndVertexList(obj: MObject3D, scene: ModelScene): { vertices.push(vertex); } center = center.divideScalar(ind.count); - console.log("center", center) return {center, vertices}; } @@ -46,7 +45,6 @@ export function distances(a: MObject3D, b: MObject3D, scene: ModelScene): { // Find the closest and farthest vertices. // TODO: Compute actual min and max distances between the two objects. - // FIXME: Working for points and lines, but not triangles... // FIXME: Really slow... (use a BVH or something) let minDistance = Infinity; let minDistanceVertices = [new Vector3(), new Vector3()]; diff --git a/frontend/misc/network.ts b/frontend/misc/network.ts index 2d03a3d..b6c6afe 100644 --- a/frontend/misc/network.ts +++ b/frontend/misc/network.ts @@ -1,19 +1,36 @@ import {settings} from "./settings"; -export class NetworkUpdateEvent extends Event { +const batchTimeout = 250; // ms + +class NetworkUpdateEventModel { name: string; url: string; + // TODO: Detect and manage instances of the same object (same hash, different name) + hash: string | null; + isRemove: boolean; - constructor(name: string, url: string) { - super("update"); + constructor(name: string, url: string, hash: string | null, isDelete: boolean) { this.name = name; this.url = url; + this.hash = hash; + this.isRemove = isDelete; + } +} + +export class NetworkUpdateEvent extends Event { + models: NetworkUpdateEventModel[]; + + constructor(models: NetworkUpdateEventModel[]) { + super("update"); + this.models = models; } } /** Listens for updates and emits events when a model changes */ export class NetworkManager extends EventTarget { private knownObjectHashes: { [name: string]: string | null } = {}; + private bufferedUpdates: NetworkUpdateEventModel[] = []; + private batchTimeout: number | null = null; /** * Tries to load a new model (.glb) from the given URL. @@ -36,7 +53,7 @@ export class NetworkManager extends EventTarget { let response = await fetch(url, {method: "HEAD"}); let hash = response.headers.get("etag"); // Only trigger an update if the hash has changed - this.foundModel(name, hash, url); + this.foundModel(name, hash, url, false); } } @@ -44,17 +61,17 @@ export class NetworkManager extends EventTarget { try { // WARNING: This will spam the console logs with failed requests when the server is down let response = await fetch(url.toString()); - console.log("Monitoring", url.toString(), response); + // console.log("Monitoring", url.toString(), response); if (response.status === 200) { let lines = readLinesStreamings(response.body!.getReader()); for await (let line of lines) { if (!line || !line.startsWith("data:")) continue; let data = JSON.parse(line.slice(5)); - console.debug("WebSocket message", data); + // console.debug("WebSocket message", data); let urlObj = new URL(url); urlObj.searchParams.delete("api_updates"); urlObj.searchParams.set("api_object", data.name); - this.foundModel(data.name, data.hash, urlObj.toString()); + this.foundModel(data.name, data.hash, urlObj.toString(), data.is_remove); } } } catch (e) { // Ignore errors (retry very soon) @@ -63,12 +80,21 @@ export class NetworkManager extends EventTarget { return; } - private foundModel(name: string, hash: string | null, url: string) { + private foundModel(name: string, hash: string | null, url: string, isRemove: boolean) { let prevHash = this.knownObjectHashes[name]; - // TODO: Detect and manage instances of the same object (same hash, different name) - if (!hash || hash !== prevHash) { + let hashToCheck = hash + (isRemove ? "-remove" : ""); + // console.debug("Found model", name, "with hash", hash, "and previous hash", prevHash); + if (!hash || hashToCheck !== prevHash) { this.knownObjectHashes[name] = hash; - this.dispatchEvent(new NetworkUpdateEvent(name, url)); + let newModel = new NetworkUpdateEventModel(name, url, hash, isRemove); + this.bufferedUpdates.push(newModel); + + // Optimization: try to batch updates automatically for faster rendering + if (this.batchTimeout !== null) clearTimeout(this.batchTimeout); + this.batchTimeout = setTimeout(() => { + this.dispatchEvent(new NetworkUpdateEvent(this.bufferedUpdates)); + this.bufferedUpdates = []; + }, batchTimeout); } } } diff --git a/frontend/misc/scene.ts b/frontend/misc/scene.ts index 4d66fd5..cde87aa 100644 --- a/frontend/misc/scene.ts +++ b/frontend/misc/scene.ts @@ -9,7 +9,7 @@ import {Matrix4} from "three/src/math/Matrix4.js" /** This class helps manage SceneManagerData. All methods are static to support reactivity... */ export class SceneMgr { /** Loads a GLB model from a URL and adds it to the viewer or replaces it if the names match */ - static async loadModel(sceneUrl: Ref, document: Document, name: string, url: string): Promise { + static async loadModel(sceneUrl: Ref, document: Document, name: string, url: string, updateHelpers: boolean = true, reloadScene: boolean = true): Promise { let loadStart = performance.now(); // Start merging into the current document, replacing or adding as needed @@ -17,11 +17,13 @@ export class SceneMgr { console.log("Model", name, "loaded in", performance.now() - loadStart, "ms"); - if (name !== extrasNameValueHelpers) { + if (updateHelpers) { // Reload the helpers to fit the new model - // TODO: Only reload the helpers after a few milliseconds of no more models being added/removed - await this.reloadHelpers(sceneUrl, document); - } else { + await this.reloadHelpers(sceneUrl, document, reloadScene); + reloadScene = false; + } + + if (reloadScene) { // Display the final fully loaded model let displayStart = performance.now(); document = await this.showCurrentDoc(sceneUrl, document); @@ -31,7 +33,7 @@ export class SceneMgr { return document; } - private static async reloadHelpers(sceneUrl: Ref, document: Document): Promise { + private static async reloadHelpers(sceneUrl: Ref, document: Document, reloadScene: boolean): Promise { let bb = SceneMgr.getBoundingBox(document); // Create the helper axes and grid box @@ -40,7 +42,7 @@ export class SceneMgr { newAxes(helpersDoc, bb.getSize(new Vector3()).multiplyScalar(0.5), transform); newGridBox(helpersDoc, bb.getSize(new Vector3()), transform); let helpersUrl = URL.createObjectURL(new Blob([await toBuffer(helpersDoc)])); - return await SceneMgr.loadModel(sceneUrl, document, extrasNameValueHelpers, helpersUrl); + return await SceneMgr.loadModel(sceneUrl, document, extrasNameValueHelpers, helpersUrl, false, reloadScene); } static getBoundingBox(document: Document): Box3 { @@ -67,7 +69,7 @@ export class SceneMgr { } /** Removes a model from the viewer */ - static async removeModel(sceneUrl: Ref, document: Document, name: string): Promise { + static async removeModel(sceneUrl: Ref, document: Document, name: string, updateHelpers: boolean = true, reloadScene: boolean = true): Promise { let loadStart = performance.now(); // Remove the model from the document @@ -75,8 +77,10 @@ export class SceneMgr { console.log("Model", name, "removed in", performance.now() - loadStart, "ms"); - // Reload the helpers to fit the new model (will also show the document) - document = await this.reloadHelpers(sceneUrl, document); + if (updateHelpers) { + // Reload the helpers to fit the new model (will also show the document) + document = await this.reloadHelpers(sceneUrl, document, reloadScene); + } return document; } diff --git a/frontend/tools/OrientationGizmo.vue b/frontend/tools/OrientationGizmo.vue index 4fe166c..d953723 100644 --- a/frontend/tools/OrientationGizmo.vue +++ b/frontend/tools/OrientationGizmo.vue @@ -47,7 +47,7 @@ function createGizmo(expectedParent: HTMLElement, scene: ModelScene): HTMLElemen } scene.queueRender(); requestIdleCallback(() => props.elem?.dispatchEvent( - new CustomEvent('camera-change', {detail: {source: 'none'}}))) + new CustomEvent('camera-change', {detail: {source: 'none'}})), {timeout: 100}) } return gizmo; } @@ -60,7 +60,7 @@ let gizmo: HTMLElement & { update: () => void } function updateGizmo() { if (gizmo.isConnected) { gizmo.update(); - requestIdleCallback(updateGizmo); + requestIdleCallback(updateGizmo, {timeout: 250}); } } @@ -69,7 +69,7 @@ let reinstall = () => { if (gizmo) container.value.removeChild(gizmo); gizmo = createGizmo(container.value, props.scene as ModelScene) as typeof gizmo; container.value.appendChild(gizmo); - requestIdleCallback(updateGizmo); // Low priority updates + requestIdleCallback(updateGizmo, {timeout: 250}); // Low priority updates } onMounted(reinstall) onUpdated(reinstall); diff --git a/frontend/tools/Selection.vue b/frontend/tools/Selection.vue index f6b21cd..6f346dd 100644 --- a/frontend/tools/Selection.vue +++ b/frontend/tools/Selection.vue @@ -54,15 +54,22 @@ let selectionListener = (event: MouseEvent) => { } // Set raycaster parameters + let paramScale = 1; // Make it easier to select vertices/edges based on camera distance + if (props.viewer?.scene) { + let scene = props.viewer.scene; + let lookAtCenter = scene.getTarget().clone().add(scene.target.position); + paramScale = scene.camera.position.distanceTo(lookAtCenter) / 150; + // console.log('paramScale', paramScale) + } if (selectFilter.value === 'Any (S)') { - raycaster.params.Line.threshold = 0.2; - raycaster.params.Points.threshold = 0.8; + raycaster.params.Line.threshold = paramScale; + raycaster.params.Points.threshold = paramScale * 2; // Make vertices easier to select than edges } else if (selectFilter.value === '(E)dges') { - raycaster.params.Line.threshold = 0.8; + raycaster.params.Line.threshold = paramScale; raycaster.params.Points.threshold = 0.0; } else if (selectFilter.value === '(V)ertices') { raycaster.params.Line.threshold = 0.0; - raycaster.params.Points.threshold = 0.8; + raycaster.params.Points.threshold = paramScale; } else if (selectFilter.value === '(F)aces') { raycaster.params.Line.threshold = 0.0; raycaster.params.Points.threshold = 0.0; @@ -74,7 +81,7 @@ let selectionListener = (event: MouseEvent) => { const ndcCoords = scene.getNDC(event.clientX, event.clientY); raycaster.setFromCamera(ndcCoords, scene.camera); if (!scene.camera.isPerspectiveCamera) { - // Need to fix the ray direction for ortho camera FIXME: Still buggy... + // Need to fix the ray direction for ortho camera FIXME: Still buggy for off-center clicks raycaster.ray.direction.copy(scene.camera.getWorldDirection(new Vector3())); } //console.log('Ray', raycaster.ray); @@ -87,19 +94,36 @@ let selectionListener = (event: MouseEvent) => { // Find all hit objects and select the wanted one based on the filter const hits = raycaster.intersectObject(scene, true); - let hit = hits.find((hit: Intersection) => { - if (!hit.object) return false; - const kind = hit.object.type - let isFace = kind === 'Mesh' || kind === 'SkinnedMesh'; - let isEdge = kind === 'Line' || kind === 'LineSegments'; - let isVertex = kind === 'Points'; - const kindOk = (selectFilter.value === 'Any (S)') || - (isFace && selectFilter.value === '(F)aces') || - (isEdge && selectFilter.value === '(E)dges') || - (isVertex && selectFilter.value === '(V)ertices'); - return hit.object.visible && !hit.object.userData.noHit && kindOk; - }) as Intersection | undefined; - //console.log('Hit', hit) + let hit = hits + // Check feasibility + .filter((hit: Intersection) => { + if (!hit.object) return false; + const kind = hit.object.type + let isFace = kind === 'Mesh' || kind === 'SkinnedMesh'; + let isEdge = kind === 'Line' || kind === 'LineSegments'; + let isVertex = kind === 'Points'; + const kindOk = (selectFilter.value === 'Any (S)') || + (isFace && selectFilter.value === '(F)aces') || + (isEdge && selectFilter.value === '(E)dges') || + (isVertex && selectFilter.value === '(V)ertices'); + return (!isFace || hit.object.visible) && !hit.object.userData.noHit && kindOk; + }) + // Sort for highlighting partially hidden edges/vertices + .sort((a, b) => { + function lowerIsBetter(hit: Intersection) { + let score = hit.distance; + // Faces are easier to hit than 0-width edges/vertices, so we need to adjust scores + if (hit.object.type === 'Mesh' || hit.object.type === 'SkinnedMesh') score += paramScale; + // Edges are easier to hit than vertices, so we need to adjust scores + if (hit.object.type === 'Line' || hit.object.type === 'LineSegments') score += paramScale / 2; + return score; + } + + return lowerIsBetter(a) - lowerIsBetter(b); + }) + // Return the best hit + [0] as Intersection | undefined; + // console.log('Hit', hit) if (!highlightNextSelection.value[0]) { // If we are selecting, toggle the selection or deselect all if no hit @@ -126,7 +150,7 @@ let selectionListener = (event: MouseEvent) => { } function select(hit: Intersection) { - console.log('Selecting', hit.object.name) + // console.log('Selecting', hit.object.name) if (selected.value.find((m) => m.object.name === hit.object.name) === undefined) { selected.value.push(hit); } @@ -141,7 +165,7 @@ function select(hit: Intersection) { } function deselect(hit: Intersection, alsoRemove = true) { - console.log('Deselecting', hit.object.name) + // console.log('Deselecting', hit.object.name) if (alsoRemove) { // Remove the matching object from the selection let toRemove = selected.value.findIndex((m) => m.object.name === hit.object.name); @@ -293,6 +317,8 @@ function updateBoundingBox() { } let from = new Vector3(...corners[edge[0]]); let to = new Vector3(...corners[edge[1]]); + let length = to.clone().sub(from).length(); + if (length < 0.05) continue; // Skip very small edges (e.g. a single point) let color = [AxesColors.x, AxesColors.y, AxesColors.z][edgeI][1]; // Secondary colors let lineCacheKey = JSON.stringify([from, to]); let matchingLine = boundingBoxLines[lineCacheKey]; @@ -300,7 +326,7 @@ function updateBoundingBox() { boundingBoxLinesToRemove = boundingBoxLinesToRemove.filter((l) => l !== lineCacheKey); } else { let newLineId = props.viewer?.addLine3D(from, to, - to.clone().sub(from).length().toFixed(1) + "mm", { + length.toFixed(1) + "mm", { "stroke": "rgb(" + color.join(',') + ")", "stroke-width": "2" }); @@ -337,7 +363,7 @@ function updateDistances() { let distanceLinesToRemove = Object.keys(distanceLines); function ensureLine(from: Vector3, to: Vector3, text: string, color: string) { - console.log('ensureLine', from, to, text, color) + // console.log('ensureLine', from, to, text, color) let lineCacheKey = JSON.stringify([from, to]); let matchingLine = distanceLines[lineCacheKey]; if (matchingLine) { @@ -444,7 +470,7 @@ window.addEventListener('keydown', (event) => { .select-parent .v-btn { position: relative; - top: -42px; + top: -20px; } .select-only { diff --git a/frontend/tools/Tools.vue b/frontend/tools/Tools.vue index 3a15a4b..2fc8a95 100644 --- a/frontend/tools/Tools.vue +++ b/frontend/tools/Tools.vue @@ -81,7 +81,7 @@ function toggleProjection() { toggleProjectionText.value = wasPerspectiveCamera ? 'ORTHO' : 'PERSP'; // The camera change may take a few frames to take effect, dispatch the event after a delay requestIdleCallback(() => props.viewer?.elem?.dispatchEvent( - new CustomEvent('camera-change', {detail: {source: 'none'}}))) + new CustomEvent('camera-change', {detail: {source: 'none'}})), {timeout: 100}) } async function centerCamera() { diff --git a/frontend/viewer/ModelViewerWrapper.vue b/frontend/viewer/ModelViewerWrapper.vue index e59164b..fcfa65f 100644 --- a/frontend/viewer/ModelViewerWrapper.vue +++ b/frontend/viewer/ModelViewerWrapper.vue @@ -73,7 +73,7 @@ function addLine3D(p1: Vector3, p2: Vector3, centerText?: string, lineAttrs: { [ lineAttrs: lineAttrs }; scene.value.queueRender() // Needed to update the hotspots - requestIdleCallback(() => onCameraChangeLine(id)); + requestIdleCallback(() => onCameraChangeLine(id), {timeout: 100}); return id; } diff --git a/package.json b/package.json index f9e9b98..c3e3dd0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "yet-another-cad-viewer", - "version": "0.4.3", + "version": "0.5.0", "description": "", "license": "MIT", "author": "Yeicor <4929005+Yeicor@users.noreply.github.com>", diff --git a/poetry.lock b/poetry.lock index fa5d4eb..68fda51 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,182 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. - -[[package]] -name = "aiohttp" -version = "3.9.3" -description = "Async http client/server framework (asyncio)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:939677b61f9d72a4fa2a042a5eee2a99a24001a67c13da113b2e30396567db54"}, - {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1f5cd333fcf7590a18334c90f8c9147c837a6ec8a178e88d90a9b96ea03194cc"}, - {file = "aiohttp-3.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:82e6aa28dd46374f72093eda8bcd142f7771ee1eb9d1e223ff0fa7177a96b4a5"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f56455b0c2c7cc3b0c584815264461d07b177f903a04481dfc33e08a89f0c26b"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bca77a198bb6e69795ef2f09a5f4c12758487f83f33d63acde5f0d4919815768"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e083c285857b78ee21a96ba1eb1b5339733c3563f72980728ca2b08b53826ca5"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab40e6251c3873d86ea9b30a1ac6d7478c09277b32e14745d0d3c6e76e3c7e29"}, - {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df822ee7feaaeffb99c1a9e5e608800bd8eda6e5f18f5cfb0dc7eeb2eaa6bbec"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:acef0899fea7492145d2bbaaaec7b345c87753168589cc7faf0afec9afe9b747"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cd73265a9e5ea618014802ab01babf1940cecb90c9762d8b9e7d2cc1e1969ec6"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a78ed8a53a1221393d9637c01870248a6f4ea5b214a59a92a36f18151739452c"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6b0e029353361f1746bac2e4cc19b32f972ec03f0f943b390c4ab3371840aabf"}, - {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7cf5c9458e1e90e3c390c2639f1017a0379a99a94fdfad3a1fd966a2874bba52"}, - {file = "aiohttp-3.9.3-cp310-cp310-win32.whl", hash = "sha256:3e59c23c52765951b69ec45ddbbc9403a8761ee6f57253250c6e1536cacc758b"}, - {file = "aiohttp-3.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:055ce4f74b82551678291473f66dc9fb9048a50d8324278751926ff0ae7715e5"}, - {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6b88f9386ff1ad91ace19d2a1c0225896e28815ee09fc6a8932fded8cda97c3d"}, - {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c46956ed82961e31557b6857a5ca153c67e5476972e5f7190015018760938da2"}, - {file = "aiohttp-3.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07b837ef0d2f252f96009e9b8435ec1fef68ef8b1461933253d318748ec1acdc"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad46e6f620574b3b4801c68255492e0159d1712271cc99d8bdf35f2043ec266"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ed3e046ea7b14938112ccd53d91c1539af3e6679b222f9469981e3dac7ba1ce"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:039df344b45ae0b34ac885ab5b53940b174530d4dd8a14ed8b0e2155b9dddccb"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7943c414d3a8d9235f5f15c22ace69787c140c80b718dcd57caaade95f7cd93b"}, - {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84871a243359bb42c12728f04d181a389718710129b36b6aad0fc4655a7647d4"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5eafe2c065df5401ba06821b9a054d9cb2848867f3c59801b5d07a0be3a380ae"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9d3c9b50f19704552f23b4eaea1fc082fdd82c63429a6506446cbd8737823da3"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:f033d80bc6283092613882dfe40419c6a6a1527e04fc69350e87a9df02bbc283"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2c895a656dd7e061b2fd6bb77d971cc38f2afc277229ce7dd3552de8313a483e"}, - {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1f5a71d25cd8106eab05f8704cd9167b6e5187bcdf8f090a66c6d88b634802b4"}, - {file = "aiohttp-3.9.3-cp311-cp311-win32.whl", hash = "sha256:50fca156d718f8ced687a373f9e140c1bb765ca16e3d6f4fe116e3df7c05b2c5"}, - {file = "aiohttp-3.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:5fe9ce6c09668063b8447f85d43b8d1c4e5d3d7e92c63173e6180b2ac5d46dd8"}, - {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:38a19bc3b686ad55804ae931012f78f7a534cce165d089a2059f658f6c91fa60"}, - {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:770d015888c2a598b377bd2f663adfd947d78c0124cfe7b959e1ef39f5b13869"}, - {file = "aiohttp-3.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee43080e75fc92bf36219926c8e6de497f9b247301bbf88c5c7593d931426679"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52df73f14ed99cee84865b95a3d9e044f226320a87af208f068ecc33e0c35b96"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9b311743a78043b26ffaeeb9715dc360335e5517832f5a8e339f8a43581e4d"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b955ed993491f1a5da7f92e98d5dad3c1e14dc175f74517c4e610b1f2456fb11"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:504b6981675ace64c28bf4a05a508af5cde526e36492c98916127f5a02354d53"}, - {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fe5571784af92b6bc2fda8d1925cccdf24642d49546d3144948a6a1ed58ca5"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ba39e9c8627edc56544c8628cc180d88605df3892beeb2b94c9bc857774848ca"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e5e46b578c0e9db71d04c4b506a2121c0cb371dd89af17a0586ff6769d4c58c1"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:938a9653e1e0c592053f815f7028e41a3062e902095e5a7dc84617c87267ebd5"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:c3452ea726c76e92f3b9fae4b34a151981a9ec0a4847a627c43d71a15ac32aa6"}, - {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff30218887e62209942f91ac1be902cc80cddb86bf00fbc6783b7a43b2bea26f"}, - {file = "aiohttp-3.9.3-cp312-cp312-win32.whl", hash = "sha256:38f307b41e0bea3294a9a2a87833191e4bcf89bb0365e83a8be3a58b31fb7f38"}, - {file = "aiohttp-3.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:b791a3143681a520c0a17e26ae7465f1b6f99461a28019d1a2f425236e6eedb5"}, - {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ed621426d961df79aa3b963ac7af0d40392956ffa9be022024cd16297b30c8c"}, - {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7f46acd6a194287b7e41e87957bfe2ad1ad88318d447caf5b090012f2c5bb528"}, - {file = "aiohttp-3.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feeb18a801aacb098220e2c3eea59a512362eb408d4afd0c242044c33ad6d542"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f734e38fd8666f53da904c52a23ce517f1b07722118d750405af7e4123933511"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b40670ec7e2156d8e57f70aec34a7216407848dfe6c693ef131ddf6e76feb672"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fdd215b7b7fd4a53994f238d0f46b7ba4ac4c0adb12452beee724ddd0743ae5d"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:017a21b0df49039c8f46ca0971b3a7fdc1f56741ab1240cb90ca408049766168"}, - {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e99abf0bba688259a496f966211c49a514e65afa9b3073a1fcee08856e04425b"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:648056db9a9fa565d3fa851880f99f45e3f9a771dd3ff3bb0c048ea83fb28194"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8aacb477dc26797ee089721536a292a664846489c49d3ef9725f992449eda5a8"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:522a11c934ea660ff8953eda090dcd2154d367dec1ae3c540aff9f8a5c109ab4"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5bce0dc147ca85caa5d33debc4f4d65e8e8b5c97c7f9f660f215fa74fc49a321"}, - {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b4af9f25b49a7be47c0972139e59ec0e8285c371049df1a63b6ca81fdd216a2"}, - {file = "aiohttp-3.9.3-cp38-cp38-win32.whl", hash = "sha256:298abd678033b8571995650ccee753d9458dfa0377be4dba91e4491da3f2be63"}, - {file = "aiohttp-3.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:69361bfdca5468c0488d7017b9b1e5ce769d40b46a9f4a2eed26b78619e9396c"}, - {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0fa43c32d1643f518491d9d3a730f85f5bbaedcbd7fbcae27435bb8b7a061b29"}, - {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:835a55b7ca49468aaaac0b217092dfdff370e6c215c9224c52f30daaa735c1c1"}, - {file = "aiohttp-3.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06a9b2c8837d9a94fae16c6223acc14b4dfdff216ab9b7202e07a9a09541168f"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abf151955990d23f84205286938796c55ff11bbfb4ccfada8c9c83ae6b3c89a3"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59c26c95975f26e662ca78fdf543d4eeaef70e533a672b4113dd888bd2423caa"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f95511dd5d0e05fd9728bac4096319f80615aaef4acbecb35a990afebe953b0e"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:595f105710293e76b9dc09f52e0dd896bd064a79346234b521f6b968ffdd8e58"}, - {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7c8b816c2b5af5c8a436df44ca08258fc1a13b449393a91484225fcb7545533"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f1088fa100bf46e7b398ffd9904f4808a0612e1d966b4aa43baa535d1b6341eb"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f59dfe57bb1ec82ac0698ebfcdb7bcd0e99c255bd637ff613760d5f33e7c81b3"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:361a1026c9dd4aba0109e4040e2aecf9884f5cfe1b1b1bd3d09419c205e2e53d"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:363afe77cfcbe3a36353d8ea133e904b108feea505aa4792dad6585a8192c55a"}, - {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e2c45c208c62e955e8256949eb225bd8b66a4c9b6865729a786f2aa79b72e9d"}, - {file = "aiohttp-3.9.3-cp39-cp39-win32.whl", hash = "sha256:f7217af2e14da0856e082e96ff637f14ae45c10a5714b63c77f26d8884cf1051"}, - {file = "aiohttp-3.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:27468897f628c627230dba07ec65dc8d0db566923c48f29e084ce382119802bc"}, - {file = "aiohttp-3.9.3.tar.gz", hash = "sha256:90842933e5d1ff760fae6caca4b2b3edba53ba8f4b71e95dacf2818a2aca06f7"}, -] - -[package.dependencies] -aiosignal = ">=1.1.2" -async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} -attrs = ">=17.3.0" -frozenlist = ">=1.1.1" -multidict = ">=4.5,<7.0" -yarl = ">=1.0,<2.0" - -[package.extras] -speedups = ["Brotli", "aiodns", "brotlicffi"] - -[[package]] -name = "aiohttp-cors" -version = "0.7.0" -description = "CORS support for aiohttp" -optional = false -python-versions = "*" -files = [ - {file = "aiohttp-cors-0.7.0.tar.gz", hash = "sha256:4d39c6d7100fd9764ed1caf8cebf0eb01bf5e3f24e2e073fda6234bc48b19f5d"}, - {file = "aiohttp_cors-0.7.0-py3-none-any.whl", hash = "sha256:0451ba59fdf6909d0e2cd21e4c0a43752bc0703d33fc78ae94d9d9321710193e"}, -] - -[package.dependencies] -aiohttp = ">=1.1" - -[[package]] -name = "aiohttp-devtools" -version = "1.1.2" -description = "Dev tools for aiohttp" -optional = false -python-versions = ">=3.8" -files = [ - {file = "aiohttp-devtools-1.1.2.tar.gz", hash = "sha256:aae66fc9dd36b6c13e1ca381004dfaa4db6ba0dd50764c8ae3a49c1ed43f9587"}, - {file = "aiohttp_devtools-1.1.2-py38.py39.py310.py311-none-any.whl", hash = "sha256:d8545e217146df97459b3765fd786c98d0851c79d56162ad8d4145f719ab0391"}, -] - -[package.dependencies] -aiohttp = ">=3.9" -click = ">=6.6" -devtools = ">=0.6" -Pygments = ">=2.2.0" -watchfiles = ">=0.10" - -[[package]] -name = "aiohttp-sse" -version = "2.2.0" -description = "Server-sent events support for aiohttp." -optional = false -python-versions = ">=3.8" -files = [ - {file = "aiohttp-sse-2.2.0.tar.gz", hash = "sha256:a48dd5774031d3f41a29e159ebdbb93e89c8f37c1e9e83e196296be51885a5c2"}, - {file = "aiohttp_sse-2.2.0-py3-none-any.whl", hash = "sha256:339f9803bcf4682a2060e75548760d86abe4424a0d92ba66ff4985de3bd743dc"}, -] - -[package.dependencies] -aiohttp = ">=3.0" - -[[package]] -name = "aiosignal" -version = "1.3.1" -description = "aiosignal: a list of registered asynchronous callbacks" -optional = false -python-versions = ">=3.7" -files = [ - {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, - {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, -] - -[package.dependencies] -frozenlist = ">=1.1.0" - -[[package]] -name = "anyio" -version = "4.3.0" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -optional = false -python-versions = ">=3.8" -files = [ - {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, - {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, -] - -[package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} -idna = ">=2.8" -sniffio = ">=1.1" -typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} - -[package.extras] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (>=0.23)"] +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "anytree" @@ -210,36 +32,6 @@ six = ">=1.12.0" astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] -[[package]] -name = "async-timeout" -version = "4.0.3" -description = "Timeout context manager for asyncio programs" -optional = false -python-versions = ">=3.7" -files = [ - {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, - {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, -] - -[[package]] -name = "attrs" -version = "23.2.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.7" -files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, -] - -[package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] -tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] - [[package]] name = "build123d" version = "0.4.0" @@ -263,17 +55,6 @@ svgpathtools = ">=1.5.1,<2" trianglesolver = "*" typing-extensions = ">=4.6.0,<5" -[[package]] -name = "cachetools" -version = "5.2.1" -description = "Extensible memoizing collections and decorators" -optional = false -python-versions = "~=3.7" -files = [ - {file = "cachetools-5.2.1-py3-none-any.whl", hash = "sha256:8462eebf3a6c15d25430a8c27c56ac61340b2ecf60c9ce57afc2b97e450e47da"}, - {file = "cachetools-5.2.1.tar.gz", hash = "sha256:5991bc0e08a1319bb618d3195ca5b6bc76646a49c21d55962977197b301cc1fe"}, -] - [[package]] name = "cadquery-ocp" version = "7.7.2" @@ -298,20 +79,6 @@ files = [ {file = "cadquery_ocp-7.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:feea223eaa2dfa33684f568b5ba2b02c35e96b5d894014f98927b5c08041a6be"}, ] -[[package]] -name = "click" -version = "8.1.7" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - [[package]] name = "colorama" version = "0.4.6" @@ -366,22 +133,6 @@ wrapt = ">=1.10,<2" [package.extras] dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] -[[package]] -name = "devtools" -version = "0.12.2" -description = "Python's missing debug print command, and more." -optional = false -python-versions = ">=3.7" -files = [ - {file = "devtools-0.12.2-py3-none-any.whl", hash = "sha256:c366e3de1df4cdd635f1ad8cbcd3af01a384d7abda71900e68d43b04eb6aaca7"}, - {file = "devtools-0.12.2.tar.gz", hash = "sha256:efceab184cb35e3a11fa8e602cc4fadacaa2e859e920fc6f87bf130b69885507"}, -] - -[package.dependencies] -asttokens = ">=2.0.0,<3.0.0" -executing = ">=1.1.1" -pygments = ">=2.15.0" - [[package]] name = "exceptiongroup" version = "1.2.0" @@ -530,103 +281,6 @@ ufo = ["fs (>=2.2.0,<3)"] unicode = ["unicodedata2 (>=15.1.0)"] woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] -[[package]] -name = "frozenlist" -version = "1.4.1" -description = "A list-like structure which implements collections.abc.MutableSequence" -optional = false -python-versions = ">=3.8" -files = [ - {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, - {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, - {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, - {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, - {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, - {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, - {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, - {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, - {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, - {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, - {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, - {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, - {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, - {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, - {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, -] - -[[package]] -name = "idna" -version = "3.6" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.5" -files = [ - {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, - {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, -] - [[package]] name = "ipython" version = "8.18.1" @@ -664,6 +318,17 @@ qtconsole = ["qtconsole"] test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"] test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"] +[[package]] +name = "iterators" +version = "0.2.0" +description = "Iterator utility classes and functions" +optional = false +python-versions = ">=3.6" +files = [ + {file = "iterators-0.2.0-py3-none-any.whl", hash = "sha256:1d7ff03f576c9de0e01bac66209556c066d6b1fc45583a99cfc9f4645be7900e"}, + {file = "iterators-0.2.0.tar.gz", hash = "sha256:e9927a1ea1ef081830fd1512f3916857c36bd4b37272819a6cd29d0f44431b97"}, +] + [[package]] name = "jedi" version = "0.19.1" @@ -716,105 +381,6 @@ files = [ [package.dependencies] traitlets = "*" -[[package]] -name = "multidict" -version = "6.0.5" -description = "multidict implementation" -optional = false -python-versions = ">=3.7" -files = [ - {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, - {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, - {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, - {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, - {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, - {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, - {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, - {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, - {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, - {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"}, - {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"}, - {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, - {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, - {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, - {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, - {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, - {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, - {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, -] - [[package]] name = "mypy-extensions" version = "1.0.0" @@ -871,73 +437,6 @@ files = [ {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, ] -[[package]] -name = "numpy-quaternion" -version = "2023.0.2" -description = "Add a quaternion dtype to NumPy" -optional = false -python-versions = "*" -files = [ - {file = "numpy-quaternion-2023.0.2.tar.gz", hash = "sha256:37f73d7f84c645bd9be95cb4862bd900b7f99bd2f801232006dde00641bf2fd7"}, - {file = "numpy_quaternion-2023.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cf487d6b56883895ddf22307a0cf8e9949604465154d0cd9b78250d800d07a0d"}, - {file = "numpy_quaternion-2023.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac5e37ed57c0e2ff938c88d4462a126b16c98581dde0c003eba05741188b7f38"}, - {file = "numpy_quaternion-2023.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b144be3dca3330f8ad5866c561cebbfe3273a5b228ece058c014cdbf8916630d"}, - {file = "numpy_quaternion-2023.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48bb1fc03b580a9bb89da9d4f8916f87101bc75682611c423bafa031b6d96176"}, - {file = "numpy_quaternion-2023.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:713e4357868ebd8e4f3500435fcb49a997a8a9a5f8514e3a79d51f46abcdf2ae"}, - {file = "numpy_quaternion-2023.0.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c829f58ebc908f07487d3351a13ba99c3e39eb5e04aea389ca5175642cfdab15"}, - {file = "numpy_quaternion-2023.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5dd15141aecbf32cdb6bf96bdc13df7dd2f31833011a7f0ef51ecc86872cf8f0"}, - {file = "numpy_quaternion-2023.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0560b12235aaab7aee56e94c2df2f7879e0c965b8aea3c6bccaad7f2b4fb031a"}, - {file = "numpy_quaternion-2023.0.2-cp310-cp310-win32.whl", hash = "sha256:e033eef943a904b9c34c1d9e66570a07fa2c3d4a311a357d1aeb305493092c08"}, - {file = "numpy_quaternion-2023.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6c7e82014a51c93fe76322654d9c59f03b2e5cd19d0d6535d606bf7a119d4394"}, - {file = "numpy_quaternion-2023.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:86d46c5f220ed2289d7d53c918b0e2432d6ddeae20c5ca232f3dab6fafe6c340"}, - {file = "numpy_quaternion-2023.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c2ddf7e16a611f1c07a170d9464d69291eeb734ade2ce50b7f4eb38d9620f007"}, - {file = "numpy_quaternion-2023.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:57d99cee91c7356c62d70817d32432db3da58f4d5f3bd29757c5696f56fa2e86"}, - {file = "numpy_quaternion-2023.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04c4536fdb7f22733631b7953e2db82b27964d96f97423901e749c971cb7f6f2"}, - {file = "numpy_quaternion-2023.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fdbc31fdac812ed2ff0287a2d51e1b87d5ec6d2aeea4a667adb14f4b6198bc5"}, - {file = "numpy_quaternion-2023.0.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f994628b10bf29461fb50cf3ce022d0a610e173068414942a9efd746b35b38b"}, - {file = "numpy_quaternion-2023.0.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:32e34d2ebeeed25b238df22eba0030ba8db4a4e82a7eb6f5e32fda45768990ee"}, - {file = "numpy_quaternion-2023.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:98bfb77597ea56462be3f94e002640ebc6ecf9d2eeea140f5d1c13145af56a31"}, - {file = "numpy_quaternion-2023.0.2-cp311-cp311-win32.whl", hash = "sha256:e6dcfec4c7f615e6c46411c2034631e0a1934ffc3509e7bd61c3aacce4ecb181"}, - {file = "numpy_quaternion-2023.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:86f931da5893db57c4da4142045b605cc99d469fb3e6238ae487e080dcd7227e"}, - {file = "numpy_quaternion-2023.0.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:bca80ceef24364eb4dc07026e3d5c7cc9932b844888a3a15f27941f0ee6ba5c3"}, - {file = "numpy_quaternion-2023.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bf6a99191d1d0b3289eb256c1eaf7e290d80d4a306bb31d04121bf9a7eb88701"}, - {file = "numpy_quaternion-2023.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b26f4961fef053d552f5dcea0957b1eb34c99fea92efe1544044013d04e1407"}, - {file = "numpy_quaternion-2023.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c5b7dfb7412b582101ae4e576f15bc6af904f66b24b832aa1fafa3a846c71da"}, - {file = "numpy_quaternion-2023.0.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c058ee103024dc15b3232e57204934a53be080d5c75246cdec9eb92e9f56c5f"}, - {file = "numpy_quaternion-2023.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:02b93874723c38ad1e684d0862899d9266bf9855fd5a5bdcba8793169e672c31"}, - {file = "numpy_quaternion-2023.0.2-cp312-cp312-win32.whl", hash = "sha256:449ba07ec505dd757aa4ba6df8ef086bdd06c85f4681529ddaecd4ce7d62e792"}, - {file = "numpy_quaternion-2023.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e218a5207be1a983d3fd54d710067a6638d324015ba695c0509082a29086284"}, - {file = "numpy_quaternion-2023.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:333dea61c9628707223dc062e4a6e0a72bbb4fffd58a84231ea24b959e694bde"}, - {file = "numpy_quaternion-2023.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b343649600eb9f30275380b47ee4430f4393ed3370e5fa3fbb1db0ebbd908228"}, - {file = "numpy_quaternion-2023.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b4df8ffdcab6f773eec518ed09abb81e233afd9a38534e3a1db0cb0bfc54b370"}, - {file = "numpy_quaternion-2023.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04671ea098c0fe879eb07a24ec80dc09efc674e178f9b58a427f9d2368b2c009"}, - {file = "numpy_quaternion-2023.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d11f6f030d1cc7b58afe83fa849422a1c8c3a742b7af30232b98acbe32cd2be6"}, - {file = "numpy_quaternion-2023.0.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60c1e9f9997205949c770702307451eeffd96f3a2824f4dc49ed42336bd698e2"}, - {file = "numpy_quaternion-2023.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6dd56641fddad6c35d86a6d9f3cee4a786d0a4c6b41ed74d60dad97741835280"}, - {file = "numpy_quaternion-2023.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:681aaa2cf4d59fc412ee00188dcdc551c8ff91ea63d54d06f37ec66dd383633d"}, - {file = "numpy_quaternion-2023.0.2-cp38-cp38-win32.whl", hash = "sha256:e6b4dd4797e6e77fcdd8b3487893f8af3fe934f1f26839d1605f771f700dded6"}, - {file = "numpy_quaternion-2023.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4b9421d46d56fbec0dd625c9909550c66bb81265a76efaecc5621166f18069bf"}, - {file = "numpy_quaternion-2023.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:41968027811fa81157c9bc9f2bf00cc22dc8865d7fb5834f9f83bafc5995b6ec"}, - {file = "numpy_quaternion-2023.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eeffe622c5cec8396e61c266f65c75ec54fa4c21688a9633e8737276dc7fcc4b"}, - {file = "numpy_quaternion-2023.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b0f8517c268d748cbfe686214bd53ac7064e85106c90e22bd7cf04940a17323e"}, - {file = "numpy_quaternion-2023.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dec65adaac6bf15f31951e25bf5fe908135db6e223cf2df0112c93afe432d5de"}, - {file = "numpy_quaternion-2023.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb3ab05505ccb5c835a6f0401811d64f23c843e622751956ba77734f7dc20493"}, - {file = "numpy_quaternion-2023.0.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13958c8628b17f9bc725bb54e910c384e211e54b057cbe069f1615aebae8735d"}, - {file = "numpy_quaternion-2023.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:eeeb8a6004a649b4a411fb25fb94a6da8e937de25b7c409c62528c937d1bb47d"}, - {file = "numpy_quaternion-2023.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d725796e9f21eb703ae19448ceea0ab34e850c903ab01fef3de06f7217ae17f5"}, - {file = "numpy_quaternion-2023.0.2-cp39-cp39-win32.whl", hash = "sha256:3f89e11f89ded410fb34e6f997d4c7f4cf7c31c3eb9537c035756a5d2a6cc4e3"}, - {file = "numpy_quaternion-2023.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:cab8b1626c6d719639360a6af920c25df3f0248ab04635b72919aa1a05cb575f"}, -] - -[package.dependencies] -numpy = ">=1.13" - -[package.extras] -docs = ["mkdocs", "mktheapidocs[plugin]", "pymdown-extensions"] -numba = ["llvmlite (<0.32.0)", "numba", "numba (<0.49.0)"] -scipy = ["scipy"] -testing = ["pytest", "pytest-cov"] - [[package]] name = "numpy-stl" version = "3.1.1" @@ -953,26 +452,6 @@ files = [ numpy = "*" python-utils = ">=3.4.5" -[[package]] -name = "ocp-tessellate" -version = "2.0.6" -description = "Tessellate OCP objects" -optional = false -python-versions = ">=3.9" -files = [ - {file = "ocp_tessellate-2.0.6-py3-none-any.whl", hash = "sha256:a3c50c9f83b47565a5fca2c63448fe7ab9cf2a06af803eb695d165b6d960d2b3"}, - {file = "ocp_tessellate-2.0.6.tar.gz", hash = "sha256:7c3e0f09f684085e50c4af7a1f8ffd839d6821ae11aa0e693b2bad5cabe5270c"}, -] - -[package.dependencies] -cachetools = ">=5.2.0,<5.3.0" -numpy = "*" -numpy-quaternion = "*" -webcolors = ">=1.12,<2.0" - -[package.extras] -dev = ["black", "bumpversion", "pyYaml", "pylint", "twine"] - [[package]] name = "ocpsvg" version = "0.2.0" @@ -1281,17 +760,6 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - [[package]] name = "stack-data" version = "0.6.3" @@ -1401,93 +869,6 @@ files = [ mypy-extensions = ">=0.3.0" typing-extensions = ">=3.7.4" -[[package]] -name = "watchfiles" -version = "0.21.0" -description = "Simple, modern and high performance file watching and code reload in python." -optional = false -python-versions = ">=3.8" -files = [ - {file = "watchfiles-0.21.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:27b4035013f1ea49c6c0b42d983133b136637a527e48c132d368eb19bf1ac6aa"}, - {file = "watchfiles-0.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c81818595eff6e92535ff32825f31c116f867f64ff8cdf6562cd1d6b2e1e8f3e"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6c107ea3cf2bd07199d66f156e3ea756d1b84dfd43b542b2d870b77868c98c03"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d9ac347653ebd95839a7c607608703b20bc07e577e870d824fa4801bc1cb124"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5eb86c6acb498208e7663ca22dbe68ca2cf42ab5bf1c776670a50919a56e64ab"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f564bf68404144ea6b87a78a3f910cc8de216c6b12a4cf0b27718bf4ec38d303"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d0f32ebfaa9c6011f8454994f86108c2eb9c79b8b7de00b36d558cadcedaa3d"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d45d9b699ecbac6c7bd8e0a2609767491540403610962968d258fd6405c17c"}, - {file = "watchfiles-0.21.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:aff06b2cac3ef4616e26ba17a9c250c1fe9dd8a5d907d0193f84c499b1b6e6a9"}, - {file = "watchfiles-0.21.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d9792dff410f266051025ecfaa927078b94cc7478954b06796a9756ccc7e14a9"}, - {file = "watchfiles-0.21.0-cp310-none-win32.whl", hash = "sha256:214cee7f9e09150d4fb42e24919a1e74d8c9b8a9306ed1474ecaddcd5479c293"}, - {file = "watchfiles-0.21.0-cp310-none-win_amd64.whl", hash = "sha256:1ad7247d79f9f55bb25ab1778fd47f32d70cf36053941f07de0b7c4e96b5d235"}, - {file = "watchfiles-0.21.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:668c265d90de8ae914f860d3eeb164534ba2e836811f91fecc7050416ee70aa7"}, - {file = "watchfiles-0.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a23092a992e61c3a6a70f350a56db7197242f3490da9c87b500f389b2d01eef"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e7941bbcfdded9c26b0bf720cb7e6fd803d95a55d2c14b4bd1f6a2772230c586"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11cd0c3100e2233e9c53106265da31d574355c288e15259c0d40a4405cbae317"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78f30cbe8b2ce770160d3c08cff01b2ae9306fe66ce899b73f0409dc1846c1b"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6674b00b9756b0af620aa2a3346b01f8e2a3dc729d25617e1b89cf6af4a54eb1"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd7ac678b92b29ba630d8c842d8ad6c555abda1b9ef044d6cc092dacbfc9719d"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c873345680c1b87f1e09e0eaf8cf6c891b9851d8b4d3645e7efe2ec20a20cc7"}, - {file = "watchfiles-0.21.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:49f56e6ecc2503e7dbe233fa328b2be1a7797d31548e7a193237dcdf1ad0eee0"}, - {file = "watchfiles-0.21.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:02d91cbac553a3ad141db016e3350b03184deaafeba09b9d6439826ee594b365"}, - {file = "watchfiles-0.21.0-cp311-none-win32.whl", hash = "sha256:ebe684d7d26239e23d102a2bad2a358dedf18e462e8808778703427d1f584400"}, - {file = "watchfiles-0.21.0-cp311-none-win_amd64.whl", hash = "sha256:4566006aa44cb0d21b8ab53baf4b9c667a0ed23efe4aaad8c227bfba0bf15cbe"}, - {file = "watchfiles-0.21.0-cp311-none-win_arm64.whl", hash = "sha256:c550a56bf209a3d987d5a975cdf2063b3389a5d16caf29db4bdddeae49f22078"}, - {file = "watchfiles-0.21.0-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:51ddac60b96a42c15d24fbdc7a4bfcd02b5a29c047b7f8bf63d3f6f5a860949a"}, - {file = "watchfiles-0.21.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:511f0b034120cd1989932bf1e9081aa9fb00f1f949fbd2d9cab6264916ae89b1"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cfb92d49dbb95ec7a07511bc9efb0faff8fe24ef3805662b8d6808ba8409a71a"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f92944efc564867bbf841c823c8b71bb0be75e06b8ce45c084b46411475a915"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:642d66b75eda909fd1112d35c53816d59789a4b38c141a96d62f50a3ef9b3360"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d23bcd6c8eaa6324fe109d8cac01b41fe9a54b8c498af9ce464c1aeeb99903d6"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18d5b4da8cf3e41895b34e8c37d13c9ed294954907929aacd95153508d5d89d7"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b8d1eae0f65441963d805f766c7e9cd092f91e0c600c820c764a4ff71a0764c"}, - {file = "watchfiles-0.21.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1fd9a5205139f3c6bb60d11f6072e0552f0a20b712c85f43d42342d162be1235"}, - {file = "watchfiles-0.21.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a1e3014a625bcf107fbf38eece0e47fa0190e52e45dc6eee5a8265ddc6dc5ea7"}, - {file = "watchfiles-0.21.0-cp312-none-win32.whl", hash = "sha256:9d09869f2c5a6f2d9df50ce3064b3391d3ecb6dced708ad64467b9e4f2c9bef3"}, - {file = "watchfiles-0.21.0-cp312-none-win_amd64.whl", hash = "sha256:18722b50783b5e30a18a8a5db3006bab146d2b705c92eb9a94f78c72beb94094"}, - {file = "watchfiles-0.21.0-cp312-none-win_arm64.whl", hash = "sha256:a3b9bec9579a15fb3ca2d9878deae789df72f2b0fdaf90ad49ee389cad5edab6"}, - {file = "watchfiles-0.21.0-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:4ea10a29aa5de67de02256a28d1bf53d21322295cb00bd2d57fcd19b850ebd99"}, - {file = "watchfiles-0.21.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:40bca549fdc929b470dd1dbfcb47b3295cb46a6d2c90e50588b0a1b3bd98f429"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9b37a7ba223b2f26122c148bb8d09a9ff312afca998c48c725ff5a0a632145f7"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec8c8900dc5c83650a63dd48c4d1d245343f904c4b64b48798c67a3767d7e165"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ad3fe0a3567c2f0f629d800409cd528cb6251da12e81a1f765e5c5345fd0137"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d353c4cfda586db2a176ce42c88f2fc31ec25e50212650c89fdd0f560ee507b"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:83a696da8922314ff2aec02987eefb03784f473281d740bf9170181829133765"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a03651352fc20975ee2a707cd2d74a386cd303cc688f407296064ad1e6d1562"}, - {file = "watchfiles-0.21.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3ad692bc7792be8c32918c699638b660c0de078a6cbe464c46e1340dadb94c19"}, - {file = "watchfiles-0.21.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06247538e8253975bdb328e7683f8515ff5ff041f43be6c40bff62d989b7d0b0"}, - {file = "watchfiles-0.21.0-cp38-none-win32.whl", hash = "sha256:9a0aa47f94ea9a0b39dd30850b0adf2e1cd32a8b4f9c7aa443d852aacf9ca214"}, - {file = "watchfiles-0.21.0-cp38-none-win_amd64.whl", hash = "sha256:8d5f400326840934e3507701f9f7269247f7c026d1b6cfd49477d2be0933cfca"}, - {file = "watchfiles-0.21.0-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:7f762a1a85a12cc3484f77eee7be87b10f8c50b0b787bb02f4e357403cad0c0e"}, - {file = "watchfiles-0.21.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6e9be3ef84e2bb9710f3f777accce25556f4a71e15d2b73223788d528fcc2052"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4c48a10d17571d1275701e14a601e36959ffada3add8cdbc9e5061a6e3579a5d"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c889025f59884423428c261f212e04d438de865beda0b1e1babab85ef4c0f01"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:66fac0c238ab9a2e72d026b5fb91cb902c146202bbd29a9a1a44e8db7b710b6f"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4a21f71885aa2744719459951819e7bf5a906a6448a6b2bbce8e9cc9f2c8128"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c9198c989f47898b2c22201756f73249de3748e0fc9de44adaf54a8b259cc0c"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f57c4461cd24fda22493109c45b3980863c58a25b8bec885ca8bea6b8d4b28"}, - {file = "watchfiles-0.21.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:853853cbf7bf9408b404754b92512ebe3e3a83587503d766d23e6bf83d092ee6"}, - {file = "watchfiles-0.21.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d5b1dc0e708fad9f92c296ab2f948af403bf201db8fb2eb4c8179db143732e49"}, - {file = "watchfiles-0.21.0-cp39-none-win32.whl", hash = "sha256:59137c0c6826bd56c710d1d2bda81553b5e6b7c84d5a676747d80caf0409ad94"}, - {file = "watchfiles-0.21.0-cp39-none-win_amd64.whl", hash = "sha256:6cb8fdc044909e2078c248986f2fc76f911f72b51ea4a4fbbf472e01d14faa58"}, - {file = "watchfiles-0.21.0-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab03a90b305d2588e8352168e8c5a1520b721d2d367f31e9332c4235b30b8994"}, - {file = "watchfiles-0.21.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:927c589500f9f41e370b0125c12ac9e7d3a2fd166b89e9ee2828b3dda20bfe6f"}, - {file = "watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bd467213195e76f838caf2c28cd65e58302d0254e636e7c0fca81efa4a2e62c"}, - {file = "watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02b73130687bc3f6bb79d8a170959042eb56eb3a42df3671c79b428cd73f17cc"}, - {file = "watchfiles-0.21.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:08dca260e85ffae975448e344834d765983237ad6dc308231aa16e7933db763e"}, - {file = "watchfiles-0.21.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:3ccceb50c611c433145502735e0370877cced72a6c70fd2410238bcbc7fe51d8"}, - {file = "watchfiles-0.21.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57d430f5fb63fea141ab71ca9c064e80de3a20b427ca2febcbfcef70ff0ce895"}, - {file = "watchfiles-0.21.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dd5fad9b9c0dd89904bbdea978ce89a2b692a7ee8a0ce19b940e538c88a809c"}, - {file = "watchfiles-0.21.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:be6dd5d52b73018b21adc1c5d28ac0c68184a64769052dfeb0c5d9998e7f56a2"}, - {file = "watchfiles-0.21.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b3cab0e06143768499384a8a5efb9c4dc53e19382952859e4802f294214f36ec"}, - {file = "watchfiles-0.21.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c6ed10c2497e5fedadf61e465b3ca12a19f96004c15dcffe4bd442ebadc2d85"}, - {file = "watchfiles-0.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43babacef21c519bc6631c5fce2a61eccdfc011b4bcb9047255e9620732c8097"}, - {file = "watchfiles-0.21.0.tar.gz", hash = "sha256:c76c635fabf542bb78524905718c39f736a98e5ab25b23ec6d4abede1a85a6a3"}, -] - -[package.dependencies] -anyio = ">=3.0.0" - [[package]] name = "wcwidth" version = "0.2.13" @@ -1499,21 +880,6 @@ files = [ {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, ] -[[package]] -name = "webcolors" -version = "1.13" -description = "A library for working with the color formats defined by HTML and CSS." -optional = false -python-versions = ">=3.7" -files = [ - {file = "webcolors-1.13-py3-none-any.whl", hash = "sha256:29bc7e8752c0a1bd4a1f03c14d6e6a72e93d82193738fa860cbff59d0fcc11bf"}, - {file = "webcolors-1.13.tar.gz", hash = "sha256:c225b674c83fa923be93d235330ce0300373d02885cef23238813b0d5668304a"}, -] - -[package.extras] -docs = ["furo", "sphinx", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-notfound-page", "sphinxext-opengraph"] -tests = ["pytest", "pytest-cov"] - [[package]] name = "wrapt" version = "1.16.0" @@ -1593,110 +959,7 @@ files = [ {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, ] -[[package]] -name = "yarl" -version = "1.9.4" -description = "Yet another URL library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, - {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, - {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, - {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, - {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, - {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, - {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, - {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, - {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, - {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, - {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, - {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, - {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, - {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, - {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, - {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, - {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, - {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, -] - -[package.dependencies] -idna = ">=2.0" -multidict = ">=4.0" - [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "734e26c5b174a1b5d0942e9b67d6fff679e1e7e06c5f2fd9978911befc9aec3c" +content-hash = "d9746e99dd8861758730e68d12dc72d9ec5fb0101b3c070a7d7a373439c658a0" diff --git a/pyproject.toml b/pyproject.toml index eabdc9a..94c1233 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "yacv-server" -version = "0.4.4" # TODO: Update automatically by CI on release (also for package.json!) +version = "0.5.0" # TODO: Update automatically by CI on release (also for package.json!) description = "Yet Another CAD Viewer (server)" authors = ["Yeicor <4929005+Yeicor@users.noreply.github.com>"] license = "MIT" @@ -15,17 +15,11 @@ python = "^3.9" # CAD build123d = "^0.4.0" -ocp-tessellate = "^2.0.6" - -# Web -aiohttp = "^3.9.3" -aiohttp-sse = "^2.2.0" -aiohttp-cors = "^0.7.0" -aiohttp-devtools = "^1.1.2" # Misc pygltflib = "^1.16.2" pillow = "^10.2.0" +iterators = "^0.2.0" [tool.poetry.build] generate-setup-file = false diff --git a/yacv_server/__init__.py b/yacv_server/__init__.py index 9e58af8..4e3b64b 100644 --- a/yacv_server/__init__.py +++ b/yacv_server/__init__.py @@ -1,42 +1,21 @@ -import logging import os -import time -from aiohttp import web +from yacv_server.yacv import YACV -from yacv_server.server import Server - -server = Server() +yacv = YACV() """The server instance. This is the main entry point to serve CAD objects and other data to the frontend.""" if 'YACV_DISABLE_SERVER' not in os.environ: # Start a new server ASAP to let the polling client connect while still building CAD objects # This is a bit of a hack, but it is seamless to the user. This behavior can be disabled by setting # the environment variable YACV_DISABLE_SERVER to a non-empty value - server.start() + yacv.start() # Expose some nice aliases using the default server instance -show = server.show +show = yacv.show show_object = show -show_image = server.show_image -show_all = server.show_cad_all -export_all = server.export_all - - -def _get_app() -> web.Application: - """Required by aiohttp-devtools""" - logging.basicConfig(level=logging.DEBUG) - from logo import build_logo, ASSETS_DIR - logo, img_location, img_path = build_logo() - server.show_cad(logo, 'logo') - server.show_cad(img_location, 'location') - server.show_image(img_path, img_location, 20) - server.show_gltf(open(os.path.join(ASSETS_DIR, 'fox.glb'), 'rb').read(), 'fox') - return server.app - - -if __name__ == '__main__': - # Publish the logo to the server (reusing code from the aiohttp-devtools) - _get_app() - # Keep the server running for testing - time.sleep(60) +show_image = yacv.show_image +show_all = yacv.show_cad_all +export_all = yacv.export_all +remove = yacv.remove +clear = yacv.clear diff --git a/yacv_server/cad.py b/yacv_server/cad.py index b3003d8..e590659 100644 --- a/yacv_server/cad.py +++ b/yacv_server/cad.py @@ -58,8 +58,8 @@ def grab_all_cad() -> List[Tuple[str, CADLike]]: return shapes -def image_to_gltf(source: str | bytes, center: any, ppmm: int, name: Optional[str] = None, - save_mime: str = 'image/jpeg') -> Tuple[bytes, str]: +def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = None, height: Optional[float] = None, + name: Optional[str] = None, save_mime: str = 'image/jpeg') -> Tuple[bytes, str]: """Convert an image to a GLTF CAD object, indicating the center location and pixels per millimeter.""" from PIL import Image import io @@ -105,11 +105,17 @@ def image_to_gltf(source: str | bytes, center: any, ppmm: int, name: Optional[st # Build the gltf mgr = GLTFMgr(image=(img_buf, save_mime)) + if width is None and height is None: + raise ValueError('At least one of width or height must be specified') # Fallback to pixels == mm? + elif width is None: + width = img.width / img.height * height + elif height is None: + height = height or img.height / img.width * width # Apply default aspect ratio if unspecified mgr.add_face([ - vert(plane.origin - plane.x_dir * img.width / (2 * ppmm) - plane.y_dir * img.height / (2 * ppmm)), - vert(plane.origin + plane.x_dir * img.width / (2 * ppmm) - plane.y_dir * img.height / (2 * ppmm)), - vert(plane.origin + plane.x_dir * img.width / (2 * ppmm) + plane.y_dir * img.height / (2 * ppmm)), - vert(plane.origin - plane.x_dir * img.width / (2 * ppmm) + plane.y_dir * img.height / (2 * ppmm)), + vert(plane.origin - plane.x_dir * width / 2 - plane.y_dir * height / 2), + vert(plane.origin + plane.x_dir * width / 2 - plane.y_dir * height / 2), + vert(plane.origin + plane.x_dir * width / 2 + plane.y_dir * height / 2), + vert(plane.origin - plane.x_dir * width / 2 + plane.y_dir * height / 2), ], [ (0, 2, 1), (0, 3, 2), diff --git a/yacv_server/logo.py b/yacv_server/logo.py index 5e9870e..70ee228 100644 --- a/yacv_server/logo.py +++ b/yacv_server/logo.py @@ -1,14 +1,12 @@ -import asyncio -import logging import os -from typing import Tuple +from typing import Union, Dict from build123d import * ASSETS_DIR = os.getenv('ASSETS_DIR', os.path.join(os.path.dirname(__file__), '..', 'assets')) -def build_logo(text: bool = True) -> Tuple[Part, Location, str]: +def build_logo(text: bool = True) -> Dict[str, Union[Part, Location, str]]: """Builds the CAD part of the logo""" with BuildPart(Plane.XY.offset(50)) as logo_obj: Box(22, 40, 30) @@ -25,34 +23,44 @@ def build_logo(text: bool = True) -> Tuple[Part, Location, str]: logo_img_location.position = Vector(logo_img_location.position.X - 4e-2, logo_img_location.position.Y, logo_img_location.position.Z) logo_img_path = os.path.join(ASSETS_DIR, 'img.jpg') - return logo_obj.part, logo_img_location, logo_img_path + + fox_glb_bytes = open(os.path.join(ASSETS_DIR, 'fox.glb'), 'rb').read() + + return {'fox': fox_glb_bytes, 'logo': logo_obj, 'location': logo_img_location, 'img_path': logo_img_path} + + +def show_logo(parts: Dict[str, Union[Part, Location, str]]) -> None: + """Shows the prebuilt logo parts""" + from yacv_server import show_image, show_object + for name, part in parts.items(): + if isinstance(part, str): + show_image(source=part, center=parts['location'], height=18, auto_clear=False) + else: + show_object(part, name, auto_clear=False) if __name__ == "__main__": + from yacv_server import export_all, remove + import logging + logging.basicConfig(level=logging.DEBUG) - # Start an offline server to export the CAD part of the logo in a way compatible with the frontend - # If this is not set, the server will auto-start on import and show_* calls will provide live updates - os.environ['YACV_DISABLE_SERVER'] = '1' - from yacv_server import show, show_image + testing_server = bool(os.getenv('TESTING_SERVER', 'False')) + + if not testing_server: + # Start an offline server to export the CAD part of the logo in a way compatible with the frontend + # If this is not set, the server will auto-start on import and show_* calls will provide live updates + os.environ['YACV_DISABLE_SERVER'] = 'True' + + # Build the CAD part of the logo + logo = build_logo() # Add the CAD part of the logo to the server - logo, img_location, img_path = build_logo() - show(logo, 'base') - show(img_location, 'location') - show_image(img_path, img_location, 20) + show_logo(logo) - - async def exporter(): - # We need access to the actual server object for advanced features like exporting to file - from yacv_server import server - for name in server.shown_object_names(): - print(f'Exporting {name} to GLB...') - with open(os.path.join(ASSETS_DIR, 'logo_build', f'{name}.glb'), 'wb') as f: - f.write(await server.export(name)) - - - # Save the complete logo to multiple GLB files (async required) - asyncio.run(exporter()) - - print('Logo saved!') + if testing_server: + remove('location') # Test removing a part + else: + # Save the complete logo to multiple GLB files + export_all(os.path.join(ASSETS_DIR, 'logo_build')) + print('Logo saved!') diff --git a/yacv_server/myhttp.py b/yacv_server/myhttp.py new file mode 100644 index 0000000..b887d88 --- /dev/null +++ b/yacv_server/myhttp.py @@ -0,0 +1,143 @@ +import io +import os +import threading +import urllib.parse +from http import HTTPStatus +from http.server import SimpleHTTPRequestHandler + +from iterators import TimeoutIterator + +from mylogger import logger + +# Find the frontend folder (optional, but recommended) +FILE_DIR = os.path.dirname(__file__) +FRONTEND_BASE_PATH = os.getenv('FRONTEND_BASE_PATH', os.path.join(FILE_DIR, 'frontend')) +if not os.path.exists(FRONTEND_BASE_PATH): + if os.path.exists(os.path.join(FILE_DIR, '..', 'dist')): # Fallback to dev build + FRONTEND_BASE_PATH = os.path.join(FILE_DIR, '..', 'dist') + else: + logger.warning('Frontend not found at %s', FRONTEND_BASE_PATH) + FRONTEND_BASE_PATH = None + +# Define the API paths (also available at the root path for simplicity) +UPDATES_API_PATH = '/api/updates' +OBJECTS_API_PATH = '/api/object' # /{name} + + +class HTTPHandler(SimpleHTTPRequestHandler): + yacv: 'yacv.YACV' + frontend_lock: threading.Lock # To avoid exiting too early while frontend makes requests + at_least_one_client: threading.Event + + def __init__(self, *args, yacv: 'yacv.YACV', **kwargs): + self.yacv = yacv + self.frontend_lock = threading.Lock() + self.at_least_one_client = threading.Event() + super().__init__(*args, **kwargs, directory=FRONTEND_BASE_PATH) + + def log_message(self, fmt, *args): + logger.debug(fmt, *args) + + def end_headers(self): + # Add CORS headers to the response + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') + super().end_headers() + + def translate_path(self, path: str) -> str: + """Translate a path to the local filesystem, adds some basic security checks""" + path = super().translate_path(path) + path = os.path.realpath(path) # Avoid symlink hacks + if self.directory: # Ensure proper subdirectory + base = os.path.abspath(self.directory) + if not os.path.abspath(path).startswith(base): + self.send_error(HTTPStatus.FORBIDDEN, "Path is not in the frontend directory") + return '' + return path + + def send_head(self): + path_parts = self.path.split('?', 1) + if len(path_parts) == 1: + path_parts.append('') + [path, query_str] = path_parts + query = urllib.parse.parse_qs(query_str) + if path == UPDATES_API_PATH or path == '/' and query.get('api_updates') is not None: + return self._api_updates() + elif path.startswith(OBJECTS_API_PATH) or path == '/' and query.get('api_object') is not None: + if path.startswith(OBJECTS_API_PATH): + obj_name = self.path[len(OBJECTS_API_PATH) + 1:] + else: + obj_name = query.get('api_object').pop() + return self._api_object(obj_name) + elif path.endswith('/'): # Frontend index.html + self.path += 'index.html' + return super().send_head() + else: # Normal frontend file + return super().send_head() + + def _api_updates(self): + """Handles a publish-only websocket connection that send show_object events along with their hashes and URLs""" + self.send_response(HTTPStatus.OK) + self.send_header("Content-Type", "text/event-stream") + self.send_header("Cache-Control", "no-cache") + # Chunked transfer encoding! + self.send_header("Transfer-Encoding", "chunked") + self.end_headers() + self.at_least_one_client.set() + logger.debug('Updates client connected') + + def write_chunk(_chunk_data: str): + self.wfile.write(hex(len(_chunk_data))[2:].encode('utf-8')) + self.wfile.write(b'\r\n') + self.wfile.write(_chunk_data.encode('utf-8')) + self.wfile.write(b'\r\n') + self.wfile.flush() + + write_chunk('retry: 100\n\n') + + # Send buffered events first, while keeping a lock + with self.frontend_lock: + for data in self.yacv.show_events.buffer(): + logger.debug('Sending info about %s: %s', data.name, data) + # noinspection PyUnresolvedReferences + to_send = data.to_json() + write_chunk(f'data: {to_send}\n\n') + + # Send future events over the same connection + # Also send keep-alive to know if the client is still connected + subscription = self.yacv.show_events.subscribe(include_buffered=False) + it = TimeoutIterator(subscription, sentinel=None, reset_on_next=True, timeout=5.0) # Keep-alive interval + try: + for data in it: + if data is None: + write_chunk(':keep-alive\n\n') + else: + logger.debug('Sending info about %s: %s', data.name, data) + # noinspection PyUnresolvedReferences + to_send = data.to_json() + write_chunk(f'data: {to_send}\n\n') + for i in range(200): # Need to fill browser buffers for instant updates! + write_chunk(':flush\n\n') + except BrokenPipeError: # Client disconnected normally + pass + finally: + it.interrupt() + subscription.close() + logger.debug('Updates client disconnected') + + def _api_object(self, obj_name: str): + """Returns the object file with the matching name, building it if necessary.""" + with self.frontend_lock: + # Export the object (or fail if not found) + exported_glb = self.yacv.export(obj_name) + if exported_glb is None: + self.send_error(HTTPStatus.NOT_FOUND, f'Object {obj_name} not found') + return io.BytesIO() + + # Wrap the GLB in a response and return it + self.send_response(HTTPStatus.OK) + self.send_header('Content-Type', 'model/gltf-binary') + self.send_header('Content-Length', str(len(exported_glb))) + self.send_header('Content-Disposition', f'attachment; filename="{obj_name}.glb"') + self.end_headers() + self.wfile.write(exported_glb) diff --git a/yacv_server/plugin.py b/yacv_server/plugin.py index 8e03648..2581f8e 100644 --- a/yacv_server/plugin.py +++ b/yacv_server/plugin.py @@ -1 +1 @@ -# TODO: Plugins that can freely modify the GLTF file as it is being built +# TODO(if there is interest): Plugins that can freely modify the GLTF file as it is being built diff --git a/yacv_server/pubsub.py b/yacv_server/pubsub.py index 60e6a60..42b1891 100644 --- a/yacv_server/pubsub.py +++ b/yacv_server/pubsub.py @@ -1,6 +1,8 @@ -import asyncio +import threading +import queue +import threading from typing import List, TypeVar, \ - Generic, AsyncGenerator + Generic, Generator from yacv_server.mylogger import logger @@ -8,61 +10,74 @@ T = TypeVar('T') class BufferedPubSub(Generic[T]): - """A simple implementation of publish-subscribe pattern using asyncio and buffering all previous events""" + """A simple implementation of publish-subscribe pattern using threading and buffering all previous events""" _buffer: List[T] - _subscribers: List[asyncio.Queue[T]] - _lock = asyncio.Lock() - max_buffer_size = 1000 + _buffer_lock: threading.Lock + _subscribers: List[queue.Queue[T]] + _subscribers_lock: threading.Lock + max_buffer_size: int - def __init__(self): + def __init__(self, max_buffer_size: int = 100): self._buffer = [] + self._buffer_lock = threading.Lock() self._subscribers = [] + self._subscribers_lock = threading.Lock() + self.max_buffer_size = max_buffer_size - def publish_nowait(self, event: T): + def publish(self, event: T): """Publishes an event without blocking (synchronous API does not require locking)""" - self._buffer.append(event) - if len(self._buffer) > self.max_buffer_size: - self._buffer.pop(0) - for q in self._subscribers: - q.put_nowait(event) + with self._buffer_lock: + self._buffer.append(event) + if len(self._buffer) > self.max_buffer_size: + self._buffer.pop(0) + for q in self._subscribers: + q.put(event) - async def _subscribe(self, include_buffered: bool = True, include_future: bool = True) -> asyncio.Queue[T]: + def _subscribe(self, include_buffered: bool = True, include_future: bool = True) -> queue.Queue[T]: """Subscribes to events""" - q = asyncio.Queue() - async with self._lock: + q = queue.Queue() + with self._subscribers_lock: self._subscribers.append(q) logger.debug(f"Subscribed to %s (%d subscribers)", self, len(self._subscribers)) if include_buffered: - for event in self._buffer: - await q.put(event) + with self._buffer_lock: + for event in self._buffer: + q.put(event) if not include_future: - await q.put(None) + q.put(None) return q - async def _unsubscribe(self, q: asyncio.Queue[T]): + def _unsubscribe(self, q: queue.Queue[T]): """Unsubscribes from events""" - async with self._lock: + with self._subscribers_lock: self._subscribers.remove(q) logger.debug(f"Unsubscribed from %s (%d subscribers)", self, len(self._subscribers)) - async def subscribe(self, include_buffered: bool = True, include_future: bool = True) -> AsyncGenerator[T, None]: - """Subscribes to events as an async generator that yields events and automatically unsubscribes""" - q = await self._subscribe(include_buffered, include_future) + def subscribe(self, include_buffered: bool = True, include_future: bool = True) -> Generator[T, None, None]: + """Subscribes to events as an generator that yields events and automatically unsubscribes""" + q = self._subscribe(include_buffered, include_future) try: while True: - v = await q.get() + v = q.get() # include_future is incompatible with None values as they are used to signal the end of the stream if v is None and not include_future: break yield v finally: # When aclose() is called - await self._unsubscribe(q) + self._unsubscribe(q) def buffer(self) -> List[T]: """Returns a shallow copy of the list of buffered events""" - return self._buffer[:] + with self._buffer_lock: + return self._buffer[:] def delete(self, event: T): """Deletes an event from the buffer""" - self._buffer.remove(event) \ No newline at end of file + with self._buffer_lock: + self._buffer.remove(event) + + def clear(self): + """Clears the buffer""" + with self._buffer_lock: + self._buffer.clear() \ No newline at end of file diff --git a/yacv_server/server.py b/yacv_server/server.py deleted file mode 100644 index 280a0b5..0000000 --- a/yacv_server/server.py +++ /dev/null @@ -1,361 +0,0 @@ -import asyncio -import atexit -import os -import signal -import sys -import time -from dataclasses import dataclass -from threading import Thread -from typing import Optional, Dict, Union, Callable - -import aiohttp_cors -from OCP.TopLoc import TopLoc_Location -from OCP.TopoDS import TopoDS_Shape -from aiohttp import web -from aiohttp_sse import sse_response -from build123d import Shape, Axis, Location, Vector -from dataclasses_json import dataclass_json - -from yacv_server.cad import get_shape, grab_all_cad, image_to_gltf, CADLike -from yacv_server.mylogger import logger -from yacv_server.pubsub import BufferedPubSub -from yacv_server.tessellate import _hashcode, tessellate - -# Find the frontend folder (optional, but recommended) -FILE_DIR = os.path.dirname(__file__) -FRONTEND_BASE_PATH = os.getenv('FRONTEND_BASE_PATH', os.path.join(FILE_DIR, 'frontend')) -if not os.path.exists(FRONTEND_BASE_PATH): - if os.path.exists(os.path.join(FILE_DIR, '..', 'dist')): # Fallback to dev build - FRONTEND_BASE_PATH = os.path.join(FILE_DIR, '..', 'dist') - else: - logger.warning('Frontend not found at %s', FRONTEND_BASE_PATH) - FRONTEND_BASE_PATH = None - -# Define the API paths (also available at the root path for simplicity) -UPDATES_API_PATH = '/api/updates' -OBJECTS_API_PATH = '/api/object' # /{name} - - -@dataclass_json -@dataclass -class UpdatesApiData: - """Data sent to the client through the updates API""" - name: str - """Name of the object. Should be unique unless you want to overwrite the previous object""" - hash: str - """Hash of the object, to detect changes without rebuilding the object""" - - -class UpdatesApiFullData(UpdatesApiData): - obj: Optional[CADLike] - """The OCCT object, if any (not serialized)""" - kwargs: Optional[Dict[str, any]] - """The show_object options, if any (not serialized)""" - - def __init__(self, name: str, hash: str, obj: Optional[CADLike] = None, - kwargs: Optional[Dict[str, any]] = None): - self.name = name - self.hash = hash - self.obj = obj - self.kwargs = kwargs - - def to_json(self) -> str: - # noinspection PyUnresolvedReferences - return super().to_json() - - -# noinspection PyUnusedLocal -async def _index_handler(request: web.Request) -> web.Response: - return web.HTTPTemporaryRedirect(location='index.html') - - -class Server: - app = web.Application() - runner: web.AppRunner - thread: Optional[Thread] = None - startup_complete = asyncio.Event() - do_shutdown = asyncio.Event() - at_least_one_client = asyncio.Event() - show_events = BufferedPubSub[UpdatesApiFullData]() - object_events: Dict[str, BufferedPubSub[bytes]] = {} - object_events_lock = asyncio.Lock() - frontend_lock = asyncio.Lock() # To avoid exiting too early while frontend makes requests - - def __init__(self, *args, **kwargs): - # --- Routes --- - # - APIs - self.app.router.add_route('GET', f'{UPDATES_API_PATH}', self._api_updates) - self.app.router.add_route('GET', f'{OBJECTS_API_PATH}/{{name}}', self._api_object) - # - Single websocket/objects/frontend entrypoint to ease client configuration - self.app.router.add_get('/', self._entrypoint) - # - Static files from the frontend - self.app.router.add_get('/{path:(.*/|)}', _index_handler) # Any folder -> index.html - if FRONTEND_BASE_PATH is not None: - self.app.router.add_static('/', path=FRONTEND_BASE_PATH, name='static_frontend') - # --- CORS --- - cors = aiohttp_cors.setup(self.app, defaults={ - "*": aiohttp_cors.ResourceOptions( - allow_credentials=True, - expose_headers="*", - allow_headers="*", - ) - }) - for route in list(self.app.router.routes()): - cors.add(route) - # --- Misc --- - self.loop = asyncio.new_event_loop() - - def start(self): - """Starts the web server in the background""" - assert self.thread is None, "Server currently running, cannot start another one" - # Start the server in a separate daemon thread - self.thread = Thread(target=self._run_server, name='yacv_server', daemon=True) - signal.signal(signal.SIGINT | signal.SIGTERM, self.stop) - atexit.register(self.stop) - self.thread.start() - logger.info('Server started (requested)...') - # Wait for the server to be ready before returning - while not self.startup_complete.is_set(): - time.sleep(0.01) - logger.info('Server started (received)...') - - # noinspection PyUnusedLocal - def stop(self, *args): - """Stops the web server""" - if self.thread is None: - print('Cannot stop server because it is not running') - return - - graceful_secs_connect = float(os.getenv('YACV_GRACEFUL_SECS_CONNECT', 12.0)) - graceful_secs_request = float(os.getenv('YACV_GRACEFUL_SECS_REQUEST', 5.0)) - # Make sure we can hold the lock for more than 100ms (to avoid exiting too early) - logger.info('Stopping server (waiting for at least one frontend request first, cancel with CTRL+C)...') - start = time.time() - try: - while not self.at_least_one_client.is_set() and time.time() - start < graceful_secs_connect: - time.sleep(0.01) - except KeyboardInterrupt: - pass - - logger.info('Stopping server (waiting for no more frontend requests)...') - start = time.time() - while time.time() - start < graceful_secs_request: - if self.frontend_lock.locked(): - start = time.time() - time.sleep(0.01) - - # Stop the server in the background - self.loop.call_soon_threadsafe(lambda *a: self.do_shutdown.set()) - logger.info('Stopping server (sent)...') - - # Wait for the server to stop gracefully - self.thread.join(timeout=30) - self.thread = None - logger.info('Stopping server (confirmed)...') - if len(args) >= 1 and args[0] in (signal.SIGINT, signal.SIGTERM): - sys.exit(0) # Exit with success - - def _run_server(self): - """Runs the web server""" - asyncio.set_event_loop(self.loop) - self.loop.run_until_complete(self._run_server_async()) - self.loop.stop() - self.loop.close() - - async def _run_server_async(self): - """Runs the web server (async)""" - runner = web.AppRunner(self.app) - await runner.setup() - site = web.TCPSite(runner, os.getenv('YACV_HOST', 'localhost'), int(os.getenv('YACV_PORT', 32323))) - await site.start() - logger.info('Server started (sent)...') - self.startup_complete.set() - # Wait for a signal to stop the server while running - await self.do_shutdown.wait() - logger.info('Stopping server (received)...') - await runner.shutdown() - # await runner.cleanup() # Gets stuck? - logger.info('Stopping server (done)...') - - async def _entrypoint(self, request: web.Request) -> web.StreamResponse: - """Main entrypoint to the server, which automatically serves the frontend/updates/objects""" - if request.query.get('api_updates', '') != '': # ?api_updates -> updates API - return await self._api_updates(request) - elif request.query.get('api_object', '') != '': # ?api_object={name} -> object API - request.match_info['name'] = request.query['api_object'] - return await self._api_object(request) - else: # Anything else -> frontend index.html - return await _index_handler(request) - - async def _api_updates(self, request: web.Request) -> web.StreamResponse: - """Handles a publish-only websocket connection that send show_object events along with their hashes and URLs""" - self.at_least_one_client.set() - async with sse_response(request) as resp: - resp.ping_interval = 0.1 # HACK: Browsers don't receive instant updates without this - logger.debug('Client connected: %s', request.remote) - - # Send buffered events first, while keeping a lock - async with self.frontend_lock: - for data in self.show_events.buffer(): - logger.debug('Sending info about %s to %s: %s', data.name, request.remote, data) - # noinspection PyUnresolvedReferences - await resp.send(data.to_json()) - - # Send future events over the same connection - subscription = self.show_events.subscribe(include_buffered=False) - try: - async for data in subscription: - logger.debug('Sending info about %s to %s: %s', data.name, request.remote, data) - # noinspection PyUnresolvedReferences - await resp.send(data.to_json()) - finally: - await subscription.aclose() - logger.debug('Client disconnected: %s', request.remote) - - return resp - - obj_counter = 0 - - def _show_common(self, name: Optional[str], hash: str, start: float, obj: Optional[CADLike] = None, - kwargs=None): - name = name or f'object_{self.obj_counter}' - self.obj_counter += 1 - # Remove a previous object with the same name - for old_event in self.show_events.buffer(): - if old_event.name == name: - self.show_events.delete(old_event) - if name in self.object_events: - del self.object_events[name] - break - precomputed_info = UpdatesApiFullData(name=name, hash=hash, obj=obj, kwargs=kwargs or {}) - self.show_events.publish_nowait(precomputed_info) - logger.info('show_object(%s, %s) took %.3f seconds', name, hash, time.time() - start) - return precomputed_info - - def show(self, any_object: Union[bytes, CADLike, any], name: Optional[str] = None, **kwargs): - """Publishes "any" object to the server""" - if isinstance(any_object, bytes): - self.show_gltf(any_object, name, **kwargs) - else: - self.show_cad(any_object, name, **kwargs) - - def show_gltf(self, gltf: bytes, name: Optional[str] = None, **kwargs): - """Publishes any single-file GLTF object to the server.""" - start = time.time() - # Precompute the info and send it to the client as if it was a CAD object - precomputed_info = self._show_common(name, _hashcode(gltf, **kwargs), start, kwargs=kwargs) - # Also pre-populate the GLTF data for the object API - publish_to = BufferedPubSub[bytes]() - publish_to.publish_nowait(gltf) - publish_to.publish_nowait(b'') # Signal the end of the stream - self.object_events[precomputed_info.name] = publish_to - - def show_image(self, source: str | bytes, center: any, ppmm: int, name: Optional[str] = None, - save_mime: str = 'image/jpeg', **kwargs): - """Publishes an image as a quad GLTF object, indicating the center location and pixels per millimeter.""" - # Convert the image to a GLTF CAD object - gltf, name = image_to_gltf(source, center, ppmm, name, save_mime) - # Publish it like any other GLTF object - self.show_gltf(gltf, name, **kwargs) - - def show_cad(self, obj: Union[CADLike, any], name: Optional[str] = None, **kwargs): - """Publishes a CAD object to the server""" - start = time.time() - - # Get the shape of a CAD-like object - obj = get_shape(obj) - - # Convert Z-up (OCCT convention) to Y-up (GLTF convention) - if isinstance(obj, TopoDS_Shape): - obj = Shape(obj).rotate(Axis.X, -90).wrapped - elif isinstance(obj, TopLoc_Location): - tmp_location = Location(obj) - tmp_location.position = Vector(tmp_location.position.X, tmp_location.position.Z, -tmp_location.position.Y) - tmp_location.orientation = Vector(tmp_location.orientation.X - 90, tmp_location.orientation.Y, - tmp_location.orientation.Z) - obj = tmp_location.wrapped - - self._show_common(name, _hashcode(obj, **kwargs), start, obj, kwargs) - - def show_cad_all(self, **kwargs): - """Publishes all CAD objects to the server""" - for name, obj in grab_all_cad(): - self.show_cad(obj, name, **kwargs) - - async def _api_object(self, request: web.Request) -> web.Response: - """Returns the object file with the matching name, building it if necessary.""" - async with self.frontend_lock: - # Export the object (or fail if not found) - exported_glb = await self.export(request.match_info['name']) - - # Wrap the GLB in a response and return it - response = web.Response(body=exported_glb) - response.content_type = 'model/gltf-binary' - response.headers['Content-Disposition'] = f'attachment; filename="{request.match_info["name"]}.glb"' - return response - - def shown_object_names(self) -> list[str]: - """Returns the names of all objects that have been shown""" - return list([obj.name for obj in self.show_events.buffer()]) - - def _shown_object(self, name: str) -> Optional[UpdatesApiFullData]: - """Returns the object with the given name, if it exists""" - for obj in self.show_events.buffer(): - if obj.name == name: - return obj - return None - - async def export(self, name: str) -> bytes: - """Export the given previously-shown object to a single GLB file, building it if necessary.""" - start = time.time() - - # Check that the object to build exists and grab it if it does - event = self._shown_object(name) - if not event: - raise web.HTTPNotFound(text=f'No object named {name} was previously shown') - - # Use the lock to ensure that we don't build the object twice - async with self.object_events_lock: - # If there are no object events for this name, we need to build the object - if name not in self.object_events: - # Prepare the pubsub for the object - publish_to = BufferedPubSub[bytes]() - self.object_events[name] = publish_to - - def _build_object(): - # Build and publish the object (once) - gltf = tessellate(event.obj, tolerance=event.kwargs.get('tolerance', 0.1), - angular_tolerance=event.kwargs.get('angular_tolerance', 0.1), - faces=event.kwargs.get('faces', True), - edges=event.kwargs.get('edges', True), - vertices=event.kwargs.get('vertices', True)) - glb_list_of_bytes = gltf.save_to_bytes() - publish_to.publish_nowait(b''.join(glb_list_of_bytes)) - logger.info('export(%s) took %.3f seconds, %d parts', name, time.time() - start, - len(gltf.meshes[0].primitives)) - - # await asyncio.get_running_loop().run_in_executor(None, _build_object) - # The previous line has problems with auto-closed loop on script exit - # and is cancellable, so instead run blocking code in async context :( - logger.debug('Building object %s... %s', name, event.obj) - _build_object() - - # In either case return the elements of a subscription to the async generator - subscription = self.object_events[name].subscribe() - try: - return await anext(subscription) - finally: - await subscription.aclose() - - def export_all(self, folder: str, export_filter: Callable[[str, Optional[CADLike]], bool] = lambda name, obj: True): - """Export all previously-shown objects to GLB files in the given folder""" - import asyncio - - async def _export_all(): - os.makedirs(folder, exist_ok=True) - for name in self.shown_object_names(): - if export_filter(name, self._shown_object(name).obj): - with open(os.path.join(folder, f'{name}.glb'), 'wb') as f: - f.write(await self.export(name)) - - asyncio.run(_export_all()) diff --git a/yacv_server/yacv.py b/yacv_server/yacv.py new file mode 100644 index 0000000..285367a --- /dev/null +++ b/yacv_server/yacv.py @@ -0,0 +1,286 @@ +import atexit +import inspect +import os +import signal +import sys +import threading +import time +from dataclasses import dataclass +from http.server import ThreadingHTTPServer +from threading import Thread +from typing import Optional, Dict, Union, Callable + +from OCP.TopLoc import TopLoc_Location +from OCP.TopoDS import TopoDS_Shape +# noinspection PyProtectedMember +from build123d import Shape, Axis, Location, Vector +from dataclasses_json import dataclass_json + +from myhttp import HTTPHandler +from yacv_server.cad import get_shape, grab_all_cad, image_to_gltf, CADLike +from yacv_server.mylogger import logger +from yacv_server.pubsub import BufferedPubSub +from yacv_server.tessellate import _hashcode, tessellate + + +@dataclass_json +@dataclass +class UpdatesApiData: + """Data sent to the client through the updates API""" + name: str + """Name of the object. Should be unique unless you want to overwrite the previous object""" + hash: str + """Hash of the object, to detect changes without rebuilding the object""" + is_remove: bool + """Whether to remove the object from the scene""" + + +class UpdatesApiFullData(UpdatesApiData): + obj: Optional[CADLike] + """The OCCT object, if any (not serialized)""" + kwargs: Optional[Dict[str, any]] + """The show_object options, if any (not serialized)""" + + def __init__(self, name: str, _hash: str, is_remove: bool = False, obj: Optional[CADLike] = None, + kwargs: Optional[Dict[str, any]] = None): + self.name = name + self.hash = _hash + self.is_remove = is_remove + self.obj = obj + self.kwargs = kwargs + + def to_json(self) -> str: + # noinspection PyUnresolvedReferences + return super().to_json() + + +class YACV: + server_thread: Optional[Thread] + server: Optional[ThreadingHTTPServer] + startup_complete: threading.Event + show_events: BufferedPubSub[UpdatesApiFullData] + object_events: Dict[str, BufferedPubSub[bytes]] + object_events_lock: threading.Lock + + def __init__(self): + self.server_thread = None + self.server = None + self.startup_complete = threading.Event() + self.at_least_one_client = threading.Event() + self.show_events = BufferedPubSub() + self.object_events = {} + self.object_events_lock = threading.Lock() + self.frontend_lock = threading.Lock() + + def start(self): + """Starts the web server in the background""" + print('yacv>start') + assert self.server_thread is None, "Server currently running, cannot start another one" + assert self.startup_complete.is_set() is False, "Server already started" + # Start the server in a separate daemon thread + self.server_thread = Thread(target=self._run_server, name='yacv_server', daemon=True) + signal.signal(signal.SIGINT | signal.SIGTERM, self.stop) + atexit.register(self.stop) + self.server_thread.start() + logger.info('Server started (requested)...') + # Wait for the server to be ready before returning + while not self.startup_complete.wait(): + time.sleep(0.01) + logger.info('Server started (received)...') + + # noinspection PyUnusedLocal + def stop(self, *args): + """Stops the web server""" + if self.server_thread is None: + print('Cannot stop server because it is not running') + return + + graceful_secs_connect = float(os.getenv('YACV_GRACEFUL_SECS_CONNECT', 12.0)) + graceful_secs_request = float(os.getenv('YACV_GRACEFUL_SECS_REQUEST', 5.0)) + # Make sure we can hold the lock for more than 100ms (to avoid exiting too early) + logger.info('Stopping server (waiting for at least one frontend request first, cancel with CTRL+C)...') + start = time.time() + try: + while not self.at_least_one_client.wait( + graceful_secs_connect / 10) and time.time() - start < graceful_secs_connect: + time.sleep(0.01) + except KeyboardInterrupt: + pass + + logger.info('Stopping server (waiting for no more frontend requests)...') + start = time.time() + try: + while time.time() - start < graceful_secs_request: + if self.frontend_lock.locked(): + start = time.time() + time.sleep(0.01) + except KeyboardInterrupt: + pass + + # Stop the server in the background + self.server.shutdown() + logger.info('Stopping server (sent)...') + + # Wait for the server to stop gracefully + self.server_thread.join(timeout=30) + self.server_thread = None + logger.info('Stopping server (confirmed)...') + if len(args) >= 1 and args[0] in (signal.SIGINT, signal.SIGTERM): + sys.exit(0) # Exit with success + + def _run_server(self): + """Runs the web server""" + print('yacv>run_server', inspect.stack()) + logger.info('Starting server...') + self.server = ThreadingHTTPServer( + (os.getenv('YACV_HOST', 'localhost'), int(os.getenv('YACV_PORT', 32323))), + lambda a, b, c: HTTPHandler(a, b, c, yacv=self)) + # noinspection HttpUrlsUsage + logger.info(f'Serving at http://{self.server.server_name}:{self.server.server_port}') + self.startup_complete.set() + self.server.serve_forever() + + def _show_common(self, name: Optional[str], _hash: str, start: float, obj: Optional[CADLike] = None, + kwargs=None): + if kwargs.get('auto_clear', True): + self.clear() + name = name or f'object_{len(self.show_events.buffer())}' + # Remove a previous object with the same name + for old_event in self.show_events.buffer(): + if old_event.name == name: + self.show_events.delete(old_event) + if name in self.object_events: + del self.object_events[name] + break + precomputed_info = UpdatesApiFullData(name=name, _hash=_hash, obj=obj, kwargs=kwargs or {}) + self.show_events.publish(precomputed_info) + logger.info('show_object(%s, %s) took %.3f seconds', name, _hash, time.time() - start) + return precomputed_info + + def show(self, any_object: Union[bytes, CADLike, any], name: Optional[str] = None, **kwargs): + """Publishes "any" object to the server""" + if isinstance(any_object, bytes): + self.show_gltf(any_object, name, **kwargs) + else: + self.show_cad(any_object, name, **kwargs) + + def show_gltf(self, gltf: bytes, name: Optional[str] = None, **kwargs): + """Publishes any single-file GLTF object to the server.""" + start = time.time() + # Precompute the info and send it to the client as if it was a CAD object + precomputed_info = self._show_common(name, _hashcode(gltf, **kwargs), start, kwargs=kwargs) + # Also pre-populate the GLTF data for the object API + publish_to = BufferedPubSub[bytes]() + publish_to.publish(gltf) + publish_to.publish(b'') # Signal the end of the stream + self.object_events[precomputed_info.name] = publish_to + + def show_image(self, source: str | bytes, center: any, width: Optional[float] = None, + height: Optional[float] = None, name: Optional[str] = None, save_mime: str = 'image/jpeg', **kwargs): + """Publishes an image as a quad GLTF object, indicating the center location and pixels per millimeter.""" + # Convert the image to a GLTF CAD object + gltf, name = image_to_gltf(source, center, width, height, name, save_mime) + # Publish it like any other GLTF object + self.show_gltf(gltf, name, **kwargs) + + def show_cad(self, obj: Union[CADLike, any], name: Optional[str] = None, **kwargs): + """Publishes a CAD object to the server""" + start = time.time() + + # Get the shape of a CAD-like object + obj = get_shape(obj) + + # Convert Z-up (OCCT convention) to Y-up (GLTF convention) + if isinstance(obj, TopoDS_Shape): + obj = Shape(obj).rotate(Axis.X, -90).wrapped + elif isinstance(obj, TopLoc_Location): + tmp_location = Location(obj) + tmp_location.position = Vector(tmp_location.position.X, tmp_location.position.Z, + -tmp_location.position.Y) + tmp_location.orientation = Vector(tmp_location.orientation.X - 90, tmp_location.orientation.Y, + tmp_location.orientation.Z) + obj = tmp_location.wrapped + + self._show_common(name, _hashcode(obj, **kwargs), start, obj, kwargs) + + def show_cad_all(self, **kwargs): + """Publishes all CAD objects in the current scope to the server""" + for name, obj in grab_all_cad(): + self.show_cad(obj, name, **kwargs) + + def remove(self, name: str): + """Removes a previously-shown object from the scene""" + shown_object = self._shown_object(name) + if shown_object: + shown_object.is_remove = True + with self.object_events_lock: + if name in self.object_events: + del self.object_events[name] + self.show_events.publish(shown_object) + + def clear(self): + """Clears all previously-shown objects from the scene""" + for event in self.show_events.buffer(): + self.remove(event.name) + + def shown_object_names(self) -> list[str]: + """Returns the names of all objects that have been shown""" + return list([obj.name for obj in self.show_events.buffer()]) + + def _shown_object(self, name: str) -> Optional[UpdatesApiFullData]: + """Returns the object with the given name, if it exists""" + for obj in self.show_events.buffer(): + if obj.name == name: + return obj + return None + + def export(self, name: str) -> Optional[bytes]: + """Export the given previously-shown object to a single GLB file, building it if necessary.""" + start = time.time() + + # Check that the object to build exists and grab it if it does + event = self._shown_object(name) + if event is None: + return None + + # Use the lock to ensure that we don't build the object twice + with self.object_events_lock: + # If there are no object events for this name, we need to build the object + if name not in self.object_events: + # Prepare the pubsub for the object + publish_to = BufferedPubSub[bytes]() + self.object_events[name] = publish_to + + def _build_object(): + # Build and publish the object (once) + gltf = tessellate(event.obj, tolerance=event.kwargs.get('tolerance', 0.1), + angular_tolerance=event.kwargs.get('angular_tolerance', 0.1), + faces=event.kwargs.get('faces', True), + edges=event.kwargs.get('edges', True), + vertices=event.kwargs.get('vertices', True)) + glb_list_of_bytes = gltf.save_to_bytes() + publish_to.publish(b''.join(glb_list_of_bytes)) + logger.info('export(%s) took %.3f seconds, %d parts', name, time.time() - start, + len(gltf.meshes[0].primitives)) + + # await asyncio.get_running_loop().run_in_executor(None, _build_object) + # The previous line has problems with auto-closed loop on script exit + # and is cancellable, so instead run blocking code in async context :( + logger.debug('Building object %s... %s', name, event.obj) + _build_object() + + # In either case return the elements of a subscription to the async generator + subscription = self.object_events[name].subscribe() + try: + return next(subscription) + finally: + subscription.close() + + def export_all(self, folder: str, + export_filter: Callable[[str, Optional[CADLike]], bool] = lambda name, obj: True): + """Export all previously-shown objects to GLB files in the given folder""" + os.makedirs(folder, exist_ok=True) + for name in self.shown_object_names(): + if export_filter(name, self._shown_object(name).obj): + with open(os.path.join(folder, f'{name}.glb'), 'wb') as f: + f.write(self.export(name))