Compare commits

...

103 Commits

Author SHA1 Message Date
izzy lyseggen 5dde1bfcf1 Merge pull request #136 from specklesystems/izzy/stream-wrapper-fix
fix(wrapper): fix for nested branches
2021-11-17 16:12:42 +00:00
izzy lyseggen 82c9d874c9 test(branches): server now returns main first 2021-11-17 16:11:22 +00:00
izzy lyseggen 9acf2c8a92 fix(wrapper): fix for nested branches 2021-11-17 15:59:51 +00:00
Gergő Jedlicska 95012e60c1 Merge pull request #132 from specklesystems/commit-recieved
Commit recieved
2021-10-29 11:01:21 +02:00
Gergő Jedlicska 19b6500bbd Merge branch 'main' of github.com:specklesystems/specklepy into commit-recieved 2021-10-29 10:53:28 +02:00
Gergő Jedlicska 47a06e4630 version bump 2021-10-29 10:49:36 +02:00
Alan Rynne e5a8b40bb2 Merge pull request #131 from specklesystems/commit-recieved
commit recieved service implementation
2021-10-29 10:48:55 +02:00
Gergő Jedlicska 219456f5f8 circleci py310 is not working 2021-10-27 20:28:01 +02:00
Gergő Jedlicska d1544ae89f Merge branch 'commit-recieved' of github.com:specklesystems/specklepy into commit-recieved 2021-10-27 19:20:15 +02:00
Gergő Jedlicska 8f7d4b2ca7 add 3.10 as target 2021-10-27 19:20:04 +02:00
Gergő Jedlicska a7d31d4983 Delete stream_copy.py 2021-10-27 19:18:45 +02:00
Gergő Jedlicska a89b12a02c remove receive server test 2021-10-27 19:16:08 +02:00
Gergő Jedlicska 15ae68f5d7 commit received implementation 2021-10-27 14:13:49 +02:00
izzy lyseggen 0709cd99b5 Merge pull request #129 from specklesystems/izzy/token-auth
feat(wrapper/transport): token auth
2021-10-22 11:21:23 +02:00
izzy lyseggen faf06f7141 fix(server): set transport url 2021-10-21 13:12:19 +01:00
izzy lyseggen b54e09f811 test: server transport & wrapper token auth 2021-10-21 12:20:09 +01:00
izzy lyseggen 55b7e0d732 feat(wrapper): token auth for client and transport 2021-10-21 12:19:49 +01:00
izzy lyseggen 45c922679b feat(server): allow construction with token & url 2021-10-21 12:19:19 +01:00
izzy lyseggen b1c149382a Merge pull request #128 from specklesystems/izzy/cereal-fixes
fix(deserialisation): type check bug and brep encoding hotfix
2021-10-14 17:09:27 +02:00
izzy lyseggen 393e98c8c2 fix(encoding): add none unit type 2021-10-14 16:07:34 +01:00
izzy lyseggen 8376329cbb fix(base): type check error with optional generics
reported by rob on the forum:
https://speckle.community/t/issue-with-type-checking-in-pyhton/1861
2021-10-14 15:41:30 +01:00
izzy lyseggen 1567fe9e68 fix(breps): temp hotfix for curve encoding fail
addresses 🥒 Bug with brep receiving (curve encoding) #127
2021-10-14 15:38:51 +01:00
izzy lyseggen 364b826a1b Merge pull request #126 from specklesystems/izzy/metrics-hotfix
fix(metrics): typo in tracking send
2021-10-13 11:28:52 +02:00
izzy lyseggen 297dbab479 fix(metrics): typo in tracking send
i'm a dumdum
2021-10-13 10:27:58 +01:00
izzy lyseggen 81680ed766 Merge pull request #125 from specklesystems/izzy/metrics
metrics 🛰
2021-10-12 11:38:40 +02:00
izzy lyseggen c934720bb0 fix(metrics): try catch whole track method 2021-10-12 10:36:38 +01:00
izzy lyseggen 9297a5df49 feat(metrics): disable in tests 2021-10-11 18:10:53 +01:00
izzy lyseggen 7b8bf49769 feat(metrics): track creds, ops, and stream events 2021-10-11 18:10:33 +01:00
izzy lyseggen c834496b72 feat(metrics): add them! 🛰 2021-10-11 18:09:50 +01:00
izzy lyseggen f49491611f Merge pull request #124 from specklesystems/izzy/objects
fix(objects): quick geo and style edits
2021-10-04 09:39:55 +01:00
izzy lyseggen 19b83ba191 fix(objects): quick geo and style edits 2021-10-04 09:37:42 +01:00
izzy lyseggen 8d81aab1ac Merge pull request #123 from specklesystems/izzy/server-errors
fix(batch sender): improve error messages
2021-10-04 08:53:40 +01:00
izzy lyseggen 16868fbf3b fix(batch sender): improve error messages
for when send fails completely. previously user only got a
json decode error
2021-10-04 08:53:02 +01:00
Matteo Cominetti 00892fc838 Create close-issue.yml 2021-10-02 17:13:24 +01:00
Matteo Cominetti 4987b33de2 Create open-issue.yml 2021-10-02 17:13:09 +01:00
izzy lyseggen 766f1fa840 Merge pull request #120 from AntoineDao/geometry-chunks-serialization
Geometry chunks serialization
2021-09-30 11:59:28 +01:00
AntoineDao 69a5248abb fix(brep): fix Curve2DValues.setter 2021-09-19 10:08:57 +00:00
AntoineDao 5c93e4f9dc test(geometry): rewrite tests so they can fail
I noticed that the Curv2D setter had a bug which wasn't caught by tests so I rewrote them to fail first
2021-09-19 10:08:28 +00:00
AntoineDao e20b9b73c9 fix(brep): update vertices and trim values serialization
It turns out that those are not serialized like the other list attributes... Which is not super helpful or consistent but oh well :D
2021-09-18 22:54:58 +00:00
AntoineDao c06b20a963 style(comments): remove commented out code... 2021-09-18 21:56:59 +00:00
AntoineDao 5bc6b8c4ed style(brep): use getter/setter on XValues attributes 2021-09-18 21:54:58 +00:00
AntoineDao 3005e421a6 refactor(base): use get_serializable_attributes 2021-09-18 11:47:47 +00:00
AntoineDao 8fb03972d5 fix(serialization): fix some bugs I introduced by not testing before committing... 2021-09-18 11:29:00 +00:00
AntoineDao 02702190c9 fix(object): move array encoding from datachunk to its own class 2021-09-15 23:30:59 +01:00
AntoineDao 2bd31ae954 style(lint): clean up code and update/re-order imports 2021-09-15 23:00:41 +01:00
AntoineDao d0f8f95e4e feat(brep): use list serializers 2021-09-12 17:43:12 +00:00
AntoineDao fc3ae3b98e feat(base): add 'ignore_serialize' attribute
This enables users to specify attributes which should not be included in object serialization but should still be public members
2021-09-12 17:42:00 +00:00
AntoineDao a6b19025e6 feat(base): add data chunk encoding/decoding methods
These are helpful to iterate over chunked lists of encoded geometry objects and decode them into base objects (and vice versa)
2021-09-12 17:38:56 +00:00
AntoineDao 2be82f0874 feat(geometry): add list encoding serialization
Most geometry objects can be encoded and decoded from list of floats that look like protobuffs in C#. This commit simply reproduces those methods
2021-09-12 17:36:40 +00:00
izzy lyseggen 70191b97a2 docs: update readme to align with new format 2021-09-03 16:20:15 +01:00
izzy lyseggen dd2825272d docs: update readme to align with new format 2021-09-03 16:18:18 +01:00
izzy lyseggen 9303af6827 Merge pull request #118 from specklesystems/izzy/simpler-typing
feat(base): remove pydantic and roll our own type checking
2021-09-02 12:46:18 +01:00
izzy lyseggen 973dc07d5b fix(base): little tweaks 2021-08-24 15:50:08 +01:00
izzy lyseggen 7dd5b7a2a1 test(base): type checks 2021-08-24 12:11:46 +01:00
izzy lyseggen f259f256c7 feat(base): smol get_member fixes and others 2021-08-24 11:50:23 +01:00
izzy lyseggen 08986056a3 test(obj): quick fix 2021-08-20 13:29:21 +01:00
izzy lyseggen f89b07eacb feat(🥣): custom base types and test fixes 2021-08-20 13:09:09 +01:00
izzy lyseggen c973d916b3 fix(base): py 3.6 typing fix 2021-08-20 12:10:52 +01:00
izzy lyseggen 4ff6288317 feat(🥣): faster get_member_names 2021-08-20 11:56:04 +01:00
izzy lyseggen 8566674f2e Merge pull request #119 from specklesystems/izzy/cred-fix
fix(credentials): fix in get client
2021-08-19 18:28:09 +01:00
izzy lyseggen 1f3b6da9c7 fix(credentials): fix in get client
oopsie think this was a merge error
2021-08-19 18:26:44 +01:00
izzy lyseggen 5d99d5fcad feat(serialisation): swap out json for ujson 2021-08-19 18:25:12 +01:00
izzy lyseggen 4fc07f33d0 fix(base): try fix for 3.7 and 3.6 2021-08-19 17:52:22 +01:00
izzy lyseggen 4e23a69b89 fix(base): chunk fixes 2021-08-19 17:44:50 +01:00
izzy lyseggen 04a0ddc8c4 fix(api): obj receive change w new base 2021-08-19 17:33:41 +01:00
izzy lyseggen 1b4d43e0aa fix(base): add init w kwargs 2021-08-19 16:01:00 +01:00
izzy lyseggen f78c8c407f docs(base): docstring for of_type 2021-08-19 15:50:45 +01:00
izzy lyseggen 892c11f38f fix(base): remove unused init 2021-08-19 14:13:43 +01:00
izzy lyseggen 72639bf4bb fix(base): bypass for setting speckle_type on base 2021-08-19 13:58:25 +01:00
izzy lyseggen b2c210abc1 fix(serialiser): pop speckle_type from obj dict 2021-08-19 13:54:23 +01:00
izzy lyseggen 2250e8a897 fix(wrapper): bug in get_client 2021-08-19 13:53:58 +01:00
izzy lyseggen cb07f55551 feat(base): remove pydantic and diy the type check
- also improves performance by moving adding chunkables/detachables to
  the init_subclass hook
- all inits in the geo classes have been removed
- type checking is enforced on setting attributes from the `Base` class
    - unrecognised types are ignored (no type checking)
    - generics are checked for the generic only, not for the args
    - ints and strs are attempted to be parsed as floats,
      but not the other way around
2021-08-19 12:25:03 +01:00
izzy lyseggen d1b3d5e25e Merge pull request #117 from specklesystems/izzy/tiny-fix
fix(objects): init polyline value w empty list
2021-08-12 11:51:34 +01:00
izzy lyseggen 79cca557f5 fix(objects): init polyline value w empty list
soz!
2021-08-12 11:49:41 +01:00
izzy lyseggen 1e6e66a90a Merge pull request #116 from specklesystems/izzy/commit-spec
feat(client): add `branchName` to commit model
2021-08-11 09:24:49 +01:00
izzy lyseggen 09d84cf64a feat(client): add branchName to commit model 2021-08-11 09:21:04 +01:00
izzy lyseggen 3ccb0ae2a8 ci: try diff env tag variable 2021-08-10 15:49:08 +01:00
izzy lyseggen 6028a38355 Merge pull request #115 from specklesystems/ci/tags-and-versions
ci: fix workflows and patch version with git tag
2021-08-10 15:42:42 +01:00
izzy lyseggen 07418cfc9c ci: rename test job 2021-08-10 15:41:03 +01:00
izzy lyseggen 1ada797d81 ci: rename test job 2021-08-10 15:40:32 +01:00
izzy lyseggen 73703f6237 ci: patch version with git tag 2021-08-10 15:38:02 +01:00
izzy lyseggen 7644af22df fix(ci): add tag filter to build job 2021-08-10 15:37:19 +01:00
izzy lyseggen 564e1d4432 Merge pull request #114 from specklesystems/izzy/stream-wrapper-update
feat(wrapper): add get acct helper
2021-08-10 12:34:08 +01:00
izzy lyseggen fc4511ad02 chore: bump version 2021-08-10 12:03:06 +01:00
izzy lyseggen ad710b72da feat(wrapper): add acct helper 2021-08-10 12:02:42 +01:00
izzy lyseggen 041d9f56ce ci: another workflow fix 🙃 2021-08-06 17:13:16 +01:00
izzy lyseggen e1c0b705ad ci: build in deploy fix 2021-08-06 17:10:55 +01:00
izzy lyseggen 7b011b1122 ci: require build in deploy step 2021-08-06 17:06:42 +01:00
izzy lyseggen 3f09cd9d77 chore: bump version 2021-08-06 17:02:19 +01:00
izzy lyseggen 29a361892b Merge pull request #113 from specklesystems/izzy/stream-wrapper
🌯 Stream Wrapper with Client & Server Transport helper methods
2021-08-06 16:54:53 +01:00
izzy lyseggen 2672b40aff ci: remove speckle-server version 2021-08-06 16:53:09 +01:00
izzy lyseggen 35b6911b27 ci: quick fix 2021-08-06 16:43:53 +01:00
izzy lyseggen a4f0a2cc2b feat(server): transport init exception if no token 2021-08-06 16:39:02 +01:00
izzy lyseggen 1970890ecc test: stream wrapper tests 2021-08-06 16:38:40 +01:00
izzy lyseggen 13df5135b8 feat(credentials): stream wrapper!
can provide client and transport for you
2021-08-06 16:38:10 +01:00
izzy lyseggen 4e206b5c60 style: formatting 2021-08-06 16:09:12 +01:00
izzy lyseggen e696091555 feat(client): add string repr 2021-08-06 16:08:46 +01:00
izzy lyseggen a512dbb4e4 feat(logging): add SpeckleWarning class 2021-08-06 16:08:18 +01:00
izzy lyseggen 9a1f28516d chore: bump version to 2.2.5 2021-07-30 11:35:43 +01:00
izzy lyseggen 92892b83d8 Merge pull request #110 from specklesystems/izzy/sql-fix
fix(sqlite): delay and try except transport init
2021-07-30 11:31:55 +01:00
izzy lyseggen 8904e9eeb4 docs(ops): note that sqlite is default for receive 2021-07-30 11:31:18 +01:00
izzy lyseggen 68dc1794ee fix(sqlite): try catch initialisation of transport 2021-07-30 11:25:35 +01:00
izzy lyseggen 7e7940f25b refactor(ops): delay init of SQLiteTransport 2021-07-30 11:25:02 +01:00
34 changed files with 2183 additions and 330 deletions
+11 -8
View File
@@ -4,7 +4,7 @@ orbs:
python: circleci/python@1.3.2
jobs:
build:
test:
docker:
- image: "cimg/python:<<parameters.tag>>"
- image: "circleci/node:12"
@@ -14,7 +14,7 @@ jobs:
POSTGRES_DB: speckle2_test
POSTGRES_PASSWORD: speckle
POSTGRES_USER: speckle
- image: "speckle/speckle-server:5f8cf11cba07ea6a54000243f9cb343b61cbba13"
- image: "speckle/speckle-server"
command: ["bash", "-c", "/wait && node bin/www"]
environment:
POSTGRES_URL: "localhost"
@@ -38,27 +38,30 @@ jobs:
name: upgrade pip
- python/install-packages:
pkg-manager: poetry
- run: poetry run pytest --version
- run: poetry run pytest
deploy:
docker:
- image: "circleci/python:3.8"
steps:
- checkout
- run: python patch_version.py $CIRCLE_TAG
- run: poetry build
- run: poetry publish -u specklesystems -p $PYPI_PASSWORD
workflows:
main:
jobs:
- build:
jobs:
- test:
matrix:
parameters:
tag: ["3.6", "3.7", "3.8", "3.9"]
publish:
jobs:
filters:
tags:
only: /.*/
- deploy:
requires:
- test
filters:
tags:
only: /[0-9]+(\.[0-9]+)*/
+78
View File
@@ -0,0 +1,78 @@
name: Update issue Status
on:
issues:
types: [closed]
jobs:
update_issue:
runs-on: ubuntu-latest
steps:
- name: Get project data
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ORGANIZATION: specklesystems
PROJECT_NUMBER: 9
run: |
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
query($org: String!, $number: Int!) {
organization(login: $org){
projectNext(number: $number) {
id
fields(first:20) {
nodes {
id
name
settings
}
}
}
}
}' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
echo "$PROJECT_ID"
echo "$STATUS_FIELD_ID"
echo 'DONE_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .settings | fromjson | .options[] | select(.name== "Done") | .id' project_data.json) >> $GITHUB_ENV
echo "$DONE_ID"
- name: Add Issue to project #it's already in the project, but we do this to get its node id!
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ISSUE_ID: ${{ github.event.issue.node_id }}
run: |
item_id="$( gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
mutation($project:ID!, $id:ID!) {
addProjectNextItem(input: {projectId: $project, contentId: $id}) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
- name: Update Status
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ISSUE_ID: ${{ github.event.issue.node_id }}
run: |
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
mutation($project:ID!, $status:ID!, $id:ID!, $value:String!) {
set_status: updateProjectNextItemField(
input: {
projectId: $project
itemId: $id
fieldId: $status
value: $value
}
) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f status=$STATUS_FIELD_ID -f id=$ITEM_ID -f value=${{ env.DONE_ID }}
+50
View File
@@ -0,0 +1,50 @@
name: Move new issues into Project
on:
issues:
types: [opened]
jobs:
track_issue:
runs-on: ubuntu-latest
steps:
- name: Get project data
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ORGANIZATION: specklesystems
PROJECT_NUMBER: 9
run: |
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
query($org: String!, $number: Int!) {
organization(login: $org){
projectNext(number: $number) {
id
fields(first:20) {
nodes {
id
name
settings
}
}
}
}
}' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
- name: Add Issue to project
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ISSUE_ID: ${{ github.event.issue.node_id }}
run: |
item_id="$( gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
mutation($project:ID!, $id:ID!) {
addProjectNextItem(input: {projectId: $project, contentId: $id}) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
+2
View File
@@ -1,3 +1,5 @@
.tool-versions
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
+44 -111
View File
@@ -1,17 +1,53 @@
# speckle-py 🥧
<h1 align="center">
<img src="https://user-images.githubusercontent.com/2679513/131189167-18ea5fe1-c578-47f6-9785-3748178e4312.png" width="150px"/><br/>
Speckle | specklepy 🐍
</h1>
<h3 align="center">
The Python SDK
</h3>
<p align="center"><b>Speckle</b> is the data infrastructure for the AEC industry.</p><br/>
[![Twitter Follow](https://img.shields.io/twitter/follow/SpeckleSystems?style=social)](https://twitter.com/SpeckleSystems) [![Community forum users](https://img.shields.io/discourse/users?server=https%3A%2F%2Fdiscourse.speckle.works&style=flat-square&logo=discourse&logoColor=white)](https://discourse.speckle.works) [![website](https://img.shields.io/badge/https://-speckle.systems-royalblue?style=flat-square)](https://speckle.systems) [![docs](https://img.shields.io/badge/docs-speckle.guide-orange?style=flat-square&logo=read-the-docs&logoColor=white)](https://speckle.guide/dev/)
<p align="center"><a href="https://twitter.com/SpeckleSystems"><img src="https://img.shields.io/twitter/follow/SpeckleSystems?style=social" alt="Twitter Follow"></a> <a href="https://speckle.community"><img src="https://img.shields.io/discourse/users?server=https%3A%2F%2Fspeckle.community&amp;style=flat-square&amp;logo=discourse&amp;logoColor=white" alt="Community forum users"></a> <a href="https://speckle.systems"><img src="https://img.shields.io/badge/https://-speckle.systems-royalblue?style=flat-square" alt="website"></a> <a href="https://speckle.guide/dev/"><img src="https://img.shields.io/badge/docs-speckle.guide-orange?style=flat-square&amp;logo=read-the-docs&amp;logoColor=white" alt="docs"></a></p>
<p align="center"><a href="https://github.com/specklesystems/specklepy/"><img src="https://circleci.com/gh/specklesystems/specklepy.svg?style=svg&amp;circle-token=76eabd350ea243575cbb258b746ed3f471f7ac29" alt="Speckle-Next"></a> </p>
## Introduction
# About Speckle
> ⚠ This is the start of the Python client for Speckle 2.0. It is currently quite nebulous and may be trashed and rebuilt at any moment! It is compatible with Python 3.6+ ⚠
>
What is Speckle? Check our ![YouTube Video Views](https://img.shields.io/youtube/views/B9humiSpHzM?label=Speckle%20in%201%20minute%20video&style=social)
## Documentation
### Features
Comprehensive developer and user documentation can be found in our:
- **Object-based:** say goodbye to files! Speckle is the first object based platform for the AEC industry
- **Version control:** Speckle is the Git & Hub for geometry and BIM data
- **Collaboration:** share your designs collaborate with others
- **3D Viewer:** see your CAD and BIM models online, share and embed them anywhere
- **Interoperability:** get your CAD and BIM models into other software without exporting or importing
- **Real time:** get real time updates and notifications and changes
- **GraphQL API:** get what you need anywhere you want it
- **Webhooks:** the base for a automation and next-gen pipelines
- **Built for developers:** we are building Speckle with developers in mind and got tools for every stack
- **Built for the AEC industry:** Speckle connectors are plugins for the most common software used in the industry such as Revit, Rhino, Grasshopper, AutoCAD, Civil 3D, Excel, Unreal Engine, Unity, QGIS, Blender and more!
#### 📚 [Speckle Docs website](https://speckle.guide/dev/)
### Try Speckle now!
Give Speckle a try in no time by:
- [![speckle XYZ](https://img.shields.io/badge/https://-speckle.xyz-0069ff?style=flat-square&logo=hackthebox&logoColor=white)](https://speckle.xyz) ⇒ creating an account at our public server
- [![create a droplet](https://img.shields.io/badge/Create%20a%20Droplet-0069ff?style=flat-square&logo=digitalocean&logoColor=white)](https://marketplace.digitalocean.com/apps/speckle-server?refcode=947a2b5d7dc1) ⇒ deploying an instance in 1 click
### Resources
- [![Community forum users](https://img.shields.io/badge/community-forum-green?style=for-the-badge&logo=discourse&logoColor=white)](https://speckle.community) for help, feature requests or just to hang with other speckle enthusiasts, check out our community forum!
- [![website](https://img.shields.io/badge/tutorials-speckle.systems-royalblue?style=for-the-badge&logo=youtube)](https://speckle.systems) our tutorials portal is full of resources to get you started using Speckle
- [![docs](https://img.shields.io/badge/docs-speckle.guide-orange?style=for-the-badge&logo=read-the-docs&logoColor=white)](https://speckle.guide/dev/) reference on almost any end-user and developer functionality
# Repo structure
## Usage
Send and receive data from a Speckle Server with `operations`, interact with the Speckle API with the `SpeckleClient`, create and extend your own custom Speckle Objects with `Base`, and more!
Head to the [**📚 specklepy docs**](https://speckle.guide/dev/python.html) for more information and usage examples.
## Developing & Debugging
@@ -34,109 +70,6 @@ It may be helpful to know where the local accounts and object cache dbs are stor
- Linux: `$XDG_DATA_HOME` or by default `~/.local/share/Speckle`
- Mac: `~/.config/Speckle`
## Overview of functionality
The `SpeckleClient` is the entry point for interacting with the GraphQL API. You'll need to have a running server to use this.
```py
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import get_default_account, get_local_accounts
all_accounts = get_local_accounts() # get back a list
account = get_default_account()
client = SpeckleClient(host="speckle.xyz")
# client = SpeckleClient(host="yourserver.com") or whatever your host is
client.authenticate(account.token)
```
Interacting with streams is meant to be intuitive and evocative of PySpeckle 1.0
```py
# get your streams
stream_list = client.stream.list()
# search your streams
results = client.user.search("mech")
# create a stream
new_stream_id = client.stream.create(name="a shiny new stream")
# get a stream
new_stream = client.stream.get(id=new_stream_id)
```
New in 2.0: commits! Here are some basic commit interactions.
```py
# get list of commits
commits = client.commit.list("stream id")
# get a specific commit
commit = client.commit.get("stream id", "commit id")
# create a commit
commit_id = client.commit.create("stream id", "object id", "this is a commit message to describe the commit")
# delete a commit
deleted = client.commit.delete("stream id", "commit id")
```
The `BaseObjectSerializer` is used for decomposing and serializing `Base` objects so they can be sent / received to the server. You can use it directly to get the id (hash) and a serializable object representation of the decomposed `Base`. You can learn more about the Speckle `Base` object [here](https://discourse.speckle.works/t/core-2-0-the-base-object/782) and the decomposition API [here](https://discourse.speckle.works/t/core-2-0-decomposition-api/911).
```py
from specklepy.objects.base import Base
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
detached_base = Base()
detached_base.name = "this will get detached"
base_obj = Base()
base_obj.name = "my base"
base_obj["@nested"] = detached_base
serializer = BaseObjectSerializer()
hash, obj_dict = serializer.traverse_base(base_obj)
```
If you use the `operations`, you will not need to interact with the serializer directly as this will be taken care of for you. You will just need to provide a transport to indicate where the objects should be sent / received from. At the moment, just the `MemoryTransport` and the `ServerTransport` are fully functional at the moment. If you'd like to learn more about Transports in Speckle 2.0, have a look [here](https://discourse.speckle.works/t/core-2-0-transports/919).
```py
from specklepy.transports.memory import MemoryTransport
from specklepy.api import operations
transport = MemoryTransport()
# this serialises the object and sends it to the transport
hash = operations.send(base=base_obj, transports=[transport])
# if the object had detached objects, you can see these as well
saved_objects = transport.objects # a dict with the obj hash as the key
# this receives and object from the given transport, deserialises it, and recomposes it into a base object
received_base = operations.receive(obj_id=hash, remote_transport=transport)
```
You can also use the GraphQL API to send and receive objects.
```py
# create a test base object
test_base = Base()
test_base.testing = "a test base obj"
# run it through the serialiser
s = BaseObjectSerializer()
hash, obj = s.traverse_base(test_base)
# send it to the server
objCreate = client.object.create(stream_id="stream id", objects=[obj])
received_base = client.object.get("stream id", hash)
```
This doc is not complete - there's more to see so have a dive into the code and play around! Please feel free to provide feedback, submit issues, or discuss new features ✨
## Contributing
Please make sure you read the [contribution guidelines](.github/CONTRIBUTING.md) for an overview of the best practices we try to follow.
+31
View File
@@ -0,0 +1,31 @@
import re
import sys
def patch(tag):
print(f"Patching version: {tag}")
with open("pyproject.toml", "r") as f:
lines = f.readlines()
if "version" not in lines[2]:
raise Exception(f"Invalid pyproject.toml. Could not patch version.")
lines[2] = f'version = "{tag}"\n'
with open("pyproject.toml", "w") as file:
file.writelines(lines)
def main():
if len(sys.argv) < 2:
return
tag = sys.argv[1]
if not re.match(r"[0-9]+(\.[0-9]+)*$", tag):
raise ValueError(f"Invalid tag provided: {tag}")
patch(tag)
if __name__ == "__main__":
main()
Generated
+32 -1
View File
@@ -371,6 +371,14 @@ category = "main"
optional = false
python-versions = "*"
[[package]]
name = "ujson"
version = "4.1.0"
description = "Ultra fast JSON encoder and decoder for Python"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "urllib3"
version = "1.26.5"
@@ -420,7 +428,7 @@ testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake
[metadata]
lock-version = "1.1"
python-versions = "^3.6.5"
content-hash = "84e846c1bb02924ceada07406e95032e0632d229a36657ba2b85129e68f1526d"
content-hash = "728db0014dfb8a83c50fe5ce6e86d068c4c87d319d50fb1e8135e63507713f30"
[metadata.files]
aiohttp = [
@@ -711,6 +719,29 @@ typing-extensions = [
{file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"},
{file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"},
]
ujson = [
{file = "ujson-4.1.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:148680f2bc6e52f71c56908b65f59b36a13611ac2f75a86f2cb2bce2b2c2588c"},
{file = "ujson-4.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1c2fb32976982e4e75ca0843a1e7b2254b8c5d8c45d979ebf2db29305b4fa31"},
{file = "ujson-4.1.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:971d4b450e689bfec8ad6b22060fb9b9bec1e0860dbdf0fa7cfe4068adbc5f58"},
{file = "ujson-4.1.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f453480b275192ae40ef350a4e8288977f00b02e504ed34245ebd12d633620cb"},
{file = "ujson-4.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f135db442e5470d9065536745968efc42a60233311c8509b9327bcd59a8821c7"},
{file = "ujson-4.1.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:2251fc9395ba4498cbdc48136a179b8f20914fa8b815aa9453b20b48ad120f43"},
{file = "ujson-4.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9005d0d952d0c1b3dff5cdb79df2bde35a3499e2de3f708a22c45bbb4089a1f6"},
{file = "ujson-4.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:117855246a9ea3f61f3b69e5ca1b1d11d622b3126f50a0ec08b577cb5c87e56e"},
{file = "ujson-4.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:989bed422e7e20c7ba740a4e1bbeb28b3b6324e04f023ea238a2e5449fc53668"},
{file = "ujson-4.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:44993136fd2ecade747b6db95917e4f015a3279e09a08113f70cbbd0d241e66a"},
{file = "ujson-4.1.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9e962df227fd1d851ff095382a9f8432c2470c3ee640f02ae14231dc5728e6f3"},
{file = "ujson-4.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be6013cda610c5149fb80a84ee815b210aa2e7fe4edf1d2bce42c02336715208"},
{file = "ujson-4.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:41b7e5422184249b5b94d1571206f76e5d91e8d721ce51abe341a88f41dd6692"},
{file = "ujson-4.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:807bb0585f30a650ec981669827721ed3ee1ee24f2c6f333a64982a40eb66b82"},
{file = "ujson-4.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:d2955dd5cce0e76ba56786d647aaedca2cebb75eda9f0ec1787110c3646751a8"},
{file = "ujson-4.1.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:a873c93d43f9bd14d9e9a6d2c6eb7aae4aad9717fe40c748d0cd4b6ed7767c62"},
{file = "ujson-4.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e8fe9bbeca130debb10eea7910433a0714c8efc057fad36353feccb87c1d07f"},
{file = "ujson-4.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:81a49dbf176ae041fc86d2da564f5b9b46faf657306035632da56ecfd7203193"},
{file = "ujson-4.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1fb2455e62f20ab4a6d49f78b5dc4ff99c72fdab9466e761120e9757fa35f4d7"},
{file = "ujson-4.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:44db30b8fc52e70a6f67def11804f74818addafef0a65cd7f0abb98b7830920f"},
{file = "ujson-4.1.0.tar.gz", hash = "sha256:22b63ec4409f0d2f2c4c9d5aa331997e02470b7a15a3233f3cc32f2f9b92d58c"},
]
urllib3 = [
{file = "urllib3-1.26.5-py2.py3-none-any.whl", hash = "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c"},
{file = "urllib3-1.26.5.tar.gz", hash = "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"},
+2 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "specklepy"
version = "2.2.4"
version = "2.4.0"
description = "The Python SDK for Speckle 2.0"
readme = "README.md"
authors = ["Speckle Systems <devops@speckle.systems>"]
@@ -15,6 +15,7 @@ python = "^3.6.5"
pydantic = "^1.7.3"
appdirs = "^1.4.4"
gql = {version = ">=3.0.0a6", extras = ["all"], allow-prereleases = true}
ujson = "^4.1.0"
[tool.poetry.dev-dependencies]
black = "^20.8b1"
+5
View File
@@ -82,6 +82,11 @@ class SpeckleClient:
except Exception as ex:
raise SpeckleException(f"{self.url} is not a compatible Speckle Server", ex)
def __repr__(self):
return (
f"SpeckleClient( server: {self.url}, authenticated: {self.me is not None} )"
)
def authenticate(self, token: str) -> None:
"""Authenticate the client using a personal access token
The token is saved in the client object and a synchronous GraphQL entrypoint is created
+151 -2
View File
@@ -1,9 +1,14 @@
import os
from typing import List, Optional
from warnings import warn
from pydantic import BaseModel
from typing import List, Optional
from urllib.parse import urlparse, unquote
from specklepy.logging import metrics
from specklepy.api.models import ServerInfo
from specklepy.api.client import SpeckleClient
from specklepy.transports.sqlite import SQLiteTransport
from specklepy.logging.exceptions import SpeckleException
from specklepy.transports.server.server import ServerTransport
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
class UserInfo(BaseModel):
@@ -37,6 +42,7 @@ def get_local_accounts(base_path: str = None) -> List[Account]:
Returns:
List[Account] -- list of all local accounts or an empty list if no accounts were found
"""
metrics.track(metrics.ACCOUNT_LIST)
account_storage = SQLiteTransport(scope="Accounts", base_path=base_path)
json_path = os.path.join(account_storage._base_path, "Accounts")
os.makedirs(json_path, exist_ok=True)
@@ -69,6 +75,7 @@ def get_default_account(base_path: str = None) -> Account:
Returns:
Account -- the default account or None if no local accounts were found
"""
metrics.track(metrics.ACCOUNT_DEFAULT)
accounts = get_local_accounts(base_path=base_path)
if not accounts:
return None
@@ -79,3 +86,145 @@ def get_default_account(base_path: str = None) -> Account:
default.isDefault = True
return default
class StreamWrapper:
"""
The `StreamWrapper` gives you some handy helpers to deal with urls and get authenticated clients and transports.
Construct a `StreamWrapper` with a stream, branch, commit, or object URL. The corresponding ids will be stored
in the wrapper. If you have local accounts on the machine, you can use the `get_account` and `get_client` methods
to get a local account for the server. You can also pass a token into `get_client` if you don't have a corresponding
local account for the server.
```py
from specklepy.api.credentials import StreamWrapper
# provide any stream, branch, commit, object, or globals url
wrapper = StreamWrapper("https://speckle.xyz/streams/3073b96e86/commits/604bea8cc6")
# get an authenticated SpeckleClient if you have a local account for the server
client = wrapper.get_client()
# get an authenticated ServerTransport if you have a local account for the server
transport = wrapper.get_transport()
```
"""
stream_url: str = None
use_ssl: bool = True
host: str = None
stream_id: str = None
commit_id: str = None
object_id: str = None
branch_name: str = None
client: SpeckleClient = None
account: Account = None
def __repr__(self):
return f"StreamWrapper( server: {self.host}, stream_id: {self.stream_id}, type: {self.type} )"
def __str__(self) -> str:
return self.__repr__()
@property
def type(self) -> str:
if self.object_id:
return "object"
elif self.commit_id:
return "commit"
elif self.branch_name:
return "branch"
else:
return "stream" if self.stream_id else "invalid"
def __init__(self, url: str) -> None:
metrics.track("streamwrapper")
self.stream_url = url
parsed = urlparse(url)
self.host = parsed.netloc
self.use_ssl = parsed.scheme == "https"
segments = parsed.path.strip("/").split("/", 3)
if not segments or len(segments) < 2:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - invalid URL provided."
)
while segments:
segment = segments.pop(0)
if segments and segment.lower() == "streams":
self.stream_id = segments.pop(0)
elif segments and segment.lower() == "commits":
self.commit_id = segments.pop(0)
elif segments and segment.lower() == "branches":
self.branch_name = unquote(segments.pop(0))
elif segments and segment.lower() == "objects":
self.object_id = segments.pop(0)
elif segment.lower() == "globals":
self.branch_name = "globals"
if segments:
self.commit_id = segments.pop(0)
else:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - invalid URL provided."
)
if not self.stream_id:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - no stream id found."
)
def get_account(self) -> Account:
"""
Gets an account object for this server from the local accounts db (added via Speckle Manager or a json file)
"""
if self.account:
return self.account
self.account = next(
(a for a in get_local_accounts() if self.host in a.serverInfo.url),
None,
)
return self.account
def get_client(self, token: str = None) -> SpeckleClient:
"""
Gets an authenticated client for this server. You may provide a token if there aren't any local accounts on this
machine. If no account is found and no token is provided, an unauthenticated client is returned.
Arguments:
token {str} -- optional token if no local account is available (defaults to None)
Returns:
SpeckleClient -- authenticated with a corresponding local account or the provided token
"""
if self.client and token is None:
return self.client
if not self.account:
self.get_account()
if not self.client:
self.client = SpeckleClient(host=self.host, use_ssl=self.use_ssl)
if self.account is None and token is None:
warn(f"No local account found for server {self.host}", SpeckleWarning)
return self.client
self.client.authenticate(self.account.token if self.account else token)
return self.client
def get_transport(self, token: str = None) -> ServerTransport:
"""
Gets a server transport for this stream using an authenticated client. If there is no local account for this
server and the client was not authenticated with a token, this will throw an exception.
Returns:
ServerTransport -- constructed for this stream with a pre-authenticated client
"""
if not self.client or not self.client.me:
self.get_client(token)
return ServerTransport(self.stream_id, self.client)
+1
View File
@@ -22,6 +22,7 @@ class Commit(BaseModel):
authorName: Optional[str]
authorId: Optional[str]
authorAvatar: Optional[str]
branchName: Optional[str]
createdAt: Optional[str]
sourceApplication: Optional[str]
referencedObject: Optional[str]
+11 -7
View File
@@ -1,8 +1,7 @@
import json
from typing import List
from specklepy.logging import metrics
from specklepy.objects.base import Base
from specklepy.transports.sqlite import SQLiteTransport
from specklepy.transports.server import ServerTransport
from specklepy.logging.exceptions import SpeckleException
from specklepy.transports.abstract_transport import AbstractTransport
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
@@ -23,6 +22,7 @@ def send(
Returns:
str -- the object id of the sent object
"""
metrics.track(metrics.SEND)
if not transports and not use_default_cache:
raise SpeckleException(
message="You need to provide at least one transport: cannot send with an empty transport list and no default cache"
@@ -52,12 +52,13 @@ def receive(
Arguments:
obj_id {str} -- the id of the object to receive
remote_transport {Transport} -- the transport to receive from
local_transport {Transport} -- the transport to send from
local_transport {Transport} -- the local cache to check for existing objects
(defaults to `SQLiteTransport`)
Returns:
Base -- the base object
"""
metrics.track(metrics.RECEIVE)
if not local_transport:
local_transport = SQLiteTransport()
@@ -92,14 +93,13 @@ def serialize(base: Base, write_transports: List[AbstractTransport] = []) -> str
Returns:
str -- the serialized object
"""
metrics.track(metrics.SERIALIZE)
serializer = BaseObjectSerializer(write_transports=write_transports)
return serializer.write_json(base)[1]
def deserialize(
obj_string: str, read_transport: AbstractTransport = SQLiteTransport()
) -> Base:
def deserialize(obj_string: str, read_transport: AbstractTransport = None) -> Base:
"""
Deserialize a string object into a Base object. If the object contains referenced child objects that are not stored in the local db, a read transport needs to be provided in order to recompose the base with the children objects.
@@ -111,6 +111,10 @@ def deserialize(
Returns:
Base -- the deserialized object
"""
metrics.track(metrics.DESERIALIZE)
if not read_transport:
read_transport = SQLiteTransport()
serializer = BaseObjectSerializer(read_transport=read_transport)
return serializer.read_json(obj_string=obj_string)
+8 -3
View File
@@ -1,9 +1,10 @@
from logging import error
from specklepy.logging.exceptions import GraphQLException, SpeckleException
from specklepy.transports.sqlite import SQLiteTransport
from typing import Dict, List
from gql.client import Client
from gql.gql import gql
from gql.transport.exceptions import TransportQueryError
from specklepy.logging.exceptions import GraphQLException, SpeckleException
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
class ResourceBase(object):
@@ -40,7 +41,11 @@ class ResourceBase(object):
if schema:
return schema.parse_obj(response)
elif self.schema:
return self.schema.parse_obj(response)
try:
return self.schema.parse_obj(response)
except:
s = BaseObjectSerializer(read_transport=SQLiteTransport())
return s.recompose_base(response)
else:
return response
+38 -1
View File
@@ -1,6 +1,5 @@
from typing import Optional, List
from gql import gql
from pydantic.main import BaseModel
from specklepy.api.resource import ResourceBase
from specklepy.api.models import Commit
@@ -40,6 +39,7 @@ class Resource(ResourceBase):
authorId
authorName
authorAvatar
branchName
createdAt
sourceApplication
totalChildrenCount
@@ -185,3 +185,40 @@ class Resource(ResourceBase):
return self.make_request(
query=query, params=params, return_type="commitDelete", parse_response=False
)
def received(
self,
stream_id: str,
commit_id: str,
source_application: str = "python",
message: Optional[str] = None,
) -> bool:
"""
Mark a commit object a received by the source application.
"""
query = gql(
"""
mutation CommitReceive($receivedInput:CommitReceivedInput!){
commitReceive(input:$receivedInput)
}
"""
)
params = {
"receivedInput": {
"sourceApplication": source_application,
"streamId": stream_id,
"commitId": commit_id,
"message": "message",
}
}
try:
return self.make_request(
query=query,
params=params,
return_type="commitReceive",
parse_response=False,
)
except Exception as ex:
print(ex.with_traceback)
return False
+10 -2
View File
@@ -1,7 +1,9 @@
from typing import Dict, List, Optional
from gql import gql
from specklepy.api.resource import ResourceBase
from typing import Dict, List, Optional
from specklepy.logging import metrics
from specklepy.api.models import Stream
from specklepy.api.resource import ResourceBase
NAME = "stream"
METHODS = [
@@ -35,6 +37,7 @@ class Resource(ResourceBase):
Returns:
Stream -- the retrieved stream
"""
metrics.track(metrics.STREAM_GET)
query = gql(
"""
query Stream($id: String!, $branch_limit: Int!, $commit_limit: Int!) {
@@ -90,6 +93,7 @@ class Resource(ResourceBase):
Returns:
List[Stream] -- A list of Stream objects
"""
metrics.track(metrics.STREAM_LIST)
query = gql(
"""
query User($stream_limit: Int!) {
@@ -147,6 +151,7 @@ class Resource(ResourceBase):
Returns:
id {str} -- the id of the newly created stream
"""
metrics.track(metrics.STREAM_CREATE)
query = gql(
"""
mutation StreamCreate($stream: StreamCreateInput!) {
@@ -177,6 +182,7 @@ class Resource(ResourceBase):
Returns:
bool -- whether the stream update was successful
"""
metrics.track(metrics.STREAM_UPDATE)
query = gql(
"""
mutation StreamUpdate($stream: StreamUpdateInput!) {
@@ -207,6 +213,7 @@ class Resource(ResourceBase):
Returns:
bool -- whether the deletion was successful
"""
metrics.track(metrics.STREAM_DELETE)
query = gql(
"""
mutation StreamDelete($id: String!) {
@@ -239,6 +246,7 @@ class Resource(ResourceBase):
Returns:
List[Stream] -- a list of Streams that match the search query
"""
metrics.track(metrics.STREAM_SEARCH)
query = gql(
"""
query StreamSearch($search_query: String!,$limit: Int!, $branch_limit:Int!, $commit_limit:Int!) {
+5
View File
@@ -28,3 +28,8 @@ class GraphQLException(SpeckleException):
def __str__(self) -> str:
return f"GraphQLException: {self.message}"
class SpeckleWarning(Warning):
def __init__(self, *args: object) -> None:
super().__init__(*args)
+125
View File
@@ -0,0 +1,125 @@
import os
import queue
import logging
import requests
import threading
from requests.sessions import session
from specklepy.transports.sqlite import SQLiteTransport
"""
Anonymous telemetry to help us understand how to make a better Speckle.
This really helps us to deliver a better open source project and product!
"""
TRACK = True
HOST_APP = "python"
LOG = logging.getLogger(__name__)
METRICS_TRACKER = None
# actions
RECEIVE = "receive"
SEND = "send"
STREAM_CREATE = "stream/create"
STREAM_GET = "stream/get"
STREAM_UPDATE = "stream/update"
STREAM_DELETE = "stream/delete"
STREAM_DETAILS = "stream/details"
STREAM_LIST = "stream/list"
STREAM_VIEW = "stream/view"
STREAM_SEARCH = "stream/search"
ACCOUNT_DEFAULT = "account/default"
ACCOUNT_DETAILS = "account/details"
ACCOUNT_LIST = "account/list"
SERIALIZE = "serialization/serialize"
DESERIALIZE = "serialization/deserialize"
def disable():
global TRACK
TRACK = False
def set_host_app(host_app: str):
global HOST_APP
HOST_APP = host_app
def track(action: str):
if not TRACK:
return
try:
global METRICS_TRACKER
if not METRICS_TRACKER:
METRICS_TRACKER = MetricsTracker()
page_params = {
"rec": 1,
"idsite": METRICS_TRACKER.site_id,
"uid": METRICS_TRACKER.suuid,
"action_name": action,
"url": f"http://connectors/{HOST_APP}/{action}",
"urlref": f"http://connectors/{HOST_APP}/{action}",
"_cvar": {"1": ["hostApplication", HOST_APP]},
}
event_params = {
"rec": 1,
"idsite": METRICS_TRACKER.site_id,
"uid": MetricsTracker.suuid,
"_cvar": {"1": ["hostApplication", HOST_APP]},
"e_c": HOST_APP,
"e_a": action,
}
METRICS_TRACKER.queue.put_nowait([event_params, page_params])
except Exception as ex:
# wrapping this whole thing in a try except as we never want a failure here to annoy users!
LOG.error("Error queueing metrics request: " + str(ex))
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class MetricsTracker(metaclass=Singleton):
matomo_url = "https://speckle.matomo.cloud/matomo.php"
site_id = 2
host_app = "python"
suuid = None
sending_thread = None
queue = queue.Queue(1000)
def __init__(self) -> None:
self.sending_thread = threading.Thread(
target=self._send_tracking_requests, daemon=True
)
self.set_suuid()
self.sending_thread.start()
def set_suuid(self):
try:
file_path = os.path.join(SQLiteTransport.get_base_path("Speckle"), "suuid")
with open(file_path, "r") as file:
self.suuid = file.read()
except:
self.suuid = "unknown-suuid"
def _send_tracking_requests(self):
session = requests.Session()
while True:
params = self.queue.get()
try:
session.post(self.matomo_url, params=params[0])
session.post(self.matomo_url, params=params[1])
except Exception as ex:
LOG.error("Error sending metrics request: " + str(ex))
self.queue.task_done()
+214 -63
View File
@@ -1,15 +1,82 @@
from inspect import getattr_static
from pydantic import BaseModel, validator
from pydantic.main import Extra
from typing import ClassVar, Dict, List, Optional, Any, Set, Type
from specklepy.transports.memory import MemoryTransport
import typing
from typing import (
Any,
Callable,
ClassVar,
Dict,
List,
Optional,
Set,
Type,
get_type_hints,
)
from warnings import warn
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.units import get_units_from_string
from specklepy.transports.memory import MemoryTransport
PRIMITIVES = (int, float, str, bool)
# to remove from dir() when calling get_member_names()
REMOVE_FROM_DIR = {
"Config",
"_Base__dict_helper",
"__annotations__",
"__class__",
"__delattr__",
"__dict__",
"__dir__",
"__doc__",
"__eq__",
"__format__",
"__ge__",
"__getattribute__",
"__getitem__",
"__gt__",
"__hash__",
"__init__",
"__init_subclass__",
"__le__",
"__lt__",
"__module__",
"__ne__",
"__new__",
"__reduce__",
"__reduce_ex__",
"__repr__",
"__setattr__",
"__setitem__",
"__sizeof__",
"__str__",
"__subclasshook__",
"__weakref__",
"_chunk_size_default",
"_chunkable",
"_count_descendants",
"_attr_types",
"_detachable",
"_handle_object_count",
"_type_check",
"_type_registry",
"_units",
"add_chunkable_attrs",
"add_detachable_attrs",
"get_children_count",
"get_dynamic_member_names",
"get_id",
"get_member_names",
"get_registered_type",
"get_typed_member_names",
"to_dict",
"update_forward_refs",
"validate_prop_name",
"from_list",
"to_list",
}
class _RegisteringBase(BaseModel):
class _RegisteringBase:
"""
Private Base model for Speckle types.
@@ -21,7 +88,8 @@ class _RegisteringBase(BaseModel):
"""
speckle_type: ClassVar[str]
_type_registry: ClassVar[Dict[str, Type["Base"]]] = {}
_type_registry: ClassVar[Dict[str, "Base"]] = {}
_attr_types: ClassVar[Dict[str, Type]] = {}
class Config:
validate_assignment = True
@@ -33,7 +101,10 @@ class _RegisteringBase(BaseModel):
def __init_subclass__(
cls,
speckle_type: Optional[str] = None,
speckle_type: str = None,
chunkable: Dict[str, int] = None,
detachable: Set[str] = None,
serialize_ignore: Set[str] = None,
**kwargs: Dict[str, Any],
):
"""
@@ -51,6 +122,17 @@ class _RegisteringBase(BaseModel):
)
cls.speckle_type = speckle_type or cls.__name__
cls._type_registry[cls.speckle_type] = cls # type: ignore
try:
cls._attr_types = get_type_hints(cls)
except Exception:
cls._attr_types = getattr(cls, "__annotations__", {})
if chunkable:
chunkable = {k: v for k, v in chunkable.items() if isinstance(v, int)}
cls._chunkable = dict(cls._chunkable, **chunkable)
if detachable:
cls._detachable = cls._detachable.union(detachable)
if serialize_ignore:
cls._serialize_ignore = cls._serialize_ignore.union(serialize_ignore)
super().__init_subclass__(**kwargs)
@@ -59,9 +141,16 @@ class Base(_RegisteringBase):
totalChildrenCount: Optional[int] = None
applicationId: Optional[str] = None
_units: str = "m"
_chunkable: Dict[str, int] = {} # dict of chunkable props and their max chunk size
# dict of chunkable props and their max chunk size
_chunkable: Dict[str, int] = {}
_chunk_size_default: int = 1000
_detachable: Set[str] = set() # list of defined detachable props
_serialize_ignore: Set[str] = set()
def __init__(self, **kwargs) -> None:
super().__init__()
for k, v in kwargs.items():
self.__setattr__(k, v)
def __repr__(self) -> str:
return (
@@ -73,6 +162,23 @@ class Base(_RegisteringBase):
def __str__(self) -> str:
return self.__repr__()
@classmethod
def of_type(cls, speckle_type: str, **kwargs) -> "Base":
"""
Get a plain Base object with a specified speckle_type.
The speckle_type is protected and cannot be overwritten on a class instance.
This is to prevent problems with receiving in other platforms or connectors.
However, if you really need a base with a different type, here is a helper
to do that for you.
This is used in the deserialisation of unknown types so their speckle_type
can be preserved.
"""
b = cls(**kwargs)
b.__dict__.update(speckle_type=speckle_type)
return b
def __setitem__(self, name: str, value: Any) -> None:
self.validate_prop_name(name)
self.__dict__[name] = value
@@ -82,18 +188,41 @@ class Base(_RegisteringBase):
def __setattr__(self, name: str, value: Any) -> None:
"""
Guard attribute and property set mechanism.
Type checking, guard attribute, and property set mechanism.
The `speckle_type` is a protected class attribute it must not be overridden.
This also performs a type check if the attribute is type hinted.
"""
if name != "speckle_type":
attr = getattr(self.__class__, name, None)
if isinstance(attr, property):
try:
attr.__set__(self, value)
except AttributeError:
pass # the prop probably doesn't have a setter
super().__setattr__(name, value)
if name == "speckle_type":
# not sure if we should raise an exception here??
# raise SpeckleException(
# "Cannot override the `speckle_type`. This is set manually by the class or on deserialisation"
# )
return
# if value is not None:
value = self._type_check(name, value)
attr = getattr(self.__class__, name, None)
if isinstance(attr, property):
try:
attr.__set__(self, value)
except AttributeError:
pass # the prop probably doesn't have a setter
super().__setattr__(name, value)
@classmethod
def update_forward_refs(cls) -> None:
"""
Attempts to populate the internal defined types dict for type checking sometime after defining the class.
This is already done when defining the class, but can be called again if references to undefined types were
included.
See `objects.geometry` for an example of how this is used with the Brep class definitions
"""
try:
cls._attr_types = get_type_hints(cls)
except Exception as e:
warn(f"Could not update forward refs for class {cls.__name__}: {e}")
@classmethod
def validate_prop_name(cls, name: str) -> None:
@@ -109,6 +238,53 @@ class Base(_RegisteringBase):
"Invalid Name: Base member names cannot contain characters '.' or '/'",
)
def _type_check(self, name: str, value: Any):
"""
Lightweight type checking of values before setting them
NOTE: Does not check subscripted types within generics as the performance hit of checking
each item within a given collection isn't worth it. Eg if you have a type Dict[str, float],
we will only check if the value you're trying to set is a dict.
"""
types = getattr(self, "_attr_types", {})
t = types.get(name, None)
if t is None:
return value
if t.__module__ == "typing":
origin = getattr(t, "__origin__")
t = (
tuple(getattr(sub_t, "__origin__", sub_t) for sub_t in t.__args__)
if origin is typing.Union
else origin
)
if not isinstance(t, (type, tuple)):
warn(
f"Unrecognised type '{t}' provided for attribute '{name}'. Type will not been validated."
)
return value
if isinstance(value, t):
return value
# to be friendly, we'll parse ints and strs into floats, but not the other way around
# (to avoid unexpected rounding)
if isinstance(t, tuple):
t = t[0]
try:
if t is float:
return float(value)
if t is str and value:
return str(value)
except ValueError:
pass
raise SpeckleException(
f"Cannot set '{self.__class__.__name__}.{name}': it expects type '{t.__name__}', but received type '{type(value).__name__}'"
)
def add_chunkable_attrs(self, **kwargs: int) -> None:
"""
Mark defined attributes as chunkable for serialisation
@@ -136,53 +312,26 @@ class Base(_RegisteringBase):
def units(self, value: str):
self._units = get_units_from_string(value)
def to_dict(self) -> Dict[str, Any]:
"""Convenience method to view the whole base object as a dict"""
base_dict = self.__dict__
for key, value in base_dict.items():
if not value or isinstance(value, PRIMITIVES):
continue
else:
base_dict[key] = self.__dict_helper(value)
return base_dict
def __dict_helper(self, obj: Any) -> Any:
if not obj or isinstance(obj, PRIMITIVES):
return obj
if isinstance(obj, Base):
return self.__dict_helper(obj.__dict__)
if isinstance(obj, (list, set)):
return [self.__dict_helper(v) for v in obj]
if not isinstance(obj, dict):
raise SpeckleException(
message=f"Could not convert to dict due to unrecognized type: {type(obj)}"
)
for k, v in obj.items():
if v and not isinstance(obj, PRIMITIVES):
obj[k] = self.__dict_helper(v)
return obj
def get_member_names(self) -> List[str]:
"""Get all of the property names on this object, dynamic or not"""
attrs = list(self.__dict__.keys())
properties = [
attr_dir = list(set(dir(self)) - REMOVE_FROM_DIR)
return [
name
for name in dir(self)
if not name.startswith("_")
and name
!= "fields" # soon to be removed as this pydantic prop is depreciated
and isinstance(getattr(type(self), name, None), property)
for name in attr_dir
if not name.startswith("_") and not callable(getattr(self, name))
]
return attrs + properties
def get_serializable_attributes(self) -> List[str]:
"""Get the attributes that should be serialized"""
return list(set(self.get_member_names()) - self._serialize_ignore)
def get_typed_member_names(self) -> List[str]:
"""Get all of the names of the defined (typed) properties of this object"""
return list(self.__fields__.keys())
return list(self._attr_types.keys())
def get_dynamic_member_names(self) -> List[str]:
"""Get all of the names of the dynamic properties of this object"""
return list(set(self.__dict__.keys()) - set(self.__fields__.keys()))
return list(set(self.__dict__.keys()) - set(self._attr_types.keys()))
def get_children_count(self) -> int:
"""Get the total count of children Base objects"""
@@ -191,7 +340,8 @@ class Base(_RegisteringBase):
def get_id(self, decompose: bool = False) -> str:
"""
Gets the id (a unique hash) of this object. ⚠️ This method fully serializes the object, which in the case of large objects (with many sub-objects), has a tangible cost. Avoid using it!
Gets the id (a unique hash) of this object. ⚠️ This method fully serializes the object which,
in the case of large objects (with many sub-objects), has a tangible cost. Avoid using it!
Note: the hash of a decomposed object differs from that of a non-decomposed object
@@ -201,9 +351,7 @@ class Base(_RegisteringBase):
Returns:
str -- the hash (id) of the fully serialized object
"""
from specklepy.serialization.base_object_serializer import (
BaseObjectSerializer,
)
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
serializer = BaseObjectSerializer()
if decompose:
@@ -217,7 +365,7 @@ class Base(_RegisteringBase):
return sum(
self._handle_object_count(value, parsed)
for name, value in base.__dict__.items()
for name, value in base.get_member_names()
if not name.startswith("@")
)
@@ -245,9 +393,12 @@ class Base(_RegisteringBase):
count += self._handle_object_count(value, parsed)
return count
class Config:
extra = Extra.allow
Base.update_forward_refs()
class DataChunk(Base, speckle_type="Speckle.Core.Models.DataChunk"):
data: List[Any] = []
data: List[Any] = None
def __init__(self) -> None:
self.data = []
+136
View File
@@ -0,0 +1,136 @@
from enum import Enum
from typing import Any, Callable, List, Type
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.base import Base
class CurveTypeEncoding(int, Enum):
Arc = 0
Circle = 1
Curve = 2
Ellipse = 3
Line = 4
Polyline = 5
Polycurve = 6
@property
def object_class(self) -> Type:
from . import geometry
if self == self.Arc:
return geometry.Arc
elif self == self.Circle:
return geometry.Circle
elif self == self.Curve:
return geometry.Curve
elif self == self.Ellipse:
return geometry.Ellipse
elif self == self.Line:
return geometry.Line
elif self == self.Polyline:
return geometry.Polyline
elif self == self.Polycurve:
return geometry.Polycurve
raise SpeckleException(
f"No corresponding object class for CurveTypeEncoding: {self}"
)
def curve_from_list(args: List[float]):
curve_type = CurveTypeEncoding(args[0])
return curve_type.object_class.from_list(args)
class ObjectArray:
def __init__(self) -> None:
self.data = []
@classmethod
def from_objects(cls, objects: List[Base]) -> "ObjectArray":
data_list = cls()
if not objects:
return data_list
speckle_type = objects[0].speckle_type
for obj in objects:
if speckle_type != obj.speckle_type:
raise SpeckleException(
"All objects in chunk should have the same speckle_type. "
f"Found {speckle_type} and {obj.speckle_type}"
)
data_list.encode_object(object=obj)
return data_list
@staticmethod
def decode_data(
data: List[Any], decoder: Callable[[List[Any]], Base]
) -> List[Base]:
bases = []
if not data:
return bases
index = 0
while index < len(data):
item_length = data[index]
item_start = index + 1
item_end = item_start + item_length
item_data = data[item_start:item_end]
index = item_end
# TODO: investigate what's going on w this fail
try:
decoded_data = decoder(item_data)
bases.append(decoded_data)
except ValueError:
continue
return bases
def decode(self, decoder: Callable[[List[Any]], Any]):
return self.decode_data(data=self.data, decoder=decoder)
def encode_object(self, object: Base):
encoded = object.to_list()
encoded.insert(0, len(encoded))
self.data.extend(encoded)
class CurveArray(ObjectArray):
@classmethod
def from_curve(cls, curve: Base) -> "CurveArray":
crv_array = cls()
crv_array.data = curve.to_list()
return crv_array
@classmethod
def from_curves(cls, curves: List[Base]) -> "CurveArray":
data = []
for curve in curves:
curve_list = curve.to_list()
curve_list.insert(0, len(curve_list))
data.extend(curve_list)
crv_array = cls()
crv_array.data = data
return crv_array
@staticmethod
def curve_from_list(args: List[float]) -> Base:
curve_type = CurveTypeEncoding(args[0])
return curve_type.object_class.from_list(args)
@property
def type(self) -> CurveTypeEncoding:
return CurveTypeEncoding(self.data[0])
def to_curve(self) -> Base:
return self.type.object_class.from_list(self.data)
@classmethod
def _curve_decoder(cls, data: List[float]) -> Base:
crv_array = cls()
crv_array.data = data
return crv_array.to_curve()
def to_curves(self) -> List[Base]:
return self.decode(decoder=self._curve_decoder)
+10 -5
View File
@@ -14,7 +14,12 @@ CHUNKABLE_PROPS = {
DETACHABLE = {"detach_this", "origin", "detached_list"}
class FakeMesh(Base):
class FakeGeo(Base, chunkable={"dots": 50}, detachable={"pointslist"}):
pointslist: List[Base] = None
dots: List[int] = None
class FakeMesh(FakeGeo, chunkable=CHUNKABLE_PROPS, detachable=DETACHABLE):
vertices: List[float] = None
faces: List[int] = None
colors: List[int] = None
@@ -24,10 +29,10 @@ class FakeMesh(Base):
detached_list: List[Base] = None
_origin: Point = None
def __init__(self, **kwargs) -> None:
super(FakeMesh, self).__init__(**kwargs)
self.add_chunkable_attrs(**CHUNKABLE_PROPS)
self.add_detachable_attrs(DETACHABLE)
# def __init__(self, **kwargs) -> None:
# super(FakeMesh, self).__init__(**kwargs)
# self.add_chunkable_attrs(**CHUNKABLE_PROPS)
# self.add_detachable_attrs(DETACHABLE)
@property
def origin(self):
+455 -71
View File
@@ -1,29 +1,49 @@
from enum import Enum
from typing import Any, List, Optional
from .base import Base
from typing import Any, List
from .encoding import CurveArray, CurveTypeEncoding, ObjectArray
from .units import get_encoding_from_units, get_units_from_encoding
GEOMETRY = "Objects.Geometry."
class Interval(Base, speckle_type="Objects.Primitive.Interval"):
start: float = 0
end: float = 0
start: float = 0.0
end: float = 0.0
def length(self):
return abs(self.start - self.end)
@classmethod
def from_list(cls, args: List[Any]) -> "Interval":
return cls(start=args[0], end=args[1])
def to_list(self) -> List[Any]:
return [self.start, self.end]
class Point(Base, speckle_type=GEOMETRY + "Point"):
x: float = 0
y: float = 0
z: float = 0
def __init__(self, x: float = 0, y: float = 0, z: float = 0, **data: Any) -> None:
super().__init__(**data)
self.x, self.y, self.z = x, y, z
x: float = 0.0
y: float = 0.0
z: float = 0.0
def __repr__(self) -> str:
return f"{self.__class__.__name__}(x: {self.x}, y: {self.y}, z: {self.z}, id: {self.id}, speckle_type: {self.speckle_type})"
@classmethod
def from_list(cls, args: List[float]) -> "Point":
return cls(x=args[0], y=args[1], z=args[2])
def to_list(self) -> List[Any]:
return [self.x, self.y, self.z]
@classmethod
def from_coords(cls, x: float = 0.0, y: float = 0.0, z: float = 0.0):
pt = Point()
pt.x, pt.y, pt.z = x, y, z
return pt
class Vector(Point, speckle_type=GEOMETRY + "Vector"):
pass
@@ -39,6 +59,23 @@ class Plane(Base, speckle_type=GEOMETRY + "Plane"):
xdir: Vector = Vector()
ydir: Vector = Vector()
@classmethod
def from_list(cls, args: List[Any]) -> "Plane":
return cls(
origin=Point.from_list(args[0:3]),
normal=Vector.from_list(args[3:6]),
xdir=Vector.from_list(args[6:9]),
ydir=Vector.from_list(args[9:12]),
)
def to_list(self) -> List[Any]:
encoded = []
encoded.extend(self.origin.to_list())
encoded.extend(self.normal.to_list())
encoded.extend(self.xdir.to_list())
encoded.extend(self.ydir.to_list())
return encoded
class Box(Base, speckle_type=GEOMETRY + "Box"):
basePlane: Plane = Plane()
@@ -56,6 +93,21 @@ class Line(Base, speckle_type=GEOMETRY + "Line"):
bbox: Box = None
length: float = None
@classmethod
def from_list(cls, args: List[Any]) -> "Line":
return cls(
start=Point.from_list(args[0:3]),
end=Point.from_list(args[3:6]),
domain=Interval.from_list(args[6:9]),
)
def to_list(self) -> List[Any]:
encoded = []
encoded.extend(self.start.to_list())
encoded.extend(self.end.to_list())
encoded.extend(self.domain.to_list())
return encoded
class Arc(Base, speckle_type=GEOMETRY + "Arc"):
radius: float = None
@@ -71,6 +123,30 @@ class Arc(Base, speckle_type=GEOMETRY + "Arc"):
area: float = None
length: float = None
@classmethod
def from_list(cls, args: List[Any]) -> "Arc":
return cls(
radius=args[1],
startAngle=args[2],
endAngle=args[3],
angleRadians=args[4],
domain=Interval.from_list(args[5:7]),
plane=Plane.from_list(args[7:20]),
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
encoded = []
encoded.append(CurveTypeEncoding.Arc.value)
encoded.append(self.radius)
encoded.append(self.startAngle)
encoded.append(self.endAngle)
encoded.append(self.angleRadians)
encoded.extend(self.domain.to_list())
encoded.extend(self.plane.to_list())
encoded.append(get_encoding_from_units(self.units))
return encoded
class Circle(Base, speckle_type=GEOMETRY + "Circle"):
radius: float = None
@@ -80,6 +156,24 @@ class Circle(Base, speckle_type=GEOMETRY + "Circle"):
area: float = None
length: float = None
@classmethod
def from_list(cls, args: List[Any]) -> "Circle":
return cls(
radius=args[1],
domain=Interval.from_list(args[2:4]),
plane=Plane.from_list(args[4:17]),
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
encoded = []
encoded.append(CurveTypeEncoding.Circle.value)
encoded.append(self.radius),
encoded.extend(self.domain.to_list())
encoded.extend(self.plane.to_list())
encoded.append(get_encoding_from_units(self.units))
return encoded
class Ellipse(Base, speckle_type=GEOMETRY + "Ellipse"):
firstRadius: float = None
@@ -91,8 +185,28 @@ class Ellipse(Base, speckle_type=GEOMETRY + "Ellipse"):
area: float = None
length: float = None
@classmethod
def from_list(cls, args: List[Any]) -> "Ellipse":
return cls(
firstRadius=args[1],
secondRadius=args[2],
domain=Interval.from_list(args[3:5]),
plane=Plane.from_list(args[5:18]),
units=get_units_from_encoding(args[-1]),
)
class Polyline(Base, speckle_type=GEOMETRY + "Polyline"):
def to_list(self) -> List[Any]:
encoded = []
encoded.append(CurveTypeEncoding.Ellipse.value)
encoded.append(self.firstRadius)
encoded.append(self.secondRadius)
encoded.extend(self.domain.to_list())
encoded.extend(self.plane.to_list())
encoded.append(get_encoding_from_units(self.units))
return encoded
class Polyline(Base, speckle_type=GEOMETRY + "Polyline", chunkable={"value": 20000}):
value: List[float] = None
closed: bool = None
domain: Interval = None
@@ -100,27 +214,34 @@ class Polyline(Base, speckle_type=GEOMETRY + "Polyline"):
area: float = None
length: float = None
def __init__(self, **data: Any) -> None:
super().__init__(**data)
self.add_chunkable_attrs(value=20000)
@classmethod
def from_points(cls, points: List[Point]):
polyline = cls()
polyline.units = points[0].units
polyline.value = []
for point in points:
polyline.value.extend([point.x, point.y, point.z])
return polyline
# @property
# def value(self) -> List[float]:
# return self._value
@classmethod
def from_list(cls, args: List[Any]) -> "Polyline":
point_count = args[4]
return cls(
closed=bool(args[1]),
domain=Interval.from_list(args[2:4]),
value=args[5 : 5 + point_count],
units=get_units_from_encoding(args[-1]),
)
# @value.setter
# def value(self, coords) -> None:
# if len(coords) % 3:
# coords.extend([0] * (3 - len(coords) % 3))
# self._value = coords
def to_list(self) -> List[Any]:
encoded = []
encoded.append(CurveTypeEncoding.Polyline.value)
encoded.append(int(self.closed))
encoded.extend(self.domain.to_list())
encoded.append(len(self.value))
encoded.extend(self.value)
encoded.append(get_encoding_from_units(self.units))
return encoded
def as_points(self) -> List[Point]:
"""Converts the `value` attribute to a list of Points"""
@@ -131,10 +252,16 @@ class Polyline(Base, speckle_type=GEOMETRY + "Polyline"):
raise ValueError("Points array malformed: length%3 != 0.")
values = iter(self.value)
return [Point(v, next(values), next(values), units=self.units) for v in values]
return [
Point(x=v, y=next(values), z=next(values), units=self.units) for v in values
]
class Curve(Base, speckle_type=GEOMETRY + "Curve"):
class Curve(
Base,
speckle_type=GEOMETRY + "Curve",
chunkable={"points": 20000, "weights": 20000, "knots": 20000},
):
degree: int = None
periodic: bool = None
rational: bool = None
@@ -148,10 +275,6 @@ class Curve(Base, speckle_type=GEOMETRY + "Curve"):
area: float = None
length: float = None
def __init__(self, **data: Any) -> None:
super().__init__(**data)
self.add_chunkable_attrs(points=20000, weights=20000, knots=20000)
def as_points(self) -> List[Point]:
"""Converts the `value` attribute to a list of Points"""
if not self.points:
@@ -161,17 +284,80 @@ class Curve(Base, speckle_type=GEOMETRY + "Curve"):
raise ValueError("Points array malformed: length%3 != 0.")
values = iter(self.points)
return [Point(v, next(values), next(values), units=self.units) for v in values]
return [
Point(x=v, y=next(values), z=next(values), units=self.units) for v in values
]
@classmethod
def from_list(cls, args: List[Any]) -> "Curve":
point_count = args[7]
weights_count = args[8]
knots_count = args[9]
points_start = 10
weights_start = 10 + point_count
knots_start = weights_start + weights_count
knots_end = knots_start + knots_count
return cls(
degree=args[1],
periodic=bool(args[2]),
rational=bool(args[3]),
closed=bool(args[4]),
domain=Interval.from_list(args[5:7]),
points=args[points_start:weights_start],
weights=args[weights_start:knots_start],
knots=args[knots_start:knots_end],
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
encoded = []
encoded.append(CurveTypeEncoding.Curve.value)
encoded.append(self.degree)
encoded.append(int(self.periodic))
encoded.append(int(self.rational))
encoded.append(int(self.closed))
encoded.extend(self.domain.to_list())
encoded.append(len(self.points))
encoded.append(len(self.weights))
encoded.append(len(self.knots))
encoded.extend(self.points)
encoded.extend(self.weights)
encoded.extend(self.knots)
encoded.append(get_encoding_from_units(self.units))
return encoded
class Polycurve(Base, speckle_type=GEOMETRY + "Polycurve"):
segments: List[Base] = []
segments: List[Base] = None
domain: Interval = None
closed: bool = None
bbox: Box = None
area: float = None
length: float = None
@classmethod
def from_list(cls, args: List[Any]) -> "Polycurve":
curve_arrays = CurveArray()
curve_arrays.data = args[4:-1]
return cls(
closed=bool(args[1]),
domain=Interval.from_list(args[2:4]),
segments=curve_arrays.to_curves(),
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
encoded = []
encoded.append(CurveTypeEncoding.Polycurve.value)
encoded.append(int(self.closed))
encoded.extend(self.domain.to_list())
curve_array = CurveArray.from_curves(self.segments)
encoded.extend(curve_array.data)
encoded.append(get_encoding_from_units(self.units))
return encoded
class Extrusion(Base, speckle_type=GEOMETRY + "Extrusion"):
capped: bool = None
@@ -187,7 +373,16 @@ class Extrusion(Base, speckle_type=GEOMETRY + "Extrusion"):
bbox: Box = None
class Mesh(Base, speckle_type=GEOMETRY + "Mesh"):
class Mesh(
Base,
speckle_type=GEOMETRY + "Mesh",
chunkable={
"vertices": 2000,
"faces": 2000,
"colors": 2000,
"textureCoordinates": 2000,
},
):
vertices: List[float] = None
faces: List[int] = None
colors: List[int] = None
@@ -196,12 +391,6 @@ class Mesh(Base, speckle_type=GEOMETRY + "Mesh"):
area: float = None
volume: float = None
def __init__(self, **data) -> None:
super().__init__(**data)
self.add_chunkable_attrs(
vertices=2000, faces=2000, colors=2000, textureCoordinates=2000
)
class Surface(Base, speckle_type=GEOMETRY + "Surface"):
degreeU: int = None
@@ -212,6 +401,58 @@ class Surface(Base, speckle_type=GEOMETRY + "Surface"):
countU: int = None
countV: int = None
bbox: Box = None
closedU: bool = None
closedV: bool = None
domainU: Interval = None
domainV: Interval = None
knotsU: List[float] = None
knotsV: List[float] = None
@classmethod
def from_list(cls, args: List[Any]) -> "Surface":
point_count = int(args[11])
knots_u_count = int(args[12])
knots_v_count = int(args[13])
start_point_data = 14
start_knots_u = start_point_data + point_count
start_knots_v = start_knots_u + knots_u_count
return cls(
degreeU=int(args[0]),
degreeV=int(args[1]),
countU=int(args[2]),
countV=int(args[3]),
rational=bool(args[4]),
closedU=bool(args[5]),
closedV=bool(args[6]),
domainU=Interval(start=args[7], end=args[8]),
domainV=Interval(start=args[9], end=args[10]),
pointData=args[start_point_data:start_knots_u],
knotsU=args[start_knots_u:start_knots_v],
knotsV=args[start_knots_v : start_knots_v + knots_v_count],
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
encoded = []
encoded.append(self.degreeU)
encoded.append(self.degreeV)
encoded.append(self.countU)
encoded.append(self.countV)
encoded.append(int(self.rational))
encoded.append(int(self.closedU))
encoded.append(int(self.closedV))
encoded.extend(self.domainU.to_list())
encoded.extend(self.domainV.to_list())
encoded.append(len(self.pointData))
encoded.append(len(self.knotsU))
encoded.append(len(self.knotsV))
encoded.extend(self.pointData)
encoded.extend(self.knotsU)
encoded.extend(self.knotsV)
encoded.append(get_encoding_from_units(self.units))
return encoded
class BrepFace(Base, speckle_type=GEOMETRY + "BrepFace"):
@@ -231,7 +472,8 @@ class BrepFace(Base, speckle_type=GEOMETRY + "BrepFace"):
@property
def _loops(self):
return [self._Brep.Loops[index] for index in self.LoopIndices]
if self.LoopIndices:
return [self._Brep.Loops[i] for i in self.LoopIndices]
class BrepEdge(Base, speckle_type=GEOMETRY + "BrepEdge"):
@@ -253,7 +495,8 @@ class BrepEdge(Base, speckle_type=GEOMETRY + "BrepEdge"):
@property
def _trims(self):
return [self._Brep.Trims[i] for i in self.TrimIndices]
if self.TrimIndices:
return [self._Brep.Trims[i] for i in self.TrimIndices]
@property
def _curve(self):
@@ -272,7 +515,19 @@ class BrepLoop(Base, speckle_type=GEOMETRY + "BrepLoop"):
@property
def _trims(self):
return [self._Brep.Trims[i] for i in self.TrimIndices]
if self.TrimIndices:
return [self._Brep.Trims[i] for i in self.TrimIndices]
class BrepTrimTypeEnum(int, Enum):
Unknown = 0
Boundary = 1
Mated = 2
Seam = 3
Singular = 4
CurveOnSurface = 5
PointOnSurface = 6
Slit = 7
class BrepTrim(Base, speckle_type=GEOMETRY + "BrepTrim"):
@@ -304,45 +559,174 @@ class BrepTrim(Base, speckle_type=GEOMETRY + "BrepTrim"):
def _curve_2d(self):
return self._Brep.Curve2D[self.CurveIndex]
@classmethod
def from_list(cls, args: List[Any]) -> "BrepTrim":
return cls(
EdgeIndex=args[0],
StartIndex=args[1],
EndIndex=args[2],
FaceIndex=args[3],
LoopIndex=args[4],
CurveIndex=args[5],
IsoStatus=args[6],
TrimType=BrepTrimTypeEnum(args[7]).name,
IsReversed=bool(args[8]),
)
class Brep(Base, speckle_type=GEOMETRY + "Brep"):
def to_list(self) -> List[Any]:
encoded = []
encoded.append(self.EdgeIndex)
encoded.append(self.StartIndex)
encoded.append(self.EndIndex)
encoded.append(self.FaceIndex)
encoded.append(self.LoopIndex)
encoded.append(self.CurveIndex)
encoded.append(self.IsoStatus)
encoded.append(getattr(BrepTrimTypeEnum, self.TrimType).value)
encoded.append(self.IsReversed)
return encoded
class Brep(
Base,
speckle_type=GEOMETRY + "Brep",
chunkable={
"SurfacesValue": 200,
"Curve3DValues": 200,
"Curve2DValues": 200,
"VerticesValue": 5000,
"Edges": 5000,
"Loops": 5000,
"TrimsValue": 5000,
"Faces": 5000,
},
detachable={"displayValue"},
serialize_ignore={"Surfaces", "Curve3D", "Curve2D", "Vertices", "Trims"},
):
provenance: str = None
bbox: Box = None
area: float = None
volume: float = None
displayValue: Mesh = None
Surfaces: List[Surface] = []
Curve3D: List[Base] = []
Curve2D: List[Base] = []
Vertices: List[Point] = []
Edges: List[BrepEdge] = []
Loops: List[BrepLoop] = []
Trims: List[BrepTrim] = []
Faces: List[BrepFace] = []
Surfaces: List[Surface] = None
Curve3D: List[Base] = None
Curve2D: List[Base] = None
Vertices: List[Point] = None
IsClosed: bool = None
Orientation: int = 0
Orientation: int = None
def __init__(self, **data: Any) -> None:
super().__init__(**data)
self.add_detachable_attrs({"displayValue"})
self.add_chunkable_attrs(
Surfaces=200,
Curve3D=200,
Curve2D=200,
Vertices=5000,
Edges=5000,
Loops=5000,
Trims=5000,
Faces=5000,
)
def _inject_self_into_children(self, children: Optional[List[Base]]) -> List[Base]:
if children is None:
return children
def __setattr__(self, name: str, value: Any) -> None:
if not value:
return
if name in ["Edges", "Loops", "Trims", "Faces"]:
for val in value:
val._Brep = self
super().__setattr__(name, value)
for child in children:
child._Brep = self
return children
@property
def Edges(self) -> List[BrepEdge]:
return self._inject_self_into_children(self._Edges)
@Edges.setter
def Edges(self, value: List[BrepEdge]):
self._Edges = value
@property
def Loops(self) -> List[BrepLoop]:
return self._inject_self_into_children(self._Loops)
@Loops.setter
def Loops(self, value: List[BrepLoop]):
self._Loops = value
@property
def Faces(self) -> List[BrepFace]:
return self._inject_self_into_children(self._Faces)
@Faces.setter
def Faces(self, value: List[BrepFace]):
self._Faces = value
@property
def SurfacesValue(self) -> List[float]:
if self.Surfaces is None:
return None
return ObjectArray.from_objects(self.Surfaces).data
@SurfacesValue.setter
def SurfacesValue(self, value: List[float]):
self.Surfaces = ObjectArray.decode_data(value, Surface.from_list)
@property
def Curve3DValues(self) -> List[float]:
if self.Curve3D is None:
return None
return CurveArray.from_curves(self.Curve3D).data
@Curve3DValues.setter
def Curve3DValues(self, value: List[float]):
crv_array = CurveArray()
crv_array.data = value
self.Curve3D = crv_array.to_curves()
@property
def Curve2DValues(self) -> List[Base]:
if self.Curve2D is None:
return None
return CurveArray.from_curves(self.Curve2D).data
@Curve2DValues.setter
def Curve2DValues(self, value: List[float]):
crv_array = CurveArray()
crv_array.data = value
self.Curve2D = crv_array.to_curves()
@property
def VerticesValue(self) -> List[Point]:
if self.Vertices is None:
return None
encoded_unit = get_encoding_from_units(self.Vertices[0].units)
values = [encoded_unit]
for vertex in self.Vertices:
values.extend(vertex.to_list())
return values
@VerticesValue.setter
def VerticesValue(self, value: List[float]):
value = value.copy()
units = get_units_from_encoding(value.pop(0))
vertices = []
for i in range(0, len(value), 3):
vertex = Point.from_list(value[i : i + 3])
vertex._units = units
vertices.append(vertex)
self.Vertices = vertices
@property
def Trims(self) -> List[BrepTrim]:
return self._inject_self_into_children(self._Trims)
@Trims.setter
def Trims(self, value: List[BrepTrim]):
self._Trims = value
@property
def TrimsValue(self) -> List[float]:
if self.Trims is None:
return None
values = []
for trim in self.Trims:
values.extend(trim.to_list())
return values
@TrimsValue.setter
def TrimsValue(self, value: List[float]):
self.Trims = [
BrepTrim.from_list(value[i : i + 9]) for i in range(0, len(value), 9)
]
BrepEdge.update_forward_refs()
+31
View File
@@ -14,6 +14,18 @@ UNITS_STRINGS = {
"none": ["none", "null"],
}
UNITS_ENCODINGS = {
"none": 0,
"mm": 1,
"cm": 2,
"m": 3,
"km": 4,
"in": 5,
"ft": 6,
"yd": 7,
"mi": 8,
}
def get_units_from_string(unit: str):
unit = str.lower(unit)
@@ -24,3 +36,22 @@ def get_units_from_string(unit: str):
raise SpeckleException(
message=f"Could not understand what unit {unit} is referring to. Please enter a valid unit (eg {UNITS})."
)
def get_units_from_encoding(unit: int):
for name, encoding in UNITS_ENCODINGS.items():
if unit == encoding:
return name
raise SpeckleException(
message=f"Could not understand what unit {unit} is referring to. Please enter a valid unit encoding (eg {UNITS_ENCODINGS})."
)
def get_encoding_from_units(unit: str):
try:
return UNITS_ENCODINGS[unit]
except KeyError:
raise SpeckleException(
message=f"No encoding exists for unit {unit}. Please enter a valid unit to encode (eg {UNITS_ENCODINGS})."
)
@@ -1,4 +1,4 @@
import json
import ujson
import hashlib
import re
@@ -14,7 +14,7 @@ PRIMITIVES = (int, float, str, bool)
def hash_obj(obj: Any) -> str:
return hashlib.sha256(json.dumps(obj).encode()).hexdigest()[:32]
return hashlib.sha256(ujson.dumps(obj).encode()).hexdigest()[:32]
class BaseObjectSerializer:
@@ -35,7 +35,7 @@ class BaseObjectSerializer:
self.__reset_writer()
self.detach_lineage = [True]
hash, obj = self.traverse_base(base)
return hash, json.dumps(obj)
return hash, ujson.dumps(obj)
def traverse_base(self, base: Base) -> Tuple[str, Dict]:
"""Decomposes the given base object and builds a serializable dictionary
@@ -52,7 +52,7 @@ class BaseObjectSerializer:
self.lineage.append(uuid4().hex)
object_builder = {"id": "", "speckle_type": "Base", "totalChildrenCount": 0}
object_builder.update(speckle_type=base.speckle_type)
obj, props = base, base.get_member_names()
obj, props = base, base.get_serializable_attributes()
while props:
prop = props.pop(0)
@@ -70,7 +70,9 @@ class BaseObjectSerializer:
# only bother with chunking and detaching if there is a write transport
if self.write_transports:
dynamic_chunk_match = re.match(r"^@\((\d*)\)", prop)
dynamic_chunk_match = prop.startswith("@") and re.match(
r"^@\((\d*)\)", prop
)
if dynamic_chunk_match:
chunk_size = dynamic_chunk_match.groups()[0]
base._chunkable[prop] = (
@@ -140,7 +142,7 @@ class BaseObjectSerializer:
# write detached or root objects to transports
if detached and self.write_transports:
for t in self.write_transports:
t.save_object(id=hash, serialized_object=json.dumps(object_builder))
t.save_object(id=hash, serialized_object=ujson.dumps(object_builder))
del self.lineage[-1]
@@ -236,7 +238,7 @@ class BaseObjectSerializer:
"""
if not obj_string:
return None
obj = json.loads(obj_string)
obj = ujson.loads(obj_string)
return self.recompose_base(obj=obj)
def recompose_base(self, obj: dict) -> Base:
@@ -252,7 +254,7 @@ class BaseObjectSerializer:
if not obj:
return
if isinstance(obj, str):
obj = json.loads(obj)
obj = ujson.loads(obj)
if "speckle_type" in obj and obj["speckle_type"] == "reference":
obj = self.get_child(obj=obj)
@@ -266,7 +268,7 @@ class BaseObjectSerializer:
object_type = Base.get_registered_type(speckle_type)
# initialise the base object using `speckle_type` fall back to base if needed
base = object_type() if object_type else Base(speckle_type=speckle_type)
base = object_type() if object_type else Base.of_type(speckle_type=speckle_type)
# get total children count
if "__closure" in obj:
if not self.read_transport:
@@ -290,7 +292,7 @@ class BaseObjectSerializer:
raise SpeckleException(
f"Could not find the referenced child object of id `{ref_hash}` in the given read transport: {self.read_transport.name}"
)
ref_obj = json.loads(ref_obj_str)
ref_obj = ujson.loads(ref_obj_str)
base.__setattr__(prop, self.recompose_base(obj=ref_obj))
# 3. handle all other cases (base objects, lists, and dicts)
@@ -324,7 +326,7 @@ class BaseObjectSerializer:
# handle chunked lists
data = []
for o in obj_list:
data.extend(o["data"])
data.extend(o.data)
return data
return obj_list
@@ -348,4 +350,4 @@ class BaseObjectSerializer:
raise SpeckleException(
f"Could not find the referenced child object of id `{ref_hash}` in the given read transport: {self.read_transport.name}"
)
return json.loads(ref_obj_str)
return ujson.loads(ref_obj_str)
+19 -11
View File
@@ -102,7 +102,9 @@ class BatchSender(object):
new_objects = [obj[1] for obj in batch if obj[0] in new_object_ids]
if not new_objects:
LOG.info(f"Uploading batch of {len(batch)} objects: all objects are already in the server")
LOG.info(
f"Uploading batch of {len(batch)} objects: all objects are already in the server"
)
return
upload_data = "[" + ",".join(new_objects) + "]"
@@ -112,24 +114,30 @@ class BatchSender(object):
% (len(batch), len(new_objects), len(upload_data), len(upload_data_gzip))
)
r = session.post(
url=f"{self.server_url}/objects/{self.stream_id}",
files={"batch-1": ("batch-1", upload_data_gzip, "application/gzip")},
)
if r.status_code != 201:
LOG.warning("Upload server response: %s", r.text)
raise SpeckleException(
message=f"Could not save the object to the server - status code {r.status_code}"
try:
r = session.post(
url=f"{self.server_url}/objects/{self.stream_id}",
files={"batch-1": ("batch-1", upload_data_gzip, "application/gzip")},
)
if r.status_code != 201:
LOG.warning("Upload server response: %s", r.text)
raise SpeckleException(
message=f"Could not save the object to the server - status code {r.status_code}"
)
except json.JSONDecodeError as error:
return SpeckleException(
f"Failed to send objects to {self.server_url}. Please ensure this stream ({self.stream_id}) exists on this server and that you have permission to send to it.",
error,
)
def _create_threads(self):
for i in range(self.thread_count):
for _ in range(self.thread_count):
t = threading.Thread(target=self._sending_thread_main, daemon=True)
t.start()
self._send_threads.append(t)
def _delete_threads(self):
for i in range(len(self._send_threads)):
for _ in range(len(self._send_threads)):
self._batches.put(None)
for thread in self._send_threads:
+66 -9
View File
@@ -13,20 +13,71 @@ from .batch_sender import BatchSender
class ServerTransport(AbstractTransport):
"""
The `ServerTransport` is the vehicle through which you transport objects to and from a Speckle Server. Provide it to
`operations.send()` or `operations.receive()`.
The `ServerTransport` can be authenticted two different ways:
1. by providing a `SpeckleClient`
2. by providing a `token` and `url`
```py
from specklepy.api import operations
from specklepy.transports.server import ServerTransport
# here's the data you want to send
block = Block(length=2, height=4)
# next create the server transport - this is the vehicle through which you will send and receive
transport = ServerTransport(stream_id=new_stream_id, client=client)
# this serialises the block and sends it to the transport
hash = operations.send(base=block, transports=[transport])
# you can now create a commit on your stream with this object
commid_id = client.commit.create(
stream_id=new_stream_id,
obj_id=hash,
message="this is a block I made in speckle-py",
)
```
"""
_name = "RemoteTransport"
url: str = None
stream_id: str = None
saved_obj_count: int = 0
session: requests.Session = None
def __init__(self, client: SpeckleClient, stream_id: str, **data: Any) -> None:
def __init__(
self,
stream_id: str,
client: SpeckleClient = None,
token: str = None,
url: str = None,
**data: Any,
) -> None:
super().__init__(**data)
# TODO: replace client with account or some other auth avenue
self.url = client.url
self.stream_id = stream_id
if client is None and token is None and url is None:
raise SpeckleException(
"You must provide either a client or a token and url to construct a ServerTransport."
)
token = client.me["token"]
self._batch_sender = BatchSender(self.url, self.stream_id, token, max_batch_size_mb=1)
if client:
if not client.me:
raise SpeckleException(
"The provided SpeckleClient was not authenticated."
)
token = client.me["token"]
url = client.url
self.stream_id = stream_id
self.url = url
self._batch_sender = BatchSender(
self.url, self.stream_id, token, max_batch_size_mb=1
)
self.session = requests.Session()
self.session.headers.update(
@@ -73,19 +124,25 @@ class ServerTransport(AbstractTransport):
r.encoding = "utf-8"
if r.status_code != 200:
raise SpeckleException(f"Can't get object {self.stream_id}/{id}: HTTP error {r.status_code} ({r.text[:1000]})")
raise SpeckleException(
f"Can't get object {self.stream_id}/{id}: HTTP error {r.status_code} ({r.text[:1000]})"
)
root_obj_serialized = r.text
root_obj = json.loads(root_obj_serialized)
closures = root_obj.get('__closure', {})
closures = root_obj.get("__closure", {})
# Check which children are not already in the target transport
children_ids = list(closures.keys())
children_found_map = target_transport.has_objects(children_ids)
new_children_ids = [id for id in children_found_map if not children_found_map[id]]
new_children_ids = [
id for id in children_found_map if not children_found_map[id]
]
# Get the new children
endpoint = f"{self.url}/api/getobjects/{self.stream_id}"
r = self.session.post(endpoint, data={"objects": json.dumps(new_children_ids)}, stream=True)
r = self.session.post(
endpoint, data={"objects": json.dumps(new_children_ids)}, stream=True
)
if r.encoding is None:
r.encoding = "utf-8"
lines = r.iter_lines(decode_unicode=True)
+16 -9
View File
@@ -32,14 +32,20 @@ class SQLiteTransport(AbstractTransport):
super().__init__(**data)
self.app_name = app_name or "Speckle"
self.scope = scope or "Objects"
self._base_path = base_path or self.__get_base_path()
self._base_path = base_path or self.get_base_path(self.app_name)
os.makedirs(self._base_path, exist_ok=True)
try:
os.makedirs(self._base_path, exist_ok=True)
self._root_path = os.path.join(
os.path.join(self._base_path, f"{self.scope}.db")
)
self.__initialise()
self._root_path = os.path.join(
os.path.join(self._base_path, f"{self.scope}.db")
)
self.__initialise()
except Exception as ex:
raise SpeckleException(
f"SQLiteTransport could not initialise {self.scope}.db at {self._base_path}. Either provide a different `base_path` or use an alternative transport.",
ex,
)
def __repr__(self) -> str:
return f"SQLiteTransport(app: '{self.app_name}', scope: '{self.scope}')"
@@ -50,7 +56,8 @@ class SQLiteTransport(AbstractTransport):
# proc.start()
# proc.join()
def __get_base_path(self):
@staticmethod
def get_base_path(app_name):
# from appdirs https://github.com/ActiveState/appdirs/blob/master/appdirs.py
# default mac path is not the one we use (we use unix path), so using special case for this
system = sys.platform
@@ -62,10 +69,10 @@ class SQLiteTransport(AbstractTransport):
system = "darwin"
if system != "darwin":
return user_data_dir(appname=self.app_name, appauthor=False, roaming=True)
return user_data_dir(appname=app_name, appauthor=False, roaming=True)
path = os.path.expanduser("~/.config/")
return os.path.join(path, self.app_name)
return os.path.join(path, app_name)
# def __consume_queue(self):
# if self._is_writing or self.__queue.empty():
+5 -1
View File
@@ -7,6 +7,9 @@ from specklepy.api.client import SpeckleClient
from specklepy.objects.base import Base
from specklepy.objects.geometry import Point
from specklepy.objects.fakemesh import FakeMesh
from specklepy.logging import metrics
metrics.disable()
@pytest.fixture(scope="session")
@@ -90,7 +93,7 @@ def mesh():
[1, 2, 3],
Base(name="detached within a list"),
]
mesh.origin = Point(value=[4, 2, 0])
mesh.origin = Point(x=4, y=2)
return mesh
@@ -102,4 +105,5 @@ def base():
base.vertices = [random.uniform(0, 10) for _ in range(1, 120)]
base.test_bases = [Base(name=i) for i in range(1, 22)]
base["@detach"] = Base(name="detached base")
base["@revit_thing"] = Base.of_type("SpecialRevitFamily", name="secret tho")
return base
+42 -3
View File
@@ -1,7 +1,10 @@
import pytest
from specklepy.objects import Base
from specklepy.api import operations
from contextlib import ExitStack as does_not_raise
from typing import Dict, List, Optional
import pytest
from specklepy.api import operations
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.base import Base, DataChunk
@pytest.mark.parametrize(
@@ -72,3 +75,39 @@ def test_speckle_type_cannot_be_set(base: Base) -> None:
assert base.speckle_type == "Base"
base.speckle_type = "unset"
assert base.speckle_type == "Base"
def test_base_of_custom_speckle_type() -> None:
b1 = Base.of_type("BirdHouse", name="Tweety's Crib")
assert b1.speckle_type == "BirdHouse"
assert b1.name == "Tweety's Crib"
class FrozenYoghurt(Base):
"""Testing type checking"""
servings: int
flavours: List[str] # list item types won't be checked
customer: str
add_ons: Optional[Dict[str, float]] # dict item types won't be checked
price: float = 0.0
def test_type_checking() -> None:
order = FrozenYoghurt()
order.servings = 2
order.price = "7" # will get converted
order.customer = "izzy"
with pytest.raises(SpeckleException):
order.flavours = "not a list"
with pytest.raises(SpeckleException):
order.servings = "five"
with pytest.raises(SpeckleException):
order.add_ons = ["sprinkles"]
order.add_ons = {"sprinkles": 0.2, "chocolate": 1.0}
order.flavours = ["strawberry", "lychee", "peach", "pineapple"]
assert order.price == 7.0
+1 -1
View File
@@ -58,7 +58,7 @@ class TestBranch:
assert isinstance(branches, list)
assert len(branches) == 2
assert isinstance(branches[0], Branch)
assert branches[0].name == branch.name
assert branches[1].name == branch.name
def test_branch_update(self, client, stream, branch, updated_branch):
updated = client.branch.update(
+17
View File
@@ -68,3 +68,20 @@ class TestCommit:
deleted = client.commit.delete(stream_id=stream.id, commit_id=commit_id)
assert deleted is True
def test_commit_marked_as_received(self, client, stream, mesh) -> None:
commit = Commit(message="this commit should be received")
commit.id = client.commit.create(
stream_id=stream.id,
object_id=mesh.id,
message=commit.message,
)
commit_marked_received = client.commit.received(
stream.id,
commit.id,
source_application="pytest",
message="testing received",
)
assert commit_marked_received == True
+452
View File
@@ -0,0 +1,452 @@
import json
from typing import Callable
import pytest
from specklepy.api import operations
from specklepy.objects.base import Base
from specklepy.objects.encoding import CurveArray, ObjectArray
from specklepy.objects.geometry import (
Arc,
Box,
Brep,
BrepEdge,
BrepFace,
BrepLoop,
BrepTrim,
BrepTrimTypeEnum,
Circle,
Curve,
Ellipse,
Interval,
Line,
Mesh,
Plane,
Point,
Polycurve,
Polyline,
Surface,
Vector,
)
from specklepy.transports.memory import MemoryTransport
@pytest.fixture()
def interval():
return Interval(start=0, end=5)
@pytest.fixture()
def point():
return Point(x=1, y=10, z=0)
@pytest.fixture()
def vector():
return Vector(x=1, y=32, z=10)
@pytest.fixture()
def plane(point, vector):
return Plane(
origin=point,
normal=vector,
xdir=vector,
ydir=vector,
)
@pytest.fixture()
def box(plane, interval):
return Box(
basePlane=plane,
ySize=interval,
zSize=interval,
xSize=interval,
area=20.4,
volume=44.2,
)
@pytest.fixture()
def line(point, interval):
return Line(
start=point,
end=point,
domain=interval,
# These attributes are not handled in C#
# bbox=None,
# length=None
)
@pytest.fixture()
def arc(plane, interval):
return Arc(
radius=2.3,
startAngle=22.1,
endAngle=44.5,
angleRadians=33,
plane=plane,
domain=interval,
units="m",
# These attributes are not handled in C#
# bbox=None,
# area=None,
# length=None,
# startPoint=None,
# midPoint=None,
# endPoint=None,
)
@pytest.fixture()
def circle(plane, interval):
return Circle(
radius=22,
plane=plane,
domain=interval,
units="m",
# These attributes are not handled in C#
# bbox=None,
# area=None,
# length=None,
)
@pytest.fixture()
def ellipse(plane, interval):
return Ellipse(
firstRadius=34,
secondRadius=22,
plane=plane,
domain=interval,
units="m",
# These attributes are not handled in C#
# trimDomain=None,
# bbox=None,
# area=None,
# length=None,
)
@pytest.fixture()
def polyline(interval):
return Polyline(
value=[22, 44, 54.3, 99, 232, 21],
closed=True,
domain=interval,
units="m",
# These attributes are not handled in C#
# bbox=None,
# area=None,
# length=None,
)
@pytest.fixture()
def curve(interval):
return Curve(
degree=90,
periodic=True,
rational=False,
closed=True,
domain=interval,
points=[23, 21, 44, 43, 56, 76, 1, 3, 2],
weights=[23, 11, 23],
knots=[22, 45, 76, 11],
units="m",
# These attributes are not handled in C#
# displayValue=None,
# bbox=None,
# area=None,
# length=None,
)
@pytest.fixture()
def polycurve(interval, curve, polyline):
return Polycurve(
segments=[curve, polyline],
domain=interval,
closed=True,
units="m",
# These attributes are not handled in C#
# bbox=None,
# area=None,
# length=None
)
@pytest.fixture()
def mesh(box):
return Mesh(
vertices=[2, 1, 2, 4, 77.3, 5, 33, 4, 2],
faces=[1, 2, 3, 4, 5, 6, 7],
colors=[111, 222, 333, 444, 555, 666, 777],
bbox=box,
area=233,
volume=232.2,
)
@pytest.fixture()
def surface(interval):
return Surface(
degreeU=33,
degreeV=44,
rational=True,
pointData=[1, 2.2, 3, 4, 5, 6, 7, 8, 9],
countU=3,
countV=4,
closedU=True,
closedV=False,
domainU=interval,
domainV=interval,
knotsU=[1.1, 2.2, 3.3, 4.4],
knotsV=[9, 8, 7, 6, 5, 4.4],
units="m",
# These attributes are not handled in C#
# bbox=None,
# area=None,
)
@pytest.fixture()
def brep_face():
return BrepFace(
SurfaceIndex=3,
LoopIndices=[1, 2, 3, 4],
OuterLoopIndex=2,
OrientationReversed=False,
)
@pytest.fixture()
def brep_edge(interval):
return BrepEdge(
Curve3dIndex=2,
TrimIndices=[4, 5, 6, 7],
StartIndex=2,
EndIndex=6,
ProxyCurveIsReversed=True,
Domain=interval,
)
@pytest.fixture()
def brep_loop():
return BrepLoop(FaceIndex=5, TrimIndices=[3, 4, 5], Type="unknown")
@pytest.fixture()
def brep_trim():
return BrepTrim(
EdgeIndex=3,
StartIndex=4,
EndIndex=6,
FaceIndex=1,
LoopIndex=4,
CurveIndex=7,
IsoStatus=6,
TrimType="Mated",
IsReversed=False,
# These attributes are not handled in C#
# Domain=None,
)
@pytest.fixture
def brep(
mesh,
box,
surface,
curve,
polyline,
circle,
point,
brep_edge,
brep_loop,
brep_trim,
brep_face,
):
return Brep(
provenance="pytest",
bbox=box,
area=32,
volume=54,
displayValue=mesh,
Surfaces=[surface, surface, surface],
Curve3D=[curve, polyline],
Curve2D=[circle],
Vertices=[point, point, point, point],
Edges=[brep_edge],
Loops=[brep_loop, brep_loop],
Trims=[brep_trim],
Faces=[brep_face, brep_face],
IsClosed=False,
Orientation=3,
)
@pytest.fixture
def geometry_objects_dict(
point,
vector,
plane,
line,
arc,
circle,
ellipse,
polyline,
curve,
polycurve,
surface,
brep_trim,
):
return {
"point": point,
"vector": vector,
"plane": plane,
"line": line,
"arc": arc,
"circle": circle,
"ellipse": ellipse,
"polyline": polyline,
"curve": curve,
"polycurve": polycurve,
"surface": surface,
"brep_trim": brep_trim,
}
@pytest.mark.parametrize(
"object_name",
[
"point",
"vector",
"plane",
"line",
"arc",
"circle",
"ellipse",
"polyline",
"curve",
"polycurve",
"surface",
"brep_trim",
],
)
def test_to_and_from_list(object_name: str, geometry_objects_dict):
object = geometry_objects_dict[object_name]
assert hasattr(object, "to_list")
assert hasattr(object, "from_list")
chunks = object.to_list()
assert isinstance(chunks, list)
object_class = object.__class__
decoded_object: Base = object_class.from_list(chunks)
assert decoded_object.get_id() == object.get_id()
def test_brep_surfaces_value_serialization(surface):
brep = Brep()
assert brep.Surfaces == None
assert brep.SurfacesValue == None
brep.Surfaces = [surface, surface]
assert brep.SurfacesValue == ObjectArray.from_objects([surface, surface]).data
brep.SurfacesValue = ObjectArray.from_objects([surface]).data
assert len(brep.Surfaces) == 1
assert brep.Surfaces[0].get_id() == surface.get_id()
def test_brep_curve2d_values_serialization(curve, polyline, circle):
brep = Brep()
assert brep.Curve2D == None
assert brep.Curve2DValues == None
brep.Curve2D = [curve, polyline]
assert brep.Curve2DValues == CurveArray.from_curves([curve, polyline]).data
brep.Curve2DValues = CurveArray.from_curves([circle]).data
assert len(brep.Curve2D) == 1
assert brep.Curve2D[0].get_id() == circle.get_id()
def test_brep_curve3d_values_serialization(curve, polyline, circle):
brep = Brep()
assert brep.Curve3D == None
assert brep.Curve3DValues == None
brep.Curve3D = [curve, polyline]
assert brep.Curve3DValues == CurveArray.from_curves([curve, polyline]).data
brep.Curve3DValues = CurveArray.from_curves([circle]).data
assert len(brep.Curve3D) == 1
assert brep.Curve3D[0].get_id() == circle.get_id()
def test_brep_vertices_values_serialization():
brep = Brep()
brep.VerticesValue = [1, 1, 1, 1, 2, 2, 2, 3, 3, 3]
brep.Vertices[0].get_id() == Point(x=1, y=1, z=1, _units="mm").get_id()
brep.Vertices[1].get_id() == Point(x=2, y=2, z=2, _units="mm").get_id()
brep.Vertices[2].get_id() == Point(x=3, y=3, z=3, _units="mm").get_id()
def test_trims_value_serialization():
brep = Brep()
brep.TrimsValue = [
0,
0,
0,
0,
0,
0,
1,
1,
1,
1,
0,
0,
0,
0,
1,
2,
1,
0,
]
brep.Trims[0].get_id() == BrepTrim(
EdgeIndex=0,
StartIndex=0,
EndIndex=0,
FaceIndex=0,
LoopIndex=0,
CurveIndex=0,
IsoStatus=1,
TrimType=BrepTrimTypeEnum.Boundary,
IsReversed=False,
).get_id()
brep.Trims[1].get_id() == BrepTrim(
EdgeIndex=1,
StartIndex=0,
EndIndex=0,
FaceIndex=0,
LoopIndex=0,
CurveIndex=1,
IsoStatus=2,
TrimType=BrepTrimTypeEnum.Boundary,
IsReversed=True,
).get_id()
def test_serialized_brep_attributes(brep: Brep):
transport = MemoryTransport()
serialized = operations.serialize(brep, [transport])
serialized_dict = json.loads(serialized)
removed_keys = ["Surfaces", "Curve3D", "Curve2D", "Vertices", "Trims"]
for k in removed_keys:
assert k not in serialized_dict.keys()
+14 -6
View File
@@ -1,8 +1,9 @@
from specklepy.objects import Base
from specklepy.transports.memory import MemoryTransport
from specklepy.api.models import Stream
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
import pytest
from specklepy.api.models import Stream
from specklepy.objects import Base
from specklepy.objects.encoding import ObjectArray
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
from specklepy.transports.sqlite import SQLiteTransport
class TestObject:
@@ -19,12 +20,13 @@ class TestObject:
return stream
def test_object_create(self, client, stream, base):
transport = MemoryTransport()
transport = SQLiteTransport()
s = BaseObjectSerializer(write_transports=[transport], read_transport=transport)
_, base_dict = s.traverse_base(base)
obj_id = client.object.create(stream_id=stream.id, objects=[base_dict])[0]
assert isinstance(obj_id, str)
assert base_dict["@detach"]["speckle_type"] == "reference"
assert obj_id == base.get_id(True)
def test_object_get(self, client, stream, base):
@@ -35,4 +37,10 @@ class TestObject:
assert isinstance(fetched_base, Base)
assert fetched_base.name == base.name
assert isinstance(fetched_base.vertices, list)
assert fetched_base["@detach"]["speckle_type"] == "reference"
# assert fetched_base["@detach"]["speckle_type"] == "reference"
def test_object_array_decoder(self):
array = ObjectArray()
array.data = [5, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 3, 1, 1, 1, 2, 1, 1, 1, 1]
assert array.decode(decoder=sum) == [5, 4, 3, 2, 1]
+8 -3
View File
@@ -1,5 +1,4 @@
import json
from attr import has
import pytest
from specklepy.api import operations
from specklepy.transports.server import ServerTransport
@@ -19,6 +18,7 @@ class TestSerialization:
assert base.get_id() == deserialized.get_id()
assert base.units == "mm"
assert isinstance(base.test_bases[0], Base)
assert base["@revit_thing"].speckle_type == "SpecialRevitFamily"
assert base["@detach"].name == deserialized["@detach"].name
def test_detaching(self, mesh):
@@ -52,6 +52,11 @@ class TestSerialization:
def test_send_and_receive(self, client, sample_stream, mesh):
transport = ServerTransport(client=client, stream_id=sample_stream.id)
hash = operations.send(mesh, transports=[transport])
# also try constructing server transport with token and url
transport = ServerTransport(
stream_id=sample_stream.id, token=client.me["token"], url=client.url
)
# use a fresh memory transport to force receiving from remote
received = operations.receive(
hash, remote_transport=transport, local_transport=MemoryTransport()
@@ -60,7 +65,7 @@ class TestSerialization:
assert isinstance(received, FakeMesh)
assert received.vertices == mesh.vertices
assert isinstance(received.origin, Point)
assert received.origin.value == mesh.origin.value
assert received.origin.x == mesh.origin.x
# not comparing hashes as order is not guaranteed back from server
mesh.id = hash # populate with decomposed id for use in proceeding tests
@@ -83,4 +88,4 @@ class TestSerialization:
untyped = '{"foo": "bar"}'
deserialised = operations.deserialize(untyped)
assert deserialised == {"foo": "bar"}
assert deserialised == {"foo": "bar"}
+79
View File
@@ -0,0 +1,79 @@
import pytest
from specklepy.api.credentials import StreamWrapper
class TestWrapper:
def test_parse_stream(self):
wrap = StreamWrapper("https://testing.speckle.dev/streams/a75ab4f10f")
assert wrap.type == "stream"
def test_parse_branch(self):
wacky_wrap = StreamWrapper(
"https://testing.speckle.dev/streams/4c3ce1459c/branches/%F0%9F%8D%95%E2%AC%85%F0%9F%8C%9F%20you%20wat%3F"
)
wrap = StreamWrapper(
"https://testing.speckle.dev/streams/4c3ce1459c/branches/next%20level"
)
assert wacky_wrap.type == "branch"
assert wacky_wrap.branch_name == "🍕⬅🌟 you wat?"
assert wrap.type == "branch"
def test_parse_nested_branch(self):
wrap = StreamWrapper(
"https://testing.speckle.dev/streams/4c3ce1459c/branches/izzy/dev"
)
assert wrap.branch_name == "izzy/dev"
assert wrap.type == "branch"
def test_parse_commit(self):
wrap = StreamWrapper(
"https://testing.speckle.dev/streams/4c3ce1459c/commits/8b9b831792"
)
assert wrap.type == "commit"
def test_parse_object(self):
wrap = StreamWrapper(
"https://testing.speckle.dev/streams/a75ab4f10f/objects/5530363e6d51c904903dafc3ea1d2ec6"
)
assert wrap.type == "object"
def test_parse_globals_as_branch(self):
wrap = StreamWrapper("https://testing.speckle.dev/streams/0c6ad366c4/globals/")
assert wrap.type == "branch"
def test_parse_globals_as_commit(self):
wrap = StreamWrapper(
"https://testing.speckle.dev/streams/0c6ad366c4/globals/abd3787893"
)
assert wrap.type == "commit"
#! NOTE: the following three tests may not pass locally if you have a `speckle.xyz` account in manager
def test_get_client_without_auth(self):
wrap = StreamWrapper(
"https://speckle.xyz/streams/4c3ce1459c/commits/8b9b831792"
)
client = wrap.get_client()
assert client is not None
def test_get_new_client_with_token(self):
wrap = StreamWrapper(
"https://speckle.xyz/streams/4c3ce1459c/commits/8b9b831792"
)
client = wrap.get_client()
client = wrap.get_client(token="super-secret-token")
assert client.me["token"] == "super-secret-token"
def test_get_transport_with_token(self):
wrap = StreamWrapper(
"https://speckle.xyz/streams/4c3ce1459c/commits/8b9b831792"
)
client = wrap.get_client()
assert not client.me # unauthenticated bc no local accounts
transport = wrap.get_transport(token="super-secret-token")
assert transport is not None
assert client.me["token"] == "super-secret-token"