Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b50e658333 | |||
| 88248353ab | |||
| aec94f8f7f | |||
| e6b1604bc3 | |||
| de29b93b8b | |||
| 10aa8b59b6 | |||
| b86faa6a14 | |||
| 7430611c52 | |||
| ddd52f4af9 | |||
| 35bc6b0350 | |||
| 9585d46c4e | |||
| fd09e97a53 | |||
| 459bd0901f | |||
| ae7c4bc14d | |||
| 41f1823aae | |||
| 625bd5cd84 | |||
| 8812985c67 | |||
| c838835a65 | |||
| 361ba6bfcd | |||
| 8078a4b596 | |||
| 08b106464f | |||
| 0cfe5db674 | |||
| d9dbca2c68 | |||
| ab5b55871b | |||
| 2f87956154 | |||
| 6fc4ab1539 | |||
| 7f432e768d | |||
| d0eb364b3e | |||
| 936b2d8b5a | |||
| 5a9aec80bd | |||
| 6616526279 | |||
| 6e00de58b9 | |||
| 0ec404bbec | |||
| 506aaf68ca | |||
| 9fa6b661eb | |||
| d1adccba00 | |||
| 9e30250446 | |||
| 3e7d657e2e | |||
| 7484d8441b | |||
| 682afce05f | |||
| 21b27e2f3b | |||
| 69cd9706cf | |||
| 98075fa2cf | |||
| 782f70fb49 | |||
| 52ab27e60f |
@@ -1,16 +1,16 @@
|
|||||||
version: 2.1
|
version: 2.1
|
||||||
|
|
||||||
orbs:
|
orbs:
|
||||||
python: circleci/python@1.3.2
|
python: circleci/python@2.0.3
|
||||||
codecov: codecov/codecov@3.2.2
|
codecov: codecov/codecov@3.2.2
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
docker:
|
docker:
|
||||||
- image: "cimg/python:<<parameters.tag>>"
|
- image: "cimg/python:<<parameters.tag>>"
|
||||||
- image: 'cimg/node:14.18'
|
- image: "cimg/node:16.15"
|
||||||
- image: 'circleci/redis:6'
|
- image: "cimg/redis:6.2"
|
||||||
- image: 'cimg/postgres:12.8'
|
- image: "cimg/postgres:14.2"
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: speckle2_test
|
POSTGRES_DB: speckle2_test
|
||||||
POSTGRES_PASSWORD: speckle
|
POSTGRES_PASSWORD: speckle
|
||||||
@@ -27,6 +27,7 @@ jobs:
|
|||||||
STRATEGY_LOCAL: "true"
|
STRATEGY_LOCAL: "true"
|
||||||
CANONICAL_URL: "http://localhost:3000"
|
CANONICAL_URL: "http://localhost:3000"
|
||||||
WAIT_HOSTS: localhost:5432, localhost:6379
|
WAIT_HOSTS: localhost:5432, localhost:6379
|
||||||
|
DISABLE_FILE_UPLOADS: "true"
|
||||||
parameters:
|
parameters:
|
||||||
tag:
|
tag:
|
||||||
default: "3.8"
|
default: "3.8"
|
||||||
@@ -51,7 +52,7 @@ jobs:
|
|||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
docker:
|
docker:
|
||||||
- image: "circleci/python:3.8"
|
- image: "cimg/python:3.8"
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- run: python patch_version.py $CIRCLE_TAG
|
- run: python patch_version.py $CIRCLE_TAG
|
||||||
@@ -60,17 +61,17 @@ jobs:
|
|||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
main:
|
main:
|
||||||
jobs:
|
jobs:
|
||||||
- test:
|
- test:
|
||||||
matrix:
|
matrix:
|
||||||
parameters:
|
parameters:
|
||||||
tag: ["3.6", "3.7", "3.8", "3.9"]
|
tag: ["3.7", "3.8", "3.9", "3.10"]
|
||||||
filters:
|
filters:
|
||||||
tags:
|
tags:
|
||||||
only: /.*/
|
only: /.*/
|
||||||
- deploy:
|
- deploy:
|
||||||
requires:
|
requires:
|
||||||
- test
|
- test
|
||||||
filters:
|
filters:
|
||||||
tags:
|
tags:
|
||||||
only: /[0-9]+(\.[0-9]+)*/
|
only: /[0-9]+(\.[0-9]+)*/
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.191.1/containers/python-3/.devcontainer/base.Dockerfile
|
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/blob/main/containers/python-3/.devcontainer/base.Dockerfile
|
||||||
|
|
||||||
# [Choice] Python version: 3, 3.9, 3.8, 3.7, 3.6
|
# [Choice] Python version: 3, 3.9, 3.8, 3.7, 3.6
|
||||||
ARG VARIANT="3.9"
|
ARG VARIANT="3.10"
|
||||||
FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}
|
FROM mcr.microsoft.com/vscode/devcontainers/python:${VARIANT}
|
||||||
|
|
||||||
# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
|
# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
|
||||||
ARG NODE_VERSION="none"
|
ARG NODE_VERSION="16"
|
||||||
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
|
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
|
||||||
|
|
||||||
# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image.
|
# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image.
|
||||||
|
|||||||
@@ -22,6 +22,9 @@
|
|||||||
"python.languageServer": "Pylance",
|
"python.languageServer": "Pylance",
|
||||||
"python.linting.enabled": true,
|
"python.linting.enabled": true,
|
||||||
"python.linting.pylintEnabled": true,
|
"python.linting.pylintEnabled": true,
|
||||||
|
"python.linting.pylintArgs": [
|
||||||
|
"--max-line-length=120"
|
||||||
|
],
|
||||||
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
|
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
|
||||||
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
|
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
|
||||||
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
|
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
version: "3.3" # optional since v1.27.0
|
version: "3.3" # optional since v1.27.0
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: circleci/postgres:12
|
image: cimg/postgres:14.2
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: speckle2_test
|
POSTGRES_DB: speckle2_test
|
||||||
POSTGRES_PASSWORD: speckle
|
POSTGRES_PASSWORD: speckle
|
||||||
POSTGRES_USER: speckle
|
POSTGRES_USER: speckle
|
||||||
# ports:
|
|
||||||
# - "5432:5432"
|
|
||||||
network_mode: host
|
network_mode: host
|
||||||
redis:
|
redis:
|
||||||
image: circleci/redis:6
|
image: cimg/redis:6.2
|
||||||
# ports:
|
|
||||||
# - "6379:6379"
|
|
||||||
network_mode: host
|
network_mode: host
|
||||||
speckle-server:
|
speckle-server:
|
||||||
image: speckle/speckle-server
|
image: speckle/speckle-server:latest
|
||||||
command: ["bash", "-c", "/wait && node bin/www"]
|
command: ["bash", "-c", "/wait && node bin/www"]
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_URL: "localhost"
|
POSTGRES_URL: "localhost"
|
||||||
@@ -27,8 +23,7 @@ services:
|
|||||||
STRATEGY_LOCAL: "true"
|
STRATEGY_LOCAL: "true"
|
||||||
CANONICAL_URL: "http://localhost:3000"
|
CANONICAL_URL: "http://localhost:3000"
|
||||||
WAIT_HOSTS: localhost:5432, localhost:6379
|
WAIT_HOSTS: localhost:5432, localhost:6379
|
||||||
# ports:
|
DISABLE_FILE_UPLOADS: "true"
|
||||||
# - "3000:3000"
|
|
||||||
network_mode: host
|
network_mode: host
|
||||||
|
|
||||||
specklepy:
|
specklepy:
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
---
|
|
||||||
name: New issue
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title:
|
|
||||||
labels:
|
|
||||||
assignees:
|
|
||||||
---
|
|
||||||
|
|
||||||
If it's your first time here - or you forgot about them - make sure you read the [contribution guidelines](CONTRIBUTING.md), and then feel free to delete this line!
|
|
||||||
|
|
||||||
### Expected vs. Actual Behavior
|
|
||||||
|
|
||||||
Describe the problem here.
|
|
||||||
|
|
||||||
### Reproduction Steps & System Config (win, osx, web, etc.)
|
|
||||||
|
|
||||||
Let us know how we can reproduce this, and attach relevant files (if any).
|
|
||||||
|
|
||||||
### Proposed Solution (if any)
|
|
||||||
|
|
||||||
Let us know what how you would solve this.
|
|
||||||
|
|
||||||
#### Optional: Affected Projects
|
|
||||||
|
|
||||||
Does this issue propagate to other dependencies or dependents? If so, list them here!
|
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Help improve Speckle!
|
||||||
|
title: ''
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
<!---
|
||||||
|
|
||||||
|
Provide a short summary in the Title above. Examples of good Issue titles:
|
||||||
|
|
||||||
|
* "Bug: Error from server when reticulating splines"
|
||||||
|
* "Bug: Revit crashes when installing connector"
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
<!---
|
||||||
|
|
||||||
|
Please answer the following questions before submitting an issue.
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
- [ ] I read the [contribution guidelines](https://github.com/specklesystems/speckle-server/blob/main/CONTRIBUTING.md)
|
||||||
|
- [ ] I checked the [documentation](https://speckle.guide/) and found no answer.
|
||||||
|
- [ ] I checked [existing issues](../issues?q=is%3Aissue) and found no similar issue. <!-- If you do find an existing issue, please show your support by liking it :+1: instead of creating a new issue -->
|
||||||
|
- [ ] I checked the [community forum](https://speckle.community/) for related discussions and found no answer.
|
||||||
|
- [ ] I'm reporting the issue to the correct repository (see also [speckle-server](https://github.com/specklesystems/speckle-server), [speckle-sharp](https://github.com/specklesystems/speckle-sharp), [specklepy](https://github.com/specklesystems/specklepy), [speckle-docs](https://github.com/specklesystems/speckle-docs), and [others](https://github.com/orgs/specklesystems/repositories))
|
||||||
|
|
||||||
|
## What package are you referring to?
|
||||||
|
|
||||||
|
<!---
|
||||||
|
Is it related to the server (backend) only, or does this bug relate to the frontend, viewer, objectloader or any other package?
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Describe the bug
|
||||||
|
|
||||||
|
<!---
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## To Reproduce
|
||||||
|
|
||||||
|
<!---
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Expected behavior
|
||||||
|
|
||||||
|
<!---
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
<!---
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## System Info
|
||||||
|
|
||||||
|
If applicable, please fill in the below details - they help a lot!
|
||||||
|
|
||||||
|
### Desktop (please complete the following information):
|
||||||
|
|
||||||
|
- OS: [e.g. iOS]
|
||||||
|
- Browser [e.g. chrome, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
### Smartphone (please complete the following information):
|
||||||
|
|
||||||
|
- Device: [e.g. iPhone6]
|
||||||
|
- OS: [e.g. iOS8.1]
|
||||||
|
- Browser [e.g. stock browser, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
## Failure Logs
|
||||||
|
|
||||||
|
<!---
|
||||||
|
Please include any relevant log snippets or files here, or upload as a file.
|
||||||
|
|
||||||
|
If including inline, please use markdown code block syntax. https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
your log output here
|
||||||
|
```
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Additional context
|
||||||
|
|
||||||
|
<!---
|
||||||
|
Add any other context about the problem here.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Proposed Solution (if any)
|
||||||
|
|
||||||
|
<!---
|
||||||
|
Let us know what how you would solve this.
|
||||||
|
-->
|
||||||
|
|
||||||
|
#### Optional: Affected Projects
|
||||||
|
|
||||||
|
<!---
|
||||||
|
Does this issue propagate to other dependencies or dependents? If so, list them here with links!
|
||||||
|
-->
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for Speckle!
|
||||||
|
title: ''
|
||||||
|
labels: enhancement, question
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
<!---
|
||||||
|
|
||||||
|
Provide a short summary in the Title above. Examples of good Issue titles:
|
||||||
|
|
||||||
|
* "Enhancement: Connector for Minecraft"
|
||||||
|
* "Enhancement: Web viewer should support tesseracts"
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
<!---
|
||||||
|
|
||||||
|
Please answer the following questions before submitting an issue.
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
- [ ] I read the [contribution guidelines](https://github.com/specklesystems/speckle-server/blob/main/CONTRIBUTING.md)
|
||||||
|
- [ ] I checked the [documentation](https://speckle.guide/) and found no answer.
|
||||||
|
- [ ] I checked [existing issues](../issues?q=is%3Aissue) and found no similar issue. <!-- If you do find an existing issue, please show your support by liking it :+1: instead of creating a new issue -->
|
||||||
|
- [ ] I checked the [community forum](https://speckle.community/) for related discussions and found no answer.
|
||||||
|
- [ ] I'm requesting the feature to the correct repository (see also [speckle-server](https://github.com/specklesystems/speckle-server), [speckle-sharp](https://github.com/specklesystems/speckle-sharp), [specklepy](https://github.com/specklesystems/specklepy), [speckle-docs](https://github.com/specklesystems/speckle-docs), and [others](https://github.com/orgs/specklesystems/repositories))
|
||||||
|
|
||||||
|
## What package are you referring to?
|
||||||
|
|
||||||
|
<!---
|
||||||
|
Is it related to the server (backend) only, or does this feature request relate to the frontend, viewer, objectloader or any other package?
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Is your feature request related to a problem? Please describe.
|
||||||
|
|
||||||
|
<!---
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Describe the solution you'd like
|
||||||
|
|
||||||
|
<!---
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Describe alternatives you've considered
|
||||||
|
|
||||||
|
<!---
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Additional context
|
||||||
|
|
||||||
|
<!---
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
|
|
||||||
|
Have you seen this feature implemented in any other software? Can you provide screenshots or links to video or documentation?
|
||||||
|
What works well about these existing features in other software? What doesn't work well?
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Related issues or community discussions
|
||||||
|
|
||||||
|
<!---
|
||||||
|
Is this feature request related to (but sufficiently distinct from) any existing issues?
|
||||||
|
Does this feature request require other features to be available beforehand?
|
||||||
|
Has this feature been discussed in the community forum, please link here? https://speckle.community/
|
||||||
|
-->
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
Description of PR...
|
|
||||||
|
|
||||||
## Changes
|
|
||||||
|
|
||||||
- Item 1
|
|
||||||
- Item 2
|
|
||||||
|
|
||||||
## Checklist
|
|
||||||
|
|
||||||
- [ ] Unit tests
|
|
||||||
- [ ] Documentation
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
(optional)
|
|
||||||
|
|
||||||
Include **important** links regarding the implementation of this PR.
|
|
||||||
This usually includes and RFC or an aggregation of issues and/or individual conversations
|
|
||||||
that helped put this solution together. This helps ensure there is a good aggregation
|
|
||||||
of resources regarding the implementation.
|
|
||||||
|
|
||||||
```text
|
|
||||||
Fixes #85, Fixes #22, Fixes username/repo#123
|
|
||||||
Connects #123
|
|
||||||
```
|
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<!---
|
||||||
|
|
||||||
|
Provide a short summary in the Title above. Examples of good PR titles:
|
||||||
|
|
||||||
|
* "Feature: adds metrics to component"
|
||||||
|
|
||||||
|
* "Fix: resolves duplication in comment thread"
|
||||||
|
|
||||||
|
* "Update: apollo v2.34.0"
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Description & motivation
|
||||||
|
|
||||||
|
<!---
|
||||||
|
|
||||||
|
Describe your changes, and why you're making them. What benefit will this have to others?
|
||||||
|
|
||||||
|
Is this linked to an open Github issue, a thread in Speckle community,
|
||||||
|
or another pull request? Link it here.
|
||||||
|
|
||||||
|
If it is related to a Github issue, and resolves it, please link to the issue number, e.g.:
|
||||||
|
Fixes #85, Fixes #22, Fixes username/repo#123
|
||||||
|
Connects #123
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Changes:
|
||||||
|
|
||||||
|
<!---
|
||||||
|
|
||||||
|
- Item 1
|
||||||
|
- Item 2
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
## To-do before merge:
|
||||||
|
|
||||||
|
<!---
|
||||||
|
|
||||||
|
(Optional -- remove this section if not needed)
|
||||||
|
|
||||||
|
Include any notes about things that need to happen before this PR is merged, e.g.:
|
||||||
|
|
||||||
|
- [ ] Change the base branch
|
||||||
|
|
||||||
|
- [ ] Ensure PR #56 is merged
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Screenshots:
|
||||||
|
|
||||||
|
<!---
|
||||||
|
|
||||||
|
Include a screenshot the before and after. This can be a screenshot of a plugin, web frontend, or output in a terminal.
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Validation of changes:
|
||||||
|
|
||||||
|
<!---
|
||||||
|
|
||||||
|
Describe what tests have been added or amended, and why these demonstrate it works and will prevent this feature being accidentally broken by future changes.
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Checklist:
|
||||||
|
|
||||||
|
<!---
|
||||||
|
|
||||||
|
This checklist is mostly useful as a reminder of small things that can easily be
|
||||||
|
|
||||||
|
forgotten – it is meant as a helpful tool rather than hoops to jump through.
|
||||||
|
|
||||||
|
Put an `x` between the square brackets, e.g. [x], for all the items that apply,
|
||||||
|
|
||||||
|
make notes next to any that haven't been addressed, and remove any items that are not relevant to this PR.
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
- [ ] My pull request follows the guidelines in the [Contributing guide](https://github.com/specklesystems/speckle-server/blob/main/CONTRIBUTING.md)?
|
||||||
|
- [ ] My pull request does not duplicate any other open [Pull Requests](../../pulls) for the same update/change?
|
||||||
|
- [ ] My commits are related to the pull request and do not amend unrelated code or documentation.
|
||||||
|
- [ ] My code follows a similar style to existing code.
|
||||||
|
- [ ] I have added appropriate tests.
|
||||||
|
- [ ] I have updated or added relevant documentation.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
<!---
|
||||||
|
|
||||||
|
(Optional -- remove this section if not needed )
|
||||||
|
|
||||||
|
Include **important** links regarding the implementation of this PR.
|
||||||
|
|
||||||
|
This usually includes a RFC or an aggregation of issues and/or individual conversations
|
||||||
|
|
||||||
|
that helped put this solution together. This helps ensure we retain and share knowledge
|
||||||
|
|
||||||
|
regarding the implementation, and may help others understand motivation and design decisions etc..
|
||||||
|
|
||||||
|
-->
|
||||||
@@ -32,10 +32,10 @@ jobs:
|
|||||||
|
|
||||||
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
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 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||||
|
|
||||||
echo "$PROJECT_ID"
|
echo "$PROJECT_ID"
|
||||||
echo "$STATUS_FIELD_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='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .settings | fromjson | .options[] | select(.name== "Done") | .id' project_data.json) >> $GITHUB_ENV
|
||||||
echo "$DONE_ID"
|
echo "$DONE_ID"
|
||||||
|
|
||||||
@@ -52,9 +52,9 @@ jobs:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||||
|
|
||||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Update Status
|
- name: Update Status
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
||||||
@@ -75,4 +75,3 @@ jobs:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}' -f project=$PROJECT_ID -f status=$STATUS_FIELD_ID -f id=$ITEM_ID -f value=${{ env.DONE_ID }}
|
}' -f project=$PROJECT_ID -f status=$STATUS_FIELD_ID -f id=$ITEM_ID -f value=${{ env.DONE_ID }}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
|
|
||||||
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
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 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Add Issue to project
|
- name: Add Issue to project
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
|
||||||
@@ -46,5 +46,5 @@ jobs:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||||
|
|
||||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||||
|
|||||||
Vendored
+6
-2
@@ -4,6 +4,7 @@
|
|||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "Python: Current File",
|
"name": "Python: Current File",
|
||||||
"type": "python",
|
"type": "python",
|
||||||
@@ -13,10 +14,13 @@
|
|||||||
"justMyCode": false
|
"justMyCode": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Python: Test debug config",
|
"name": "Pytest",
|
||||||
"type": "python",
|
"type": "python",
|
||||||
"request": "test",
|
"request": "launch",
|
||||||
|
"program": "poetry",
|
||||||
|
"args": ["run", "pytest"],
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
|
"justMyCode": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -74,7 +74,7 @@ It may be helpful to know where the local accounts and object cache dbs are stor
|
|||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Please make sure you read the [contribution guidelines](.github/CONTRIBUTING.md) for an overview of the best practices we try to follow.
|
Please make sure you read the [contribution guidelines](.github/CONTRIBUTING.md) and [code of conduct](.github/CODE_OF_CONDUCT.md) for an overview of the practices we try to follow.
|
||||||
|
|
||||||
## Community
|
## Community
|
||||||
|
|
||||||
|
|||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| 2.2.+ | :white_check_mark: |
|
||||||
|
| < 2.2 | :x: |
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Hi! If you've found something off, we'd be more than happy if you would report it via security@speckle.systems. We will work together with you to correctly identify the cause and implement a fix. Thanks for helping make Speckle safer!
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
from typing import List
|
||||||
|
from specklepy.objects import Base
|
||||||
|
from specklepy.api import operations
|
||||||
|
from specklepy.transports.sqlite import SQLiteTransport
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
import string
|
||||||
|
import random
|
||||||
|
|
||||||
|
|
||||||
|
class Sub(Base):
|
||||||
|
bar: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
def random_string():
|
||||||
|
letters = string.ascii_lowercase
|
||||||
|
return "".join(random.choice(letters) for _ in range(10))
|
||||||
|
|
||||||
|
|
||||||
|
BASE_PATH = SQLiteTransport.get_base_path("Speckle")
|
||||||
|
|
||||||
|
|
||||||
|
def clean_db():
|
||||||
|
os.remove(Path(BASE_PATH, "Objects.db"))
|
||||||
|
|
||||||
|
|
||||||
|
def one_pass(clean: bool, randomize: bool, child_count: int):
|
||||||
|
|
||||||
|
foo = Base()
|
||||||
|
for i in range(child_count):
|
||||||
|
stuff = random_string() if randomize else "stuff"
|
||||||
|
foo[f"@child_{i}"] = Sub(bar=["asdf", "bar", i, stuff])
|
||||||
|
|
||||||
|
if clean:
|
||||||
|
clean_db()
|
||||||
|
transport = SQLiteTransport()
|
||||||
|
start = time.time()
|
||||||
|
hash = operations.send(base=foo, transports=[transport])
|
||||||
|
send_time = time.time() - start
|
||||||
|
|
||||||
|
receive_start = time.time()
|
||||||
|
operations.receive(hash, transport)
|
||||||
|
receive_time = time.time() - receive_start
|
||||||
|
return send_time, receive_time
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sample_size = 4
|
||||||
|
|
||||||
|
test_permutations = [
|
||||||
|
(True, True),
|
||||||
|
(False, False),
|
||||||
|
(False, True),
|
||||||
|
(True, False),
|
||||||
|
]
|
||||||
|
for clean, randomize in test_permutations:
|
||||||
|
print(f"CLEAN: {clean}, RANDOMIZE: {randomize}")
|
||||||
|
for child_count in [10, 100, 1000, 10000]:
|
||||||
|
print(f"\tCHILD COUNT: {child_count}")
|
||||||
|
for _ in range(sample_size):
|
||||||
|
send_time, receive_time = one_pass(clean, randomize, child_count)
|
||||||
|
print(f"\t\tSend: {send_time} Receive: {receive_time}")
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
from typing import List
|
||||||
|
from specklepy.api.wrapper import StreamWrapper
|
||||||
|
from specklepy.objects import Base
|
||||||
|
from specklepy.api import operations
|
||||||
|
import string
|
||||||
|
import random
|
||||||
|
|
||||||
|
|
||||||
|
class Sub(Base):
|
||||||
|
bar: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
def random_string():
|
||||||
|
letters = string.ascii_lowercase
|
||||||
|
return "".join(random.choice(letters) for _ in range(10))
|
||||||
|
|
||||||
|
|
||||||
|
def create_object(child_count: int) -> Base:
|
||||||
|
foo = Base()
|
||||||
|
for i in range(child_count):
|
||||||
|
stuff = random_string()
|
||||||
|
foo[f"@child_{i}"] = Sub(bar=["asdf", "bar", i, stuff])
|
||||||
|
return foo
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
|
||||||
|
stream_url = "http://hyperion:3000/streams/2372b54c35"
|
||||||
|
|
||||||
|
child_count = 10
|
||||||
|
foo = create_object(child_count)
|
||||||
|
|
||||||
|
wrapper = StreamWrapper(stream_url)
|
||||||
|
transport = wrapper.get_transport()
|
||||||
|
|
||||||
|
hash = operations.send(base=foo, transports=[transport], use_default_cache=False)
|
||||||
|
|
||||||
|
rec = operations.receive(hash, transport)
|
||||||
|
print(rec)
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from specklepy.api import operations
|
||||||
|
from specklepy.objects.geometry import Base
|
||||||
|
from specklepy.objects.units import Units
|
||||||
|
|
||||||
|
dct = {
|
||||||
|
"id": "1234abcd",
|
||||||
|
"units": None,
|
||||||
|
"speckle_type": "Base",
|
||||||
|
"applicationId": None,
|
||||||
|
"totalChildrenCount": 0,
|
||||||
|
}
|
||||||
|
base = Base()
|
||||||
|
for prop, value in dct.items():
|
||||||
|
base.__setattr__(prop, value)
|
||||||
|
|
||||||
|
from devtools import debug
|
||||||
|
|
||||||
|
debug(base)
|
||||||
|
debug(base.units)
|
||||||
|
|
||||||
|
base.units = "m"
|
||||||
|
debug(base.units)
|
||||||
|
base.units = None
|
||||||
|
|
||||||
|
debug(base.units)
|
||||||
|
|
||||||
|
foo = operations.serialize(base)
|
||||||
|
|
||||||
|
base.units = 10
|
||||||
|
|
||||||
|
debug(base.units)
|
||||||
|
debug(foo)
|
||||||
|
|
||||||
|
base.units = Units.mm
|
||||||
|
debug(base.units)
|
||||||
@@ -50,10 +50,10 @@ if __name__ == "__main__":
|
|||||||
)
|
)
|
||||||
# support for dynamic attributes
|
# support for dynamic attributes
|
||||||
custom_sub.extra_extra = "what is this?"
|
custom_sub.extra_extra = "what is this?"
|
||||||
debug(custom_sub.json())
|
debug(custom_sub)
|
||||||
|
|
||||||
serialized = operations.serialize(custom_sub)
|
serialized = operations.serialize(custom_sub)
|
||||||
deserialized = operations.deserialize(serialized)
|
deserialized = operations.deserialize(serialized)
|
||||||
# the only difference should be between the two data is that the deserialized
|
# the only difference should be between the two data is that the deserialized
|
||||||
# instance id attribute is not None.
|
# instance id attribute is not None.
|
||||||
debug(deserialized.json())
|
debug(deserialized)
|
||||||
|
|||||||
Generated
+864
-841
File diff suppressed because it is too large
Load Diff
+14
-9
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "specklepy"
|
name = "specklepy"
|
||||||
version = "2.4.0"
|
version = "2.9.1"
|
||||||
description = "The Python SDK for Speckle 2.0"
|
description = "The Python SDK for Speckle 2.0"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = ["Speckle Systems <devops@speckle.systems>"]
|
authors = ["Speckle Systems <devops@speckle.systems>"]
|
||||||
@@ -8,23 +8,28 @@ license = "Apache-2.0"
|
|||||||
repository = "https://github.com/specklesystems/speckle-py"
|
repository = "https://github.com/specklesystems/speckle-py"
|
||||||
documentation = "https://speckle.guide/dev/py-examples.html"
|
documentation = "https://speckle.guide/dev/py-examples.html"
|
||||||
homepage = "https://speckle.systems/"
|
homepage = "https://speckle.systems/"
|
||||||
|
packages = [
|
||||||
|
{ include = "specklepy", from = "src" },
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.6.5"
|
python = ">=3.7.2, <4.0"
|
||||||
pydantic = "^1.8.2"
|
pydantic = "^1.8.2"
|
||||||
appdirs = "^1.4.4"
|
appdirs = "^1.4.4"
|
||||||
gql = {version = ">=3.0.0b1", extras = ["all"], allow-prereleases = true}
|
gql = {extras = ["requests", "websockets"], version = "^3.3.0"}
|
||||||
ujson = "^4.3.0"
|
ujson = "^5.3.0"
|
||||||
Deprecated = "^1.2.13"
|
Deprecated = "^1.2.13"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
black = "^20.8b1"
|
black = "^22.8.0"
|
||||||
isort = "^5.7.0"
|
isort = "^5.7.0"
|
||||||
pytest = "^6.2.2"
|
pytest = "^7.1.3"
|
||||||
pytest-ordering = "^0.6"
|
pytest-ordering = "^0.6"
|
||||||
pytest-cov = "^3.0.0"
|
pytest-cov = "^3.0.0"
|
||||||
|
devtools = "^0.8.0"
|
||||||
|
pylint = "^2.14.4"
|
||||||
|
mypy = "^0.982"
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
exclude = '''
|
exclude = '''
|
||||||
@@ -43,7 +48,7 @@ exclude = '''
|
|||||||
'''
|
'''
|
||||||
include = '\.pyi?$'
|
include = '\.pyi?$'
|
||||||
line-length = 88
|
line-length = 88
|
||||||
target-version = ["py36", "py37", "py38"]
|
target-version = ["py37", "py38", "py39", "py310"]
|
||||||
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
from specklepy.api.credentials import Account
|
|
||||||
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):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
account: Account,
|
|
||||||
basepath: str,
|
|
||||||
client: Client,
|
|
||||||
name: str,
|
|
||||||
methods: list,
|
|
||||||
) -> None:
|
|
||||||
self.account = account
|
|
||||||
self.basepath = basepath
|
|
||||||
self.client = client
|
|
||||||
self.name = name
|
|
||||||
self.methods = methods
|
|
||||||
self.schema = None
|
|
||||||
|
|
||||||
def _step_into_response(self, response: dict, return_type: str or List):
|
|
||||||
"""Step into the dict to get the relevant data"""
|
|
||||||
if return_type is None:
|
|
||||||
return response
|
|
||||||
elif isinstance(return_type, str):
|
|
||||||
return response[return_type]
|
|
||||||
elif isinstance(return_type, List):
|
|
||||||
for key in return_type:
|
|
||||||
response = response[key]
|
|
||||||
return response
|
|
||||||
|
|
||||||
def _parse_response(self, response: dict or list, schema=None):
|
|
||||||
"""Try to create a class instance from the response"""
|
|
||||||
if isinstance(response, list):
|
|
||||||
return [self._parse_response(response=r, schema=schema) for r in response]
|
|
||||||
if schema:
|
|
||||||
return schema.parse_obj(response)
|
|
||||||
elif self.schema:
|
|
||||||
try:
|
|
||||||
return self.schema.parse_obj(response)
|
|
||||||
except:
|
|
||||||
s = BaseObjectSerializer(read_transport=SQLiteTransport())
|
|
||||||
return s.recompose_base(response)
|
|
||||||
else:
|
|
||||||
return response
|
|
||||||
|
|
||||||
def make_request(
|
|
||||||
self,
|
|
||||||
query: gql,
|
|
||||||
params: Dict = None,
|
|
||||||
return_type: str or List = None,
|
|
||||||
schema=None,
|
|
||||||
parse_response: bool = True,
|
|
||||||
) -> Dict or GraphQLException:
|
|
||||||
"""Executes the GraphQL query"""
|
|
||||||
try:
|
|
||||||
response = self.client.execute(query, variable_values=params)
|
|
||||||
except Exception as e:
|
|
||||||
if isinstance(e, TransportQueryError):
|
|
||||||
return GraphQLException(
|
|
||||||
message=f"Failed to execute the GraphQL {self.name} request. Errors: {e.errors}",
|
|
||||||
errors=e.errors,
|
|
||||||
data=e.data,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return SpeckleException(
|
|
||||||
message=f"Failed to execute the GraphQL {self.name} request. Inner exception: {e}",
|
|
||||||
exception=e,
|
|
||||||
)
|
|
||||||
|
|
||||||
response = self._step_into_response(response=response, return_type=return_type)
|
|
||||||
|
|
||||||
if parse_response:
|
|
||||||
return self._parse_response(response=response, schema=schema)
|
|
||||||
else:
|
|
||||||
return response
|
|
||||||
@@ -1,640 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
scalar DateTime
|
|
||||||
|
|
||||||
scalar EmailAddress
|
|
||||||
|
|
||||||
scalar BigInt
|
|
||||||
|
|
||||||
scalar JSONObject
|
|
||||||
|
|
||||||
|
|
||||||
directive @hasScope(scope: String!) on FIELD_DEFINITION
|
|
||||||
directive @hasRole(role: String!) on FIELD_DEFINITION
|
|
||||||
|
|
||||||
type Query {
|
|
||||||
"""
|
|
||||||
Stare into the void.
|
|
||||||
"""
|
|
||||||
_: String
|
|
||||||
}
|
|
||||||
type Mutation{
|
|
||||||
"""
|
|
||||||
The void stares back.
|
|
||||||
"""
|
|
||||||
_: String
|
|
||||||
}
|
|
||||||
type Subscription{
|
|
||||||
"""
|
|
||||||
It's lonely in the void.
|
|
||||||
"""
|
|
||||||
_: String
|
|
||||||
},extend type Query {
|
|
||||||
"""
|
|
||||||
Gets a specific app from the server.
|
|
||||||
"""
|
|
||||||
app( id: String! ): ServerApp
|
|
||||||
|
|
||||||
"""
|
|
||||||
Returns all the publicly available apps on this server.
|
|
||||||
"""
|
|
||||||
apps: [ServerAppListItem]
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServerApp {
|
|
||||||
id: String!
|
|
||||||
secret: String!
|
|
||||||
name: String!
|
|
||||||
description: String
|
|
||||||
termsAndConditionsLink: String
|
|
||||||
logo: String
|
|
||||||
public: Boolean
|
|
||||||
trustByDefault: Boolean
|
|
||||||
author: AppAuthor
|
|
||||||
createdAt: DateTime!
|
|
||||||
redirectUrl: String!
|
|
||||||
scopes: [Scope]!
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServerAppListItem {
|
|
||||||
id: String!
|
|
||||||
name: String!
|
|
||||||
description: String
|
|
||||||
termsAndConditionsLink: String
|
|
||||||
logo: String
|
|
||||||
author: AppAuthor
|
|
||||||
}
|
|
||||||
|
|
||||||
type AppAuthor {
|
|
||||||
name: String
|
|
||||||
id: String
|
|
||||||
}
|
|
||||||
|
|
||||||
extend type User {
|
|
||||||
"""
|
|
||||||
Returns the apps you have authorized.
|
|
||||||
"""
|
|
||||||
authorizedApps: [ServerAppListItem]
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "apps:read")
|
|
||||||
|
|
||||||
"""
|
|
||||||
Returns the apps you have created.
|
|
||||||
"""
|
|
||||||
createdApps: [ServerAppListItem]
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "apps:read")
|
|
||||||
}
|
|
||||||
|
|
||||||
extend type Mutation {
|
|
||||||
"""
|
|
||||||
Register a new third party application.
|
|
||||||
"""
|
|
||||||
appCreate(app: AppCreateInput!): String!
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "apps:write")
|
|
||||||
|
|
||||||
"""
|
|
||||||
Update an existing third party application. **Note: This will invalidate all existing tokens, refresh tokens and access codes and will require existing users to re-authorize it.**
|
|
||||||
"""
|
|
||||||
appUpdate(app: AppUpdateInput!): Boolean!
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "apps:write")
|
|
||||||
|
|
||||||
"""
|
|
||||||
Deletes a thirty party application.
|
|
||||||
"""
|
|
||||||
appDelete(appId: String!): Boolean!
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "apps:write")
|
|
||||||
|
|
||||||
"""
|
|
||||||
Revokes (de-authorizes) an application that you have previously authorized.
|
|
||||||
"""
|
|
||||||
appRevokeAccess(appId: String!): Boolean
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "apps:write")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
input AppCreateInput {
|
|
||||||
name: String!
|
|
||||||
description: String!
|
|
||||||
termsAndConditionsLink: String
|
|
||||||
logo: String
|
|
||||||
public: Boolean
|
|
||||||
redirectUrl: String!
|
|
||||||
scopes: [String]!
|
|
||||||
}
|
|
||||||
|
|
||||||
input AppUpdateInput {
|
|
||||||
id: String!
|
|
||||||
name: String!
|
|
||||||
description: String!
|
|
||||||
termsAndConditionsLink: String
|
|
||||||
logo: String
|
|
||||||
public: Boolean
|
|
||||||
redirectUrl: String!
|
|
||||||
scopes: [String]!
|
|
||||||
}
|
|
||||||
,extend type ServerInfo {
|
|
||||||
"""
|
|
||||||
The authentication strategies available on this server.
|
|
||||||
"""
|
|
||||||
authStrategies: [AuthStrategy]
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuthStrategy {
|
|
||||||
id: String!,
|
|
||||||
name: String!,
|
|
||||||
icon: String!,
|
|
||||||
url: String!,
|
|
||||||
color: String
|
|
||||||
}
|
|
||||||
,extend type User{
|
|
||||||
"""
|
|
||||||
Returns a list of your personal api tokens.
|
|
||||||
"""
|
|
||||||
apiTokens: [ApiToken]
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "tokens:read")
|
|
||||||
}
|
|
||||||
|
|
||||||
type ApiToken {
|
|
||||||
id: String!
|
|
||||||
name: String!
|
|
||||||
lastChars: String!
|
|
||||||
scopes: [String]!
|
|
||||||
createdAt: DateTime! #date
|
|
||||||
lifespan: BigInt!
|
|
||||||
lastUsed: String! #date
|
|
||||||
}
|
|
||||||
|
|
||||||
input ApiTokenCreateInput {
|
|
||||||
scopes: [String!]!,
|
|
||||||
name: String!,
|
|
||||||
lifespan: BigInt
|
|
||||||
}
|
|
||||||
|
|
||||||
extend type Mutation {
|
|
||||||
"""
|
|
||||||
Creates an personal api token.
|
|
||||||
"""
|
|
||||||
apiTokenCreate(token: ApiTokenCreateInput!):String!
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "tokens:write")
|
|
||||||
"""
|
|
||||||
Revokes (deletes) an personal api token.
|
|
||||||
"""
|
|
||||||
apiTokenRevoke(token: String!):Boolean!
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "tokens:write")
|
|
||||||
}
|
|
||||||
,extend type Stream {
|
|
||||||
commits(limit: Int! = 25, cursor: String): CommitCollection
|
|
||||||
commit(id: String!): Commit
|
|
||||||
branches(limit: Int! = 25, cursor: String): BranchCollection
|
|
||||||
branch(name: String!): Branch
|
|
||||||
}
|
|
||||||
|
|
||||||
extend type User {
|
|
||||||
commits(limit: Int! = 25, cursor: String): CommitCollectionUser
|
|
||||||
}
|
|
||||||
|
|
||||||
type Branch {
|
|
||||||
id: String!
|
|
||||||
name: String!
|
|
||||||
author: User!
|
|
||||||
description: String
|
|
||||||
commits(limit: Int! = 25, cursor: String): CommitCollection
|
|
||||||
}
|
|
||||||
|
|
||||||
type Commit {
|
|
||||||
id: String!
|
|
||||||
referencedObject: String!
|
|
||||||
message: String
|
|
||||||
authorName: String
|
|
||||||
authorId: String
|
|
||||||
createdAt: DateTime
|
|
||||||
}
|
|
||||||
|
|
||||||
type CommitCollectionUserNode {
|
|
||||||
id: String!
|
|
||||||
referencedObject: String!
|
|
||||||
message: String
|
|
||||||
streamId: String
|
|
||||||
streamName: String
|
|
||||||
createdAt: DateTime
|
|
||||||
}
|
|
||||||
|
|
||||||
type BranchCollection {
|
|
||||||
totalCount: Int!
|
|
||||||
cursor: String
|
|
||||||
items: [Branch]
|
|
||||||
}
|
|
||||||
|
|
||||||
type CommitCollection {
|
|
||||||
totalCount: Int!
|
|
||||||
cursor: String
|
|
||||||
items: [Commit]
|
|
||||||
}
|
|
||||||
|
|
||||||
type CommitCollectionUser {
|
|
||||||
totalCount: Int!
|
|
||||||
cursor: String
|
|
||||||
items: [CommitCollectionUserNode]
|
|
||||||
}
|
|
||||||
|
|
||||||
extend type Mutation {
|
|
||||||
branchCreate(branch: BranchCreateInput!): String!
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "streams:write")
|
|
||||||
branchUpdate(branch: BranchUpdateInput!): Boolean!
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "streams:write")
|
|
||||||
branchDelete(branch: BranchDeleteInput!): Boolean!
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "streams:write")
|
|
||||||
|
|
||||||
commitCreate(commit: CommitCreateInput!): String!
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "streams:write")
|
|
||||||
commitUpdate(commit: CommitUpdateInput!): Boolean!
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "streams:write")
|
|
||||||
commitDelete(commit: CommitDeleteInput!): Boolean!
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "streams:write")
|
|
||||||
}
|
|
||||||
|
|
||||||
extend type Subscription {
|
|
||||||
# TODO: auth for these subscriptions
|
|
||||||
"""
|
|
||||||
Subscribe to branch created event
|
|
||||||
"""
|
|
||||||
branchCreated(streamId: String!): JSONObject
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "streams:read")
|
|
||||||
"""
|
|
||||||
Subscribe to branch updated event.
|
|
||||||
"""
|
|
||||||
branchUpdated(streamId: String!, branchId: String): JSONObject
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "streams:read")
|
|
||||||
"""
|
|
||||||
Subscribe to branch deleted event
|
|
||||||
"""
|
|
||||||
branchDeleted(streamId: String!): JSONObject
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "streams:read")
|
|
||||||
|
|
||||||
"""
|
|
||||||
Subscribe to commit created event
|
|
||||||
"""
|
|
||||||
commitCreated(streamId: String!): JSONObject
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "streams:read")
|
|
||||||
"""
|
|
||||||
Subscribe to commit updated event.
|
|
||||||
"""
|
|
||||||
commitUpdated(streamId: String!, commitId: String): JSONObject
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "streams:read")
|
|
||||||
"""
|
|
||||||
Subscribe to commit deleted event
|
|
||||||
"""
|
|
||||||
commitDeleted(streamId: String!): JSONObject
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "streams:read")
|
|
||||||
}
|
|
||||||
|
|
||||||
input BranchCreateInput {
|
|
||||||
streamId: String!
|
|
||||||
name: String!
|
|
||||||
description: String
|
|
||||||
}
|
|
||||||
|
|
||||||
input BranchUpdateInput {
|
|
||||||
streamId: String!
|
|
||||||
id: String!
|
|
||||||
name: String
|
|
||||||
description: String
|
|
||||||
}
|
|
||||||
|
|
||||||
input BranchDeleteInput {
|
|
||||||
streamId: String!
|
|
||||||
id: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
input CommitCreateInput {
|
|
||||||
streamId: String!
|
|
||||||
branchName: String!
|
|
||||||
objectId: String!
|
|
||||||
message: String
|
|
||||||
previousCommitIds: [String]
|
|
||||||
}
|
|
||||||
|
|
||||||
input CommitUpdateInput {
|
|
||||||
streamId: String!
|
|
||||||
id: String!
|
|
||||||
message: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
input CommitDeleteInput {
|
|
||||||
streamId: String!
|
|
||||||
id: String!
|
|
||||||
}
|
|
||||||
,extend type Stream {
|
|
||||||
object( id: String! ): Object
|
|
||||||
}
|
|
||||||
|
|
||||||
type Object {
|
|
||||||
id: String!
|
|
||||||
speckleType: String!
|
|
||||||
applicationId: String
|
|
||||||
createdAt: DateTime
|
|
||||||
totalChildrenCount: Int
|
|
||||||
"""
|
|
||||||
The full object, with all its props & other things. **NOTE:** If you're requesting objects for the purpose of recreating & displaying, you probably only want to request this specific field.
|
|
||||||
"""
|
|
||||||
data: JSONObject
|
|
||||||
"""
|
|
||||||
Get any objects that this object references. In the case of commits, this will give you a commit's constituent objects.
|
|
||||||
**NOTE**: Providing any of the two last arguments ( `query`, `orderBy` ) will trigger a different code branch that executes a much more expensive SQL query. It is not recommended to do so for basic clients that are interested in purely getting all the objects of a given commit.
|
|
||||||
"""
|
|
||||||
children(
|
|
||||||
limit: Int! = 100,
|
|
||||||
depth: Int! = 50,
|
|
||||||
select: [String],
|
|
||||||
cursor: String,
|
|
||||||
query: [JSONObject!],
|
|
||||||
orderBy: JSONObject ): ObjectCollection!
|
|
||||||
}
|
|
||||||
|
|
||||||
type ObjectCollection {
|
|
||||||
totalCount: Int!
|
|
||||||
cursor: String
|
|
||||||
objects: [Object]!
|
|
||||||
}
|
|
||||||
|
|
||||||
extend type Mutation {
|
|
||||||
objectCreate( objectInput: ObjectCreateInput! ): [String]!
|
|
||||||
}
|
|
||||||
|
|
||||||
input ObjectCreateInput {
|
|
||||||
"""
|
|
||||||
The stream against which these objects will be created.
|
|
||||||
"""
|
|
||||||
streamId: String!
|
|
||||||
"""
|
|
||||||
The objects you want to create.
|
|
||||||
"""
|
|
||||||
objects: [JSONObject]!
|
|
||||||
},extend type Query {
|
|
||||||
serverInfo: ServerInfo!
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
||||||
Information about this server.
|
|
||||||
"""
|
|
||||||
type ServerInfo {
|
|
||||||
name: String!
|
|
||||||
company: String
|
|
||||||
description: String
|
|
||||||
adminContact: String
|
|
||||||
canonicalUrl: String
|
|
||||||
termsOfService: String
|
|
||||||
roles: [Role]!
|
|
||||||
scopes: [Scope]!
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
||||||
Available roles.
|
|
||||||
"""
|
|
||||||
type Role {
|
|
||||||
name: String!
|
|
||||||
description: String!
|
|
||||||
resourceTarget: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
||||||
Available scopes.
|
|
||||||
"""
|
|
||||||
type Scope {
|
|
||||||
name: String!
|
|
||||||
description: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
extend type Mutation {
|
|
||||||
serverInfoUpdate(info: ServerInfoUpdateInput!): Boolean
|
|
||||||
@hasRole(role: "server:admin")
|
|
||||||
@hasScope(scope: "server:setup")
|
|
||||||
}
|
|
||||||
|
|
||||||
input ServerInfoUpdateInput {
|
|
||||||
name: String!
|
|
||||||
company: String
|
|
||||||
description: String
|
|
||||||
adminContact: String
|
|
||||||
termsOfService: String
|
|
||||||
}
|
|
||||||
,extend type Query {
|
|
||||||
"""
|
|
||||||
Returns a specific stream.
|
|
||||||
"""
|
|
||||||
stream( id: String! ): Stream
|
|
||||||
|
|
||||||
"""
|
|
||||||
All the streams of the current user, pass in the `query` parameter to search by name, description or ID.
|
|
||||||
"""
|
|
||||||
streams( query: String, limit: Int = 25, cursor: String ): StreamCollection
|
|
||||||
@hasScope(scope: "streams:read")
|
|
||||||
}
|
|
||||||
|
|
||||||
type Stream {
|
|
||||||
id: String!
|
|
||||||
name: String!
|
|
||||||
description: String
|
|
||||||
isPublic: Boolean!
|
|
||||||
createdAt: DateTime!
|
|
||||||
updatedAt: DateTime!
|
|
||||||
collaborators: [ StreamCollaborator ]!
|
|
||||||
}
|
|
||||||
|
|
||||||
extend type User {
|
|
||||||
"""
|
|
||||||
All the streams that a user has access to.
|
|
||||||
"""
|
|
||||||
streams( limit: Int! = 25, cursor: String ): StreamCollection
|
|
||||||
}
|
|
||||||
|
|
||||||
type StreamCollaborator {
|
|
||||||
id: String!
|
|
||||||
name: String!
|
|
||||||
role: String!
|
|
||||||
company: String
|
|
||||||
avatar: String
|
|
||||||
}
|
|
||||||
|
|
||||||
type StreamCollection {
|
|
||||||
totalCount: Int!
|
|
||||||
cursor: String
|
|
||||||
items: [ Stream ]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
extend type Mutation {
|
|
||||||
"""
|
|
||||||
Creates a new stream.
|
|
||||||
"""
|
|
||||||
streamCreate( stream: StreamCreateInput! ): String
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "streams:write")
|
|
||||||
"""
|
|
||||||
Updates an existing stream.
|
|
||||||
"""
|
|
||||||
streamUpdate( stream: StreamUpdateInput! ): Boolean!
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "streams:write")
|
|
||||||
"""
|
|
||||||
Deletes an existing stream.
|
|
||||||
"""
|
|
||||||
streamDelete( id: String! ): Boolean!
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "streams:write")
|
|
||||||
"""
|
|
||||||
Grants permissions to a user on a given stream.
|
|
||||||
"""
|
|
||||||
streamGrantPermission( permissionParams: StreamGrantPermissionInput! ): Boolean
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "streams:write")
|
|
||||||
"""
|
|
||||||
Revokes the permissions of a user on a given stream.
|
|
||||||
"""
|
|
||||||
streamRevokePermission( permissionParams: StreamRevokePermissionInput! ): Boolean
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "streams:write")
|
|
||||||
}
|
|
||||||
|
|
||||||
extend type Subscription {
|
|
||||||
|
|
||||||
#
|
|
||||||
# User bound subscriptions that operate on the stream collection of an user
|
|
||||||
# Example relevant view/usecase: updating the list of streams for a user.
|
|
||||||
#
|
|
||||||
|
|
||||||
"""
|
|
||||||
Subscribes to new stream added event for your profile. Use this to display an up-to-date list of streams.
|
|
||||||
**NOTE**: If someone shares a stream with you, this subscription will be triggered with an extra value of `sharedBy` in the payload.
|
|
||||||
"""
|
|
||||||
userStreamAdded: JSONObject
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "profile:read")
|
|
||||||
|
|
||||||
"""
|
|
||||||
Subscribes to stream removed event for your profile. Use this to display an up-to-date list of streams for your profile.
|
|
||||||
**NOTE**: If someone revokes your permissions on a stream, this subscription will be triggered with an extra value of `revokedBy` in the payload.
|
|
||||||
"""
|
|
||||||
userStreamRemoved: JSONObject
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "profile:read")
|
|
||||||
|
|
||||||
#
|
|
||||||
# Stream bound subscriptions that operate on the stream itself.
|
|
||||||
# Example relevant view/usecase: a single stream connector, or view, or component in a web app
|
|
||||||
#
|
|
||||||
|
|
||||||
"""
|
|
||||||
Subscribes to stream updated event. Use this in clients/components that pertain only to this stream.
|
|
||||||
"""
|
|
||||||
streamUpdated( streamId: String ): JSONObject
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "streams:read")
|
|
||||||
|
|
||||||
"""
|
|
||||||
Subscribes to stream deleted event. Use this in clients/components that pertain only to this stream.
|
|
||||||
"""
|
|
||||||
streamDeleted( streamId: String ): JSONObject
|
|
||||||
@hasRole(role: "server:user")
|
|
||||||
@hasScope(scope: "streams:read")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
input StreamCreateInput {
|
|
||||||
name: String
|
|
||||||
description: String
|
|
||||||
isPublic: Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
input StreamUpdateInput {
|
|
||||||
id: String!
|
|
||||||
name: String
|
|
||||||
description: String
|
|
||||||
isPublic: Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
input StreamGrantPermissionInput {
|
|
||||||
streamId: String!,
|
|
||||||
userId: String!,
|
|
||||||
role: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
input StreamRevokePermissionInput {
|
|
||||||
streamId: String!,
|
|
||||||
userId: String!
|
|
||||||
}
|
|
||||||
,extend type Query {
|
|
||||||
"""
|
|
||||||
Gets the profile of a user. If no id argument is provided, will return the current authenticated user's profile (as extracted from the authorization header).
|
|
||||||
"""
|
|
||||||
user(id: String): User
|
|
||||||
userSearch(
|
|
||||||
query: String!
|
|
||||||
limit: Int! = 25
|
|
||||||
cursor: String
|
|
||||||
): UserSearchResultCollection
|
|
||||||
userPwdStrength(pwd: String!): JSONObject
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
||||||
Base user type.
|
|
||||||
"""
|
|
||||||
type User {
|
|
||||||
id: String!
|
|
||||||
suuid: String
|
|
||||||
email: String
|
|
||||||
name: String
|
|
||||||
bio: String
|
|
||||||
company: String
|
|
||||||
avatar: String
|
|
||||||
verified: Boolean
|
|
||||||
profiles: JSONObject
|
|
||||||
role: String
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserSearchResultCollection {
|
|
||||||
cursor: String
|
|
||||||
items: [UserSearchResult]
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserSearchResult {
|
|
||||||
id: String!
|
|
||||||
name: String
|
|
||||||
bio: String
|
|
||||||
company: String
|
|
||||||
avatar: String
|
|
||||||
verified: Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
extend type Mutation {
|
|
||||||
"""
|
|
||||||
Edits a user's profile.
|
|
||||||
"""
|
|
||||||
userUpdate(user: UserUpdateInput!): Boolean!
|
|
||||||
}
|
|
||||||
|
|
||||||
input UserUpdateInput {
|
|
||||||
name: String
|
|
||||||
company: String
|
|
||||||
bio: String
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
from typing import Any, List
|
|
||||||
|
|
||||||
|
|
||||||
class SpeckleException(Exception):
|
|
||||||
def __init__(self, message: str, exception: Exception = None) -> None:
|
|
||||||
self.message = message
|
|
||||||
self.exception = exception
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"SpeckleException: {self.message}"
|
|
||||||
|
|
||||||
|
|
||||||
class SerializationException(SpeckleException):
|
|
||||||
def __init__(self, message: str, object: Any, exception: Exception = None) -> None:
|
|
||||||
super().__init__(message=message)
|
|
||||||
self.object = object
|
|
||||||
self.unhandled_type = type(object)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"SpeckleException: Could not serialize object of type {self.unhandled_type}"
|
|
||||||
|
|
||||||
|
|
||||||
class GraphQLException(SpeckleException):
|
|
||||||
def __init__(self, message: str, errors: List, data=None) -> None:
|
|
||||||
super().__init__(message=message)
|
|
||||||
self.errors = errors
|
|
||||||
self.data = data
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"GraphQLException: {self.message}"
|
|
||||||
|
|
||||||
|
|
||||||
class SpeckleWarning(Warning):
|
|
||||||
def __init__(self, *args: object) -> None:
|
|
||||||
super().__init__(*args)
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
from warnings import warn
|
|
||||||
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
|
|
||||||
|
|
||||||
UNITS = ["mm", "cm", "m", "in", "ft", "yd", "mi"]
|
|
||||||
|
|
||||||
UNITS_STRINGS = {
|
|
||||||
"mm": ["mm", "mil", "millimeters", "millimetres"],
|
|
||||||
"cm": ["cm", "centimetre", "centimeter", "centimetres", "centimeters"],
|
|
||||||
"m": ["m", "meter", "meters", "metre", "metres"],
|
|
||||||
"km": ["km", "kilometer", "kilometre", "kilometers", "kilometres"],
|
|
||||||
"in": ["in", "inch", "inches"],
|
|
||||||
"ft": ["ft", "foot", "feet"],
|
|
||||||
"yd": ["yd", "yard", "yards"],
|
|
||||||
"mi": ["mi", "mile", "miles"],
|
|
||||||
"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):
|
|
||||||
if not isinstance(unit, str):
|
|
||||||
warn(
|
|
||||||
f"Invalid units: expected type str but received {type(unit)} ({unit}). Skipping - no units will be set.",
|
|
||||||
SpeckleWarning,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
unit = str.lower(unit)
|
|
||||||
for name, alternates in UNITS_STRINGS.items():
|
|
||||||
if unit in alternates:
|
|
||||||
return name
|
|
||||||
|
|
||||||
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})."
|
|
||||||
)
|
|
||||||
@@ -18,8 +18,9 @@ from specklepy.api.resources import (
|
|||||||
server,
|
server,
|
||||||
user,
|
user,
|
||||||
subscriptions,
|
subscriptions,
|
||||||
|
other_user,
|
||||||
|
active_user
|
||||||
)
|
)
|
||||||
from specklepy.api.models import ServerInfo
|
|
||||||
from gql import Client
|
from gql import Client
|
||||||
from gql.transport.requests import RequestsHTTPTransport
|
from gql.transport.requests import RequestsHTTPTransport
|
||||||
from gql.transport.websockets import WebsocketsTransport
|
from gql.transport.websockets import WebsocketsTransport
|
||||||
@@ -136,6 +137,8 @@ class SpeckleClient:
|
|||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {self.account.token}",
|
"Authorization": f"Bearer {self.account.token}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
"apollographql-client-name": metrics.HOST_APP,
|
||||||
|
"apollographql-client-version": metrics.HOST_APP_VERSION,
|
||||||
}
|
}
|
||||||
httptransport = RequestsHTTPTransport(
|
httptransport = RequestsHTTPTransport(
|
||||||
url=self.graphql, headers=headers, verify=True, retries=3
|
url=self.graphql, headers=headers, verify=True, retries=3
|
||||||
@@ -160,9 +163,38 @@ class SpeckleClient:
|
|||||||
return self.httpclient.execute(query)
|
return self.httpclient.execute(query)
|
||||||
|
|
||||||
def _init_resources(self) -> None:
|
def _init_resources(self) -> None:
|
||||||
self.stream = stream.Resource(
|
self.server = server.Resource(
|
||||||
account=self.account, basepath=self.url, client=self.httpclient
|
account=self.account, basepath=self.url, client=self.httpclient
|
||||||
)
|
)
|
||||||
|
server_version = None
|
||||||
|
try:
|
||||||
|
server_version = self.server.version()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self.user = user.Resource(
|
||||||
|
account=self.account,
|
||||||
|
basepath=self.url,
|
||||||
|
client=self.httpclient,
|
||||||
|
server_version=server_version,
|
||||||
|
)
|
||||||
|
self.other_user = other_user.Resource(
|
||||||
|
account=self.account,
|
||||||
|
basepath=self.url,
|
||||||
|
client=self.httpclient,
|
||||||
|
server_version=server_version,
|
||||||
|
)
|
||||||
|
self.active_user = active_user.Resource(
|
||||||
|
account=self.account,
|
||||||
|
basepath=self.url,
|
||||||
|
client=self.httpclient,
|
||||||
|
server_version=server_version,
|
||||||
|
)
|
||||||
|
self.stream = stream.Resource(
|
||||||
|
account=self.account,
|
||||||
|
basepath=self.url,
|
||||||
|
client=self.httpclient,
|
||||||
|
server_version=server_version,
|
||||||
|
)
|
||||||
self.commit = commit.Resource(
|
self.commit = commit.Resource(
|
||||||
account=self.account, basepath=self.url, client=self.httpclient
|
account=self.account, basepath=self.url, client=self.httpclient
|
||||||
)
|
)
|
||||||
@@ -172,12 +204,6 @@ class SpeckleClient:
|
|||||||
self.object = object.Resource(
|
self.object = object.Resource(
|
||||||
account=self.account, basepath=self.url, client=self.httpclient
|
account=self.account, basepath=self.url, client=self.httpclient
|
||||||
)
|
)
|
||||||
self.server = server.Resource(
|
|
||||||
account=self.account, basepath=self.url, client=self.httpclient
|
|
||||||
)
|
|
||||||
self.user = user.Resource(
|
|
||||||
account=self.account, basepath=self.url, client=self.httpclient
|
|
||||||
)
|
|
||||||
self.subscribe = subscriptions.Resource(
|
self.subscribe = subscriptions.Resource(
|
||||||
account=self.account,
|
account=self.account,
|
||||||
basepath=self.ws_url,
|
basepath=self.ws_url,
|
||||||
@@ -1,26 +1,27 @@
|
|||||||
import os
|
import os
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field # pylint: disable=no-name-in-module
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from specklepy.logging import metrics
|
from specklepy.logging import metrics
|
||||||
from specklepy.api.models import ServerInfo
|
from specklepy.api.models import ServerInfo
|
||||||
from specklepy.transports.sqlite import SQLiteTransport
|
from specklepy.transports.sqlite import SQLiteTransport
|
||||||
from specklepy.logging.exceptions import SpeckleException
|
from specklepy.logging.exceptions import SpeckleException
|
||||||
|
from specklepy import paths
|
||||||
|
|
||||||
|
|
||||||
class UserInfo(BaseModel):
|
class UserInfo(BaseModel):
|
||||||
name: Optional[str]
|
name: Optional[str] = None
|
||||||
email: Optional[str]
|
email: Optional[str] = None
|
||||||
company: Optional[str]
|
company: Optional[str] = None
|
||||||
id: Optional[str]
|
id: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class Account(BaseModel):
|
class Account(BaseModel):
|
||||||
isDefault: bool = False
|
isDefault: bool = False
|
||||||
token: str = None
|
token: Optional[str] = None
|
||||||
refreshToken: str = None
|
refreshToken: Optional[str] = None
|
||||||
serverInfo: ServerInfo = Field(default_factory=ServerInfo)
|
serverInfo: ServerInfo = Field(default_factory=ServerInfo)
|
||||||
userInfo: UserInfo = Field(default_factory=UserInfo)
|
userInfo: UserInfo = Field(default_factory=UserInfo)
|
||||||
id: str = None
|
id: Optional[str] = None
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"Account(email: {self.userInfo.email}, server: {self.serverInfo.url}, isDefault: {self.isDefault})"
|
return f"Account(email: {self.userInfo.email}, server: {self.serverInfo.url}, isDefault: {self.isDefault})"
|
||||||
@@ -35,7 +36,7 @@ class Account(BaseModel):
|
|||||||
return acct
|
return acct
|
||||||
|
|
||||||
|
|
||||||
def get_local_accounts(base_path: str = None) -> List[Account]:
|
def get_local_accounts(base_path: Optional[str] = None) -> List[Account]:
|
||||||
"""Gets all the accounts present in this environment
|
"""Gets all the accounts present in this environment
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
@@ -44,15 +45,30 @@ def get_local_accounts(base_path: str = None) -> List[Account]:
|
|||||||
Returns:
|
Returns:
|
||||||
List[Account] -- list of all local accounts or an empty list if no accounts were found
|
List[Account] -- list of all local accounts or an empty list if no accounts were found
|
||||||
"""
|
"""
|
||||||
account_storage = SQLiteTransport(scope="Accounts", base_path=base_path)
|
accounts: List[Account] = []
|
||||||
json_path = os.path.join(account_storage._base_path, "Accounts")
|
try:
|
||||||
os.makedirs(json_path, exist_ok=True)
|
account_storage = SQLiteTransport(scope="Accounts", base_path=base_path)
|
||||||
json_acct_files = [file for file in os.listdir(json_path) if file.endswith(".json")]
|
res = account_storage.get_all_objects()
|
||||||
|
account_storage.close()
|
||||||
|
if res:
|
||||||
|
accounts.extend(Account.parse_raw(r[1]) for r in res)
|
||||||
|
except SpeckleException:
|
||||||
|
# cannot open SQLiteTransport, probably because of the lack
|
||||||
|
# of disk write permissions
|
||||||
|
pass
|
||||||
|
|
||||||
|
json_acct_files = []
|
||||||
|
json_path = paths.accounts_path()
|
||||||
|
try:
|
||||||
|
os.makedirs(json_path, exist_ok=True)
|
||||||
|
json_acct_files.extend(
|
||||||
|
file for file in os.listdir(json_path) if file.endswith(".json")
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
# cannot find or get the json account paths
|
||||||
|
pass
|
||||||
|
|
||||||
accounts = []
|
|
||||||
res = account_storage.get_all_objects()
|
|
||||||
if res:
|
|
||||||
accounts.extend(Account.parse_raw(r[1]) for r in res)
|
|
||||||
if json_acct_files:
|
if json_acct_files:
|
||||||
try:
|
try:
|
||||||
accounts.extend(
|
accounts.extend(
|
||||||
@@ -63,7 +79,8 @@ def get_local_accounts(base_path: str = None) -> List[Account]:
|
|||||||
raise SpeckleException(
|
raise SpeckleException(
|
||||||
"Invalid json accounts could not be read. Please fix or remove them.",
|
"Invalid json accounts could not be read. Please fix or remove them.",
|
||||||
ex,
|
ex,
|
||||||
)
|
) from ex
|
||||||
|
|
||||||
metrics.track(
|
metrics.track(
|
||||||
metrics.ACCOUNTS,
|
metrics.ACCOUNTS,
|
||||||
next(
|
next(
|
||||||
@@ -75,7 +92,7 @@ def get_local_accounts(base_path: str = None) -> List[Account]:
|
|||||||
return accounts
|
return accounts
|
||||||
|
|
||||||
|
|
||||||
def get_default_account(base_path: str = None) -> Account:
|
def get_default_account(base_path: Optional[str] = None) -> Optional[Account]:
|
||||||
"""Gets this environment's default account if any. If there is no default, the first found will be returned and set as default.
|
"""Gets this environment's default account if any. If there is no default, the first found will be returned and set as default.
|
||||||
Arguments:
|
Arguments:
|
||||||
base_path {str} -- custom base path if you are not using the system default
|
base_path {str} -- custom base path if you are not using the system default
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from unicodedata import name
|
||||||
|
|
||||||
|
|
||||||
|
class HostAppVersion(Enum):
|
||||||
|
v = "v"
|
||||||
|
v6 = "v6"
|
||||||
|
v7 = "v7"
|
||||||
|
v2019 = "v2019"
|
||||||
|
v2020 = "v2020"
|
||||||
|
v2021 = "v2021"
|
||||||
|
v2022 = "v2022"
|
||||||
|
v2023 = "v2023"
|
||||||
|
v2024 = "v2024"
|
||||||
|
v2025 = "v2025"
|
||||||
|
vSandbox = "vSandbox"
|
||||||
|
vRevit = "vRevit"
|
||||||
|
vRevit2021 = "vRevit2021"
|
||||||
|
vRevit2022 = "vRevit2022"
|
||||||
|
vRevit2023 = "vRevit2023"
|
||||||
|
vRevit2024 = "vRevit2024"
|
||||||
|
vRevit2025 = "vRevit2025"
|
||||||
|
v25 = "v25"
|
||||||
|
v26 = "v26"
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HostApplication:
|
||||||
|
name: str
|
||||||
|
slug: str
|
||||||
|
|
||||||
|
def get_version(self, version: HostAppVersion) -> str:
|
||||||
|
return f"{name.replace(' ', '')}{str(version).strip('v')}"
|
||||||
|
|
||||||
|
|
||||||
|
RHINO = HostApplication("Rhino", "rhino")
|
||||||
|
GRASSHOPPER = HostApplication("Grasshopper", "grasshopper")
|
||||||
|
REVIT = HostApplication("Revit", "revit")
|
||||||
|
DYNAMO = HostApplication("Dynamo", "dynamo")
|
||||||
|
UNITY = HostApplication("Unity", "unity")
|
||||||
|
GSA = HostApplication("GSA", "gsa")
|
||||||
|
CIVIL = HostApplication("Civil 3D", "civil3d")
|
||||||
|
AUTOCAD = HostApplication("AutoCAD", "autocad")
|
||||||
|
MICROSTATION = HostApplication("MicroStation", "microstation")
|
||||||
|
OPENROADS = HostApplication("OpenRoads", "openroads")
|
||||||
|
OPENRAIL = HostApplication("OpenRail", "openrail")
|
||||||
|
OPENBUILDINGS = HostApplication("OpenBuildings", "openbuildings")
|
||||||
|
ETABS = HostApplication("ETABS", "etabs")
|
||||||
|
SAP2000 = HostApplication("SAP2000", "sap2000")
|
||||||
|
CSIBRIDGE = HostApplication("CSIBridge", "csibridge")
|
||||||
|
SAFE = HostApplication("SAFE", "safe")
|
||||||
|
TEKLASTRUCTURES = HostApplication("Tekla Structures", "teklastructures")
|
||||||
|
DXF = HostApplication("DXF Converter", "dxf")
|
||||||
|
EXCEL = HostApplication("Excel", "excel")
|
||||||
|
UNREAL = HostApplication("Unreal", "unreal")
|
||||||
|
POWERBI = HostApplication("Power BI", "powerbi")
|
||||||
|
BLENDER = HostApplication("Blender", "blender")
|
||||||
|
QGIS = HostApplication("QGIS", "qgis")
|
||||||
|
ARCGIS = HostApplication("ArcGIS", "arcgis")
|
||||||
|
SKETCHUP = HostApplication("SketchUp", "sketchup")
|
||||||
|
ARCHICAD = HostApplication("Archicad", "archicad")
|
||||||
|
TOPSOLID = HostApplication("TopSolid", "topsolid")
|
||||||
|
PYTHON = HostApplication("Python", "python")
|
||||||
|
NET = HostApplication(".NET", "net")
|
||||||
|
OTHER = HostApplication("Other", "other")
|
||||||
|
|
||||||
|
_app_name_host_app_mapping = {
|
||||||
|
"dynamo": DYNAMO,
|
||||||
|
"revit": REVIT,
|
||||||
|
"autocad": AUTOCAD,
|
||||||
|
"civil": CIVIL,
|
||||||
|
"rhino": RHINO,
|
||||||
|
"grasshopper": GRASSHOPPER,
|
||||||
|
"unity": UNITY,
|
||||||
|
"gsa": GSA,
|
||||||
|
"microstation": MICROSTATION,
|
||||||
|
"openroads": OPENROADS,
|
||||||
|
"openrail": OPENRAIL,
|
||||||
|
"openbuildings": OPENBUILDINGS,
|
||||||
|
"etabs": ETABS,
|
||||||
|
"sap": SAP2000,
|
||||||
|
"csibridge": CSIBRIDGE,
|
||||||
|
"safe": SAFE,
|
||||||
|
"teklastructures": TEKLASTRUCTURES,
|
||||||
|
"dxf": DXF,
|
||||||
|
"excel": EXCEL,
|
||||||
|
"unreal": UNREAL,
|
||||||
|
"powerbi": POWERBI,
|
||||||
|
"blender": BLENDER,
|
||||||
|
"qgis": QGIS,
|
||||||
|
"arcgis": ARCGIS,
|
||||||
|
"sketchup": SKETCHUP,
|
||||||
|
"archicad": ARCHICAD,
|
||||||
|
"topsolid": TOPSOLID,
|
||||||
|
"python": PYTHON,
|
||||||
|
"net": NET,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_host_app_from_string(app_name: str) -> HostApplication:
|
||||||
|
app_name = app_name.lower().replace(" ", "")
|
||||||
|
for partial_app_name, host_app in _app_name_host_app_mapping.items():
|
||||||
|
if partial_app_name in app_name:
|
||||||
|
return host_app
|
||||||
|
return HostApplication(app_name, app_name)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(HostAppVersion.v)
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
# generated by datamodel-codegen:
|
|
||||||
# filename: stream_schema.json
|
|
||||||
# timestamp: 2020-11-17T14:33:13+00:00
|
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
class Collaborator(BaseModel):
|
class Collaborator(BaseModel):
|
||||||
@@ -64,20 +60,20 @@ class Branches(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class Stream(BaseModel):
|
class Stream(BaseModel):
|
||||||
id: Optional[str]
|
id: Optional[str] = None
|
||||||
name: Optional[str]
|
name: Optional[str]
|
||||||
role: Optional[str]
|
role: Optional[str] = None
|
||||||
isPublic: Optional[bool]
|
isPublic: Optional[bool] = None
|
||||||
description: Optional[str]
|
description: Optional[str] = None
|
||||||
createdAt: Optional[datetime]
|
createdAt: Optional[datetime] = None
|
||||||
updatedAt: Optional[datetime]
|
updatedAt: Optional[datetime] = None
|
||||||
collaborators: List[Collaborator] = []
|
collaborators: List[Collaborator] = Field(default_factory=list)
|
||||||
branches: Optional[Branches]
|
branches: Optional[Branches] = None
|
||||||
commit: Optional[Commit]
|
commit: Optional[Commit] = None
|
||||||
object: Optional[Object]
|
object: Optional[Object] = None
|
||||||
commentCount: Optional[int]
|
commentCount: Optional[int] = None
|
||||||
favoritedDate: Optional[datetime]
|
favoritedDate: Optional[datetime] = None
|
||||||
favoritesCount: Optional[int]
|
favoritesCount: Optional[int] = None
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"Stream( id: {self.id}, name: {self.name}, description: {self.description}, isPublic: {self.isPublic})"
|
return f"Stream( id: {self.id}, name: {self.name}, description: {self.description}, isPublic: {self.isPublic})"
|
||||||
@@ -110,6 +106,36 @@ class User(BaseModel):
|
|||||||
return self.__repr__()
|
return self.__repr__()
|
||||||
|
|
||||||
|
|
||||||
|
class LimitedUser(BaseModel):
|
||||||
|
"""Limited user type, for showing public info about a user to another user."""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
name: Optional[str]
|
||||||
|
bio: Optional[str]
|
||||||
|
company: Optional[str]
|
||||||
|
avatar: Optional[str]
|
||||||
|
verified: Optional[bool]
|
||||||
|
role: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
class PendingStreamCollaborator(BaseModel):
|
||||||
|
id: Optional[str]
|
||||||
|
inviteId: Optional[str]
|
||||||
|
streamId: Optional[str]
|
||||||
|
streamName: Optional[str]
|
||||||
|
title: Optional[str]
|
||||||
|
role: Optional[str]
|
||||||
|
invitedBy: Optional[User]
|
||||||
|
user: Optional[User]
|
||||||
|
token: Optional[str]
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"PendingStreamCollaborator( inviteId: {self.inviteId}, streamId: {self.streamId}, role: {self.role}, title: {self.title}, invitedBy: {self.user.name if self.user else None})"
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.__repr__()
|
||||||
|
|
||||||
|
|
||||||
class Activity(BaseModel):
|
class Activity(BaseModel):
|
||||||
actionType: Optional[str]
|
actionType: Optional[str]
|
||||||
info: Optional[dict]
|
info: Optional[dict]
|
||||||
@@ -133,20 +159,20 @@ class ActivityCollection(BaseModel):
|
|||||||
cursor: Optional[datetime]
|
cursor: Optional[datetime]
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"ActivityCollection( totalCount: {self.totalCount}, items: {len(self.items) if self.items else 0}, cursor: {self.cursor.isoformat()} )"
|
return f"ActivityCollection( totalCount: {self.totalCount}, items: {len(self.items) if self.items else 0}, cursor: {self.cursor.isoformat() if self.cursor else None} )"
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.__repr__()
|
return self.__repr__()
|
||||||
|
|
||||||
|
|
||||||
class ServerInfo(BaseModel):
|
class ServerInfo(BaseModel):
|
||||||
name: Optional[str]
|
name: Optional[str] = None
|
||||||
company: Optional[str]
|
company: Optional[str] = None
|
||||||
url: Optional[str]
|
url: Optional[str] = None
|
||||||
description: Optional[str]
|
description: Optional[str] = None
|
||||||
adminContact: Optional[str]
|
adminContact: Optional[str] = None
|
||||||
canonicalUrl: Optional[str]
|
canonicalUrl: Optional[str] = None
|
||||||
roles: Optional[List[dict]]
|
roles: Optional[List[dict]] = None
|
||||||
scopes: Optional[List[dict]]
|
scopes: Optional[List[dict]] = None
|
||||||
authStrategies: Optional[List[dict]]
|
authStrategies: Optional[List[dict]] = None
|
||||||
version: Optional[str]
|
version: Optional[str] = None
|
||||||
@@ -2,7 +2,6 @@ from typing import List
|
|||||||
from specklepy.logging import metrics
|
from specklepy.logging import metrics
|
||||||
from specklepy.objects.base import Base
|
from specklepy.objects.base import Base
|
||||||
from specklepy.transports.sqlite import SQLiteTransport
|
from specklepy.transports.sqlite import SQLiteTransport
|
||||||
from specklepy.transports.server import ServerTransport
|
|
||||||
from specklepy.logging.exceptions import SpeckleException
|
from specklepy.logging.exceptions import SpeckleException
|
||||||
from specklepy.transports.abstract_transport import AbstractTransport
|
from specklepy.transports.abstract_transport import AbstractTransport
|
||||||
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
|
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
|
||||||
@@ -29,6 +28,9 @@ def send(
|
|||||||
message="You need to provide at least one transport: cannot send with an empty transport list and no default cache"
|
message="You need to provide at least one transport: cannot send with an empty transport list and no default cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if isinstance(transports, AbstractTransport):
|
||||||
|
transports = [transports]
|
||||||
|
|
||||||
if transports is None:
|
if transports is None:
|
||||||
metrics.track(metrics.SEND)
|
metrics.track(metrics.SEND)
|
||||||
transports = []
|
transports = []
|
||||||
@@ -40,13 +42,8 @@ def send(
|
|||||||
|
|
||||||
serializer = BaseObjectSerializer(write_transports=transports)
|
serializer = BaseObjectSerializer(write_transports=transports)
|
||||||
|
|
||||||
for t in transports:
|
|
||||||
t.begin_write()
|
|
||||||
obj_hash, _ = serializer.write_json(base=base)
|
obj_hash, _ = serializer.write_json(base=base)
|
||||||
|
|
||||||
for t in transports:
|
|
||||||
t.end_write()
|
|
||||||
|
|
||||||
return obj_hash
|
return obj_hash
|
||||||
|
|
||||||
|
|
||||||
@@ -55,6 +52,16 @@ def receive(
|
|||||||
remote_transport: AbstractTransport = None,
|
remote_transport: AbstractTransport = None,
|
||||||
local_transport: AbstractTransport = None,
|
local_transport: AbstractTransport = None,
|
||||||
) -> Base:
|
) -> Base:
|
||||||
|
metrics.track(metrics.RECEIVE, getattr(remote_transport, "account", None))
|
||||||
|
return _untracked_receive(obj_id, remote_transport, local_transport)
|
||||||
|
|
||||||
|
|
||||||
|
def _untracked_receive(
|
||||||
|
obj_id: str,
|
||||||
|
remote_transport: AbstractTransport = None,
|
||||||
|
local_transport: AbstractTransport = None,
|
||||||
|
) -> Base:
|
||||||
|
|
||||||
"""Receives an object from a transport.
|
"""Receives an object from a transport.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
@@ -66,13 +73,12 @@ def receive(
|
|||||||
Returns:
|
Returns:
|
||||||
Base -- the base object
|
Base -- the base object
|
||||||
"""
|
"""
|
||||||
metrics.track(metrics.RECEIVE, getattr(remote_transport, "account", None))
|
|
||||||
if not local_transport:
|
if not local_transport:
|
||||||
local_transport = SQLiteTransport()
|
local_transport = SQLiteTransport()
|
||||||
|
|
||||||
serializer = BaseObjectSerializer(read_transport=local_transport)
|
serializer = BaseObjectSerializer(read_transport=local_transport)
|
||||||
|
|
||||||
# try local transport first. if the parent is there, we assume all the children are there and continue with deserialisation using the local transport
|
# try local transport first. if the parent is there, we assume all the children are there and continue with deserialization using the local transport
|
||||||
obj_string = local_transport.get_object(obj_id)
|
obj_string = local_transport.get_object(obj_id)
|
||||||
if obj_string:
|
if obj_string:
|
||||||
return serializer.read_json(obj_string=obj_string)
|
return serializer.read_json(obj_string=obj_string)
|
||||||
@@ -126,3 +132,6 @@ def deserialize(obj_string: str, read_transport: AbstractTransport = None) -> Ba
|
|||||||
serializer = BaseObjectSerializer(read_transport=read_transport)
|
serializer = BaseObjectSerializer(read_transport=read_transport)
|
||||||
|
|
||||||
return serializer.read_json(obj_string=obj_string)
|
return serializer.read_json(obj_string=obj_string)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [receive.__name__, send.__name__, serialize.__name__, deserialize.__name__]
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
from graphql import DocumentNode
|
||||||
|
from specklepy.api.credentials import Account
|
||||||
|
from specklepy.transports.sqlite import SQLiteTransport
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple, Type, Union
|
||||||
|
from gql.client import Client
|
||||||
|
from gql.transport.exceptions import TransportQueryError
|
||||||
|
from specklepy.logging.exceptions import (
|
||||||
|
GraphQLException,
|
||||||
|
SpeckleException,
|
||||||
|
UnsupportedException,
|
||||||
|
)
|
||||||
|
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceBase(object):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
account: Account,
|
||||||
|
basepath: str,
|
||||||
|
client: Client,
|
||||||
|
name: str,
|
||||||
|
server_version: Optional[Tuple[Any, ...]] = None,
|
||||||
|
) -> None:
|
||||||
|
self.account = account
|
||||||
|
self.basepath = basepath
|
||||||
|
self.client = client
|
||||||
|
self.name = name
|
||||||
|
self.server_version = server_version
|
||||||
|
self.schema: Optional[Type] = None
|
||||||
|
|
||||||
|
def _step_into_response(self, response: dict, return_type: Union[str, List, None]):
|
||||||
|
"""Step into the dict to get the relevant data"""
|
||||||
|
if return_type is None:
|
||||||
|
return response
|
||||||
|
if isinstance(return_type, str):
|
||||||
|
return response[return_type]
|
||||||
|
if isinstance(return_type, List):
|
||||||
|
for key in return_type:
|
||||||
|
response = response[key]
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _parse_response(self, response: Union[dict, list, None], schema=None):
|
||||||
|
"""Try to create a class instance from the response"""
|
||||||
|
if response is None:
|
||||||
|
return None
|
||||||
|
if isinstance(response, list):
|
||||||
|
return [self._parse_response(response=r, schema=schema) for r in response]
|
||||||
|
if schema:
|
||||||
|
return schema.parse_obj(response)
|
||||||
|
elif self.schema:
|
||||||
|
try:
|
||||||
|
return self.schema.parse_obj(response)
|
||||||
|
except:
|
||||||
|
s = BaseObjectSerializer(read_transport=SQLiteTransport())
|
||||||
|
return s.recompose_base(response)
|
||||||
|
else:
|
||||||
|
return response
|
||||||
|
|
||||||
|
def make_request(
|
||||||
|
self,
|
||||||
|
query: DocumentNode,
|
||||||
|
params: Dict = None,
|
||||||
|
return_type: Union[str, List, None] = None,
|
||||||
|
schema=None,
|
||||||
|
parse_response: bool = True,
|
||||||
|
) -> Any:
|
||||||
|
"""Executes the GraphQL query"""
|
||||||
|
try:
|
||||||
|
response = self.client.execute(query, variable_values=params)
|
||||||
|
except Exception as ex:
|
||||||
|
if isinstance(ex, TransportQueryError):
|
||||||
|
return GraphQLException(
|
||||||
|
message=f"Failed to execute the GraphQL {self.name} request. Errors: {ex.errors}",
|
||||||
|
errors=ex.errors,
|
||||||
|
data=ex.data,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return SpeckleException(
|
||||||
|
message=f"Failed to execute the GraphQL {self.name} request. Inner exception: {ex}",
|
||||||
|
exception=ex,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self._step_into_response(response=response, return_type=return_type)
|
||||||
|
|
||||||
|
if parse_response:
|
||||||
|
return self._parse_response(response=response, schema=schema)
|
||||||
|
else:
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _check_server_version_at_least(
|
||||||
|
self, target_version: Tuple[Any, ...], unsupported_message: str = None
|
||||||
|
):
|
||||||
|
"""Use this check to guard against making unsupported requests on older servers.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
target_version {tuple} -- the minimum server version in the format (major, minor, patch, (tag, build))
|
||||||
|
eg (2, 6, 3) for a stable build and (2, 6, 4, 'alpha', 4711) for alpha
|
||||||
|
"""
|
||||||
|
if not unsupported_message:
|
||||||
|
unsupported_message = f"The client method used is not supported on Speckle Server versions prior to v{'.'.join(target_version)}"
|
||||||
|
# if version is dev, it should be supported... (or not)
|
||||||
|
if self.server_version == ("dev",):
|
||||||
|
return
|
||||||
|
if self.server_version and self.server_version < target_version:
|
||||||
|
raise UnsupportedException(unsupported_message)
|
||||||
|
|
||||||
|
def _check_invites_supported(self):
|
||||||
|
"""Invites are only supported for Speckle Server >= 2.6.4.
|
||||||
|
Use this check to guard against making unsupported requests on older servers.
|
||||||
|
"""
|
||||||
|
self._check_server_version_at_least(
|
||||||
|
(2, 6, 4),
|
||||||
|
(
|
||||||
|
"Stream invites are only supported as of Speckle Server v2.6.4. "
|
||||||
|
"Please update your Speckle Server to use this method or use the `grant_permission` flow instead."
|
||||||
|
),
|
||||||
|
)
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
import inspect
|
|
||||||
import pkgutil
|
import pkgutil
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
from typing import List, Optional
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from gql import gql
|
||||||
|
from specklepy.logging import metrics
|
||||||
|
from specklepy.logging.exceptions import SpeckleException
|
||||||
|
from specklepy.api.resource import ResourceBase
|
||||||
|
from specklepy.api.models import ActivityCollection, PendingStreamCollaborator, User
|
||||||
|
|
||||||
|
|
||||||
|
NAME = "active_user"
|
||||||
|
|
||||||
|
|
||||||
|
class Resource(ResourceBase):
|
||||||
|
"""API Access class for users"""
|
||||||
|
|
||||||
|
def __init__(self, account, basepath, client, server_version) -> None:
|
||||||
|
super().__init__(
|
||||||
|
account=account,
|
||||||
|
basepath=basepath,
|
||||||
|
client=client,
|
||||||
|
name=NAME,
|
||||||
|
server_version=server_version,
|
||||||
|
)
|
||||||
|
self.schema = User
|
||||||
|
|
||||||
|
def get(self) -> User:
|
||||||
|
"""Gets the profile of a user. If no id argument is provided, will return the current authenticated user's profile (as extracted from the authorization header).
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
id {str} -- the user id
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
User -- the retrieved user
|
||||||
|
"""
|
||||||
|
metrics.track(metrics.USER, self.account, {"name": "get"})
|
||||||
|
query = gql(
|
||||||
|
"""
|
||||||
|
query User {
|
||||||
|
activeUser {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
name
|
||||||
|
bio
|
||||||
|
company
|
||||||
|
avatar
|
||||||
|
verified
|
||||||
|
profiles
|
||||||
|
role
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
return self.make_request(query=query, params=params, return_type="activeUser")
|
||||||
|
|
||||||
|
def update(
|
||||||
|
self,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
company: Optional[str] = None,
|
||||||
|
bio: Optional[str] = None,
|
||||||
|
avatar: Optional[str] = None,
|
||||||
|
):
|
||||||
|
"""Updates your user profile. All arguments are optional.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
name {str} -- your name
|
||||||
|
company {str} -- the company you may or may not work for
|
||||||
|
bio {str} -- tell us about yourself
|
||||||
|
avatar {str} -- a nice photo of yourself
|
||||||
|
|
||||||
|
Returns @deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT):
|
||||||
|
bool -- True if your profile was updated successfully
|
||||||
|
"""
|
||||||
|
metrics.track(metrics.USER, self.account, {"name": "update"})
|
||||||
|
query = gql(
|
||||||
|
"""
|
||||||
|
mutation UserUpdate($user: UserUpdateInput!) {
|
||||||
|
userUpdate(user: $user)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
params = {"name": name, "company": company, "bio": bio, "avatar": avatar}
|
||||||
|
|
||||||
|
params = {"user": {k: v for k, v in params.items() if v is not None}}
|
||||||
|
|
||||||
|
if not params["user"]:
|
||||||
|
return SpeckleException(
|
||||||
|
message="You must provide at least one field to update your user profile"
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.make_request(
|
||||||
|
query=query, params=params, return_type="userUpdate", parse_response=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def activity(
|
||||||
|
self,
|
||||||
|
limit: int = 20,
|
||||||
|
action_type: Optional[str] = None,
|
||||||
|
before: Optional[datetime] = None,
|
||||||
|
after: Optional[datetime] = None,
|
||||||
|
cursor: Optional[datetime] = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get the activity from a given stream in an Activity collection. Step into the activity `items` for the list of activity.
|
||||||
|
If no id argument is provided, will return the current authenticated user's activity (as extracted from the authorization header).
|
||||||
|
|
||||||
|
Note: all timestamps arguments should be `datetime` of any tz as they will be converted to UTC ISO format strings
|
||||||
|
|
||||||
|
user_id {str} -- the id of the user to get the activity from
|
||||||
|
action_type {str} -- filter results to a single action type (eg: `commit_create` or `commit_receive`)
|
||||||
|
limit {int} -- max number of Activity items to return
|
||||||
|
before {datetime} -- latest cutoff for activity (ie: return all activity _before_ this time)
|
||||||
|
after {datetime} -- oldest cutoff for activity (ie: return all activity _after_ this time)
|
||||||
|
cursor {datetime} -- timestamp cursor for pagination
|
||||||
|
"""
|
||||||
|
|
||||||
|
query = gql(
|
||||||
|
"""
|
||||||
|
query UserActivity($action_type: String, $before:DateTime, $after: DateTime, $cursor: DateTime, $limit: Int){
|
||||||
|
activeUser {
|
||||||
|
activity(actionType: $action_type, before: $before, after: $after, cursor: $cursor, limit: $limit) {
|
||||||
|
totalCount
|
||||||
|
cursor
|
||||||
|
items {
|
||||||
|
actionType
|
||||||
|
info
|
||||||
|
userId
|
||||||
|
streamId
|
||||||
|
resourceId
|
||||||
|
resourceType
|
||||||
|
message
|
||||||
|
time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"limit": limit,
|
||||||
|
"action_type": action_type,
|
||||||
|
"before": before.astimezone(timezone.utc).isoformat() if before else before,
|
||||||
|
"after": after.astimezone(timezone.utc).isoformat() if after else after,
|
||||||
|
"cursor": cursor.astimezone(timezone.utc).isoformat() if cursor else cursor,
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.make_request(
|
||||||
|
query=query,
|
||||||
|
params=params,
|
||||||
|
return_type=["activeUser", "activity"],
|
||||||
|
schema=ActivityCollection,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_all_pending_invites(self) -> List[PendingStreamCollaborator]:
|
||||||
|
"""Get all of the active user's pending stream invites
|
||||||
|
|
||||||
|
Requires Speckle Server version >= 2.6.4
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[PendingStreamCollaborator] -- a list of pending invites for the current user
|
||||||
|
"""
|
||||||
|
metrics.track(metrics.INVITE, self.account, {"name": "get"})
|
||||||
|
self._check_invites_supported()
|
||||||
|
|
||||||
|
query = gql(
|
||||||
|
"""
|
||||||
|
query StreamInvites {
|
||||||
|
streamInvites{
|
||||||
|
id
|
||||||
|
token
|
||||||
|
inviteId
|
||||||
|
streamId
|
||||||
|
streamName
|
||||||
|
title
|
||||||
|
role
|
||||||
|
invitedBy {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
company
|
||||||
|
avatar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.make_request(
|
||||||
|
query=query,
|
||||||
|
return_type="streamInvites",
|
||||||
|
schema=PendingStreamCollaborator,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_pending_invite(
|
||||||
|
self, stream_id: str, token: Optional[str] = None
|
||||||
|
) -> Optional[PendingStreamCollaborator]:
|
||||||
|
"""Get a particular pending invite for the active user on a given stream.
|
||||||
|
If no invite_id is provided, any valid invite will be returned.
|
||||||
|
|
||||||
|
Requires Speckle Server version >= 2.6.4
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
stream_id {str} -- the id of the stream to look for invites on
|
||||||
|
token {str} -- the token of the invite to look for (optional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PendingStreamCollaborator -- the invite for the given stream (or None if it isn't found)
|
||||||
|
"""
|
||||||
|
metrics.track(metrics.INVITE, self.account, {"name": "get"})
|
||||||
|
self._check_invites_supported()
|
||||||
|
|
||||||
|
query = gql(
|
||||||
|
"""
|
||||||
|
query StreamInvite($streamId: String!, $token: String) {
|
||||||
|
streamInvite(streamId: $streamId, token: $token) {
|
||||||
|
id
|
||||||
|
token
|
||||||
|
streamId
|
||||||
|
streamName
|
||||||
|
title
|
||||||
|
role
|
||||||
|
invitedBy {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
company
|
||||||
|
avatar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
params = {"streamId": stream_id}
|
||||||
|
if token:
|
||||||
|
params["token"] = token
|
||||||
|
|
||||||
|
return self.make_request(
|
||||||
|
query=query,
|
||||||
|
params=params,
|
||||||
|
return_type="streamInvite",
|
||||||
|
schema=PendingStreamCollaborator,
|
||||||
|
)
|
||||||
@@ -4,7 +4,6 @@ from specklepy.api.models import Branch
|
|||||||
from specklepy.logging import metrics
|
from specklepy.logging import metrics
|
||||||
|
|
||||||
NAME = "branch"
|
NAME = "branch"
|
||||||
METHODS = ["create"]
|
|
||||||
|
|
||||||
|
|
||||||
class Resource(ResourceBase):
|
class Resource(ResourceBase):
|
||||||
@@ -16,7 +15,6 @@ class Resource(ResourceBase):
|
|||||||
basepath=basepath,
|
basepath=basepath,
|
||||||
client=client,
|
client=client,
|
||||||
name=NAME,
|
name=NAME,
|
||||||
methods=METHODS,
|
|
||||||
)
|
)
|
||||||
self.schema = Branch
|
self.schema = Branch
|
||||||
|
|
||||||
@@ -6,7 +6,6 @@ from specklepy.logging import metrics
|
|||||||
|
|
||||||
|
|
||||||
NAME = "commit"
|
NAME = "commit"
|
||||||
METHODS = []
|
|
||||||
|
|
||||||
|
|
||||||
class Resource(ResourceBase):
|
class Resource(ResourceBase):
|
||||||
@@ -18,7 +17,6 @@ class Resource(ResourceBase):
|
|||||||
basepath=basepath,
|
basepath=basepath,
|
||||||
client=client,
|
client=client,
|
||||||
name=NAME,
|
name=NAME,
|
||||||
methods=METHODS,
|
|
||||||
)
|
)
|
||||||
self.schema = Commit
|
self.schema = Commit
|
||||||
|
|
||||||
@@ -4,7 +4,6 @@ from specklepy.api.resource import ResourceBase
|
|||||||
from specklepy.objects.base import Base
|
from specklepy.objects.base import Base
|
||||||
|
|
||||||
NAME = "object"
|
NAME = "object"
|
||||||
METHODS = []
|
|
||||||
|
|
||||||
|
|
||||||
class Resource(ResourceBase):
|
class Resource(ResourceBase):
|
||||||
@@ -16,7 +15,6 @@ class Resource(ResourceBase):
|
|||||||
basepath=basepath,
|
basepath=basepath,
|
||||||
client=client,
|
client=client,
|
||||||
name=NAME,
|
name=NAME,
|
||||||
methods=METHODS,
|
|
||||||
)
|
)
|
||||||
self.schema = Base
|
self.schema = Base
|
||||||
|
|
||||||
@@ -52,7 +50,9 @@ class Resource(ResourceBase):
|
|||||||
params = {"stream_id": stream_id, "object_id": object_id}
|
params = {"stream_id": stream_id, "object_id": object_id}
|
||||||
|
|
||||||
return self.make_request(
|
return self.make_request(
|
||||||
query=query, params=params, return_type=["stream", "object", "data"]
|
query=query,
|
||||||
|
params=params,
|
||||||
|
return_type=["stream", "object", "data"],
|
||||||
)
|
)
|
||||||
|
|
||||||
def create(self, stream_id: str, objects: List[Dict]) -> str:
|
def create(self, stream_id: str, objects: List[Dict]) -> str:
|
||||||
@@ -1,50 +1,51 @@
|
|||||||
|
from typing import List, Optional, Union
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
from gql import gql
|
||||||
from specklepy.logging import metrics
|
from specklepy.logging import metrics
|
||||||
from specklepy.logging.exceptions import SpeckleException
|
from specklepy.logging.exceptions import SpeckleException
|
||||||
from typing import List
|
|
||||||
from gql import gql
|
|
||||||
from specklepy.api.resource import ResourceBase
|
from specklepy.api.resource import ResourceBase
|
||||||
from specklepy.api.models import ActivityCollection, User
|
from specklepy.api.models import (
|
||||||
|
ActivityCollection,
|
||||||
|
LimitedUser,
|
||||||
|
)
|
||||||
|
|
||||||
NAME = "user"
|
NAME = "other_user"
|
||||||
METHODS = ["get", "search", "update", "activity"]
|
|
||||||
|
|
||||||
|
|
||||||
class Resource(ResourceBase):
|
class Resource(ResourceBase):
|
||||||
"""API Access class for users"""
|
"""API Access class for other users, that are not the currently active user."""
|
||||||
|
|
||||||
def __init__(self, account, basepath, client) -> None:
|
def __init__(self, account, basepath, client, server_version) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
account=account,
|
account=account,
|
||||||
basepath=basepath,
|
basepath=basepath,
|
||||||
client=client,
|
client=client,
|
||||||
name=NAME,
|
name=NAME,
|
||||||
methods=METHODS,
|
server_version=server_version,
|
||||||
)
|
)
|
||||||
self.schema = User
|
self.schema = LimitedUser
|
||||||
|
|
||||||
def get(self, id: str = None) -> User:
|
def get(self, id: str) -> LimitedUser:
|
||||||
"""Gets the profile of a user. If no id argument is provided, will return the current authenticated user's profile (as extracted from the authorization header).
|
"""
|
||||||
|
Gets the profile of another user.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
id {str} -- the user id
|
id {str} -- the user id
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
User -- the retrieved user
|
LimitedUser -- the retrieved profile of another user
|
||||||
"""
|
"""
|
||||||
metrics.track(metrics.USER, self.account, {"name": "get"})
|
metrics.track(metrics.OTHER_USER, self.account, {"name": "get"})
|
||||||
query = gql(
|
query = gql(
|
||||||
"""
|
"""
|
||||||
query User($id: String) {
|
query OtherUser($id: String!) {
|
||||||
user(id: $id) {
|
otherUser(id: $id) {
|
||||||
id
|
id
|
||||||
email
|
|
||||||
name
|
name
|
||||||
bio
|
bio
|
||||||
company
|
company
|
||||||
avatar
|
avatar
|
||||||
verified
|
verified
|
||||||
profiles
|
|
||||||
role
|
role
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,23 +54,25 @@ class Resource(ResourceBase):
|
|||||||
|
|
||||||
params = {"id": id}
|
params = {"id": id}
|
||||||
|
|
||||||
return self.make_request(query=query, params=params, return_type="user")
|
return self.make_request(query=query, params=params, return_type="otherUser")
|
||||||
|
|
||||||
def search(self, search_query: str, limit: int = 25) -> List[User]:
|
def search(
|
||||||
|
self, search_query: str, limit: int = 25
|
||||||
|
) -> Union[List[LimitedUser], SpeckleException]:
|
||||||
"""Searches for user by name or email. The search query must be at least 3 characters long
|
"""Searches for user by name or email. The search query must be at least 3 characters long
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
search_query {str} -- a string to search for
|
search_query {str} -- a string to search for
|
||||||
limit {int} -- the maximum number of results to return
|
limit {int} -- the maximum number of results to return
|
||||||
Returns:
|
Returns:
|
||||||
List[User] -- a list of User objects that match the search query
|
List[LimitedUser] -- a list of User objects that match the search query
|
||||||
"""
|
"""
|
||||||
if len(search_query) < 3:
|
if len(search_query) < 3:
|
||||||
return SpeckleException(
|
return SpeckleException(
|
||||||
message="User search query must be at least 3 characters"
|
message="User search query must be at least 3 characters"
|
||||||
)
|
)
|
||||||
|
|
||||||
metrics.track(metrics.USER, self.account, {"name": "search"})
|
metrics.track(metrics.OTHER_USER, self.account, {"name": "search"})
|
||||||
query = gql(
|
query = gql(
|
||||||
"""
|
"""
|
||||||
query UserSearch($search_query: String!, $limit: Int!) {
|
query UserSearch($search_query: String!, $limit: Int!) {
|
||||||
@@ -92,53 +95,17 @@ class Resource(ResourceBase):
|
|||||||
query=query, params=params, return_type=["userSearch", "items"]
|
query=query, params=params, return_type=["userSearch", "items"]
|
||||||
)
|
)
|
||||||
|
|
||||||
def update(
|
|
||||||
self, name: str = None, company: str = None, bio: str = None, avatar: str = None
|
|
||||||
):
|
|
||||||
"""Updates your user profile. All arguments are optional.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
name {str} -- your name
|
|
||||||
company {str} -- the company you may or may not work for
|
|
||||||
bio {str} -- tell us about yourself
|
|
||||||
avatar {str} -- a nice photo of yourself
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool -- True if your profile was updated successfully
|
|
||||||
"""
|
|
||||||
metrics.track(metrics.USER, self.account, {"name": "update"})
|
|
||||||
query = gql(
|
|
||||||
"""
|
|
||||||
mutation UserUpdate($user: UserUpdateInput!) {
|
|
||||||
userUpdate(user: $user)
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
params = {"name": name, "company": company, "bio": bio, "avatar": avatar}
|
|
||||||
|
|
||||||
params = {"user": {k: v for k, v in params.items() if v is not None}}
|
|
||||||
|
|
||||||
if not params["user"]:
|
|
||||||
return SpeckleException(
|
|
||||||
message="You must provide at least one field to update your user profile"
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.make_request(
|
|
||||||
query=query, params=params, return_type="userUpdate", parse_response=False
|
|
||||||
)
|
|
||||||
|
|
||||||
def activity(
|
def activity(
|
||||||
self,
|
self,
|
||||||
user_id: str = None,
|
user_id: str,
|
||||||
limit: int = 20,
|
limit: int = 20,
|
||||||
action_type: str = None,
|
action_type: Optional[str] = None,
|
||||||
before: datetime = None,
|
before: Optional[datetime] = None,
|
||||||
after: datetime = None,
|
after: Optional[datetime] = None,
|
||||||
cursor: datetime = None,
|
cursor: Optional[datetime] = None,
|
||||||
):
|
) -> ActivityCollection:
|
||||||
"""
|
"""
|
||||||
Get the activity from a given stream in an Activity collection. Step into the activity `items` for the list of activity.
|
Get the activity from a given stream in an Activity collection. Step into the activity `items` for the list of activity.
|
||||||
If no id argument is provided, will return the current authenticated user's activity (as extracted from the authorization header).
|
|
||||||
|
|
||||||
Note: all timestamps arguments should be `datetime` of any tz as they will be converted to UTC ISO format strings
|
Note: all timestamps arguments should be `datetime` of any tz as they will be converted to UTC ISO format strings
|
||||||
|
|
||||||
@@ -152,8 +119,8 @@ class Resource(ResourceBase):
|
|||||||
|
|
||||||
query = gql(
|
query = gql(
|
||||||
"""
|
"""
|
||||||
query UserActivity($user_id: String, $action_type: String, $before:DateTime, $after: DateTime, $cursor: DateTime, $limit: Int){
|
query UserActivity($user_id: String!, $action_type: String, $before:DateTime, $after: DateTime, $cursor: DateTime, $limit: Int){
|
||||||
user(id: $user_id) {
|
otherUser(id: $user_id) {
|
||||||
activity(actionType: $action_type, before: $before, after: $after, cursor: $cursor, limit: $limit) {
|
activity(actionType: $action_type, before: $before, after: $after, cursor: $cursor, limit: $limit) {
|
||||||
totalCount
|
totalCount
|
||||||
cursor
|
cursor
|
||||||
@@ -185,6 +152,6 @@ class Resource(ResourceBase):
|
|||||||
return self.make_request(
|
return self.make_request(
|
||||||
query=query,
|
query=query,
|
||||||
params=params,
|
params=params,
|
||||||
return_type=["user", "activity"],
|
return_type=["otherUser", "activity"],
|
||||||
schema=ActivityCollection,
|
schema=ActivityCollection,
|
||||||
)
|
)
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
from typing import Dict, List
|
import re
|
||||||
|
from typing import Any, Dict, List, Tuple
|
||||||
from gql import gql
|
from gql import gql
|
||||||
from specklepy.api.models import ServerInfo
|
from specklepy.api.models import ServerInfo
|
||||||
from specklepy.api.resource import ResourceBase
|
from specklepy.api.resource import ResourceBase
|
||||||
from specklepy.logging import metrics
|
from specklepy.logging import metrics
|
||||||
|
from specklepy.logging.exceptions import GraphQLException
|
||||||
|
|
||||||
|
|
||||||
NAME = "server"
|
NAME = "server"
|
||||||
METHODS = ["get", "apps"]
|
|
||||||
|
|
||||||
|
|
||||||
class Resource(ResourceBase):
|
class Resource(ResourceBase):
|
||||||
@@ -18,7 +19,6 @@ class Resource(ResourceBase):
|
|||||||
basepath=basepath,
|
basepath=basepath,
|
||||||
client=client,
|
client=client,
|
||||||
name=NAME,
|
name=NAME,
|
||||||
methods=METHODS,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def get(self) -> ServerInfo:
|
def get(self) -> ServerInfo:
|
||||||
@@ -61,6 +61,39 @@ class Resource(ResourceBase):
|
|||||||
query=query, return_type="serverInfo", schema=ServerInfo
|
query=query, return_type="serverInfo", schema=ServerInfo
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def version(self) -> Tuple[Any, ...]:
|
||||||
|
"""Get the server version
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple -- the server version in the format (major, minor, patch, (tag, build))
|
||||||
|
eg (2, 6, 3) for a stable build and (2, 6, 4, 'alpha', 4711) for alpha
|
||||||
|
"""
|
||||||
|
# not tracking as it will be called along with other mutations / queries as a check
|
||||||
|
query = gql(
|
||||||
|
"""
|
||||||
|
query Server {
|
||||||
|
serverInfo {
|
||||||
|
version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
ver = self.make_request(
|
||||||
|
query=query, return_type=["serverInfo", "version"], parse_response=False
|
||||||
|
)
|
||||||
|
if isinstance(ver, Exception):
|
||||||
|
raise GraphQLException(
|
||||||
|
f"Could not get server version for {self.basepath}", [ver]
|
||||||
|
)
|
||||||
|
|
||||||
|
# pylint: disable=consider-using-generator; (list comp is faster)
|
||||||
|
return tuple(
|
||||||
|
[
|
||||||
|
int(segment) if segment.isdigit() else segment
|
||||||
|
for segment in re.split(r"\.|-", ver)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
def apps(self) -> Dict:
|
def apps(self) -> Dict:
|
||||||
"""Get the apps registered on the server
|
"""Get the apps registered on the server
|
||||||
|
|
||||||
@@ -1,26 +1,26 @@
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
from typing import List, Optional
|
||||||
|
from deprecated import deprecated
|
||||||
from gql import gql
|
from gql import gql
|
||||||
from typing import List
|
|
||||||
from specklepy.logging import metrics
|
from specklepy.logging import metrics
|
||||||
from specklepy.api.models import ActivityCollection, Stream
|
from specklepy.api.models import ActivityCollection, PendingStreamCollaborator, Stream
|
||||||
from specklepy.api.resource import ResourceBase
|
from specklepy.api.resource import ResourceBase
|
||||||
from specklepy.logging.exceptions import SpeckleException
|
from specklepy.logging.exceptions import UnsupportedException, SpeckleException
|
||||||
|
|
||||||
|
|
||||||
NAME = "stream"
|
NAME = "stream"
|
||||||
METHODS = ["list", "create", "get", "update", "delete", "search", "activity"]
|
|
||||||
|
|
||||||
|
|
||||||
class Resource(ResourceBase):
|
class Resource(ResourceBase):
|
||||||
"""API Access class for streams"""
|
"""API Access class for streams"""
|
||||||
|
|
||||||
def __init__(self, account, basepath, client) -> None:
|
def __init__(self, account, basepath, client, server_version) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
account=account,
|
account=account,
|
||||||
basepath=basepath,
|
basepath=basepath,
|
||||||
client=client,
|
client=client,
|
||||||
name=NAME,
|
name=NAME,
|
||||||
methods=METHODS,
|
server_version=server_version,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.schema = Stream
|
self.schema = Stream
|
||||||
@@ -342,9 +342,18 @@ class Resource(ResourceBase):
|
|||||||
query=query, params=params, return_type=["streamFavorite"]
|
query=query, params=params, return_type=["streamFavorite"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@deprecated(
|
||||||
|
version="2.6.4",
|
||||||
|
reason=(
|
||||||
|
"As of Speckle Server v2.6.4, this method is deprecated. "
|
||||||
|
"Users need to be invited and accept the invite before being added to a stream"
|
||||||
|
),
|
||||||
|
)
|
||||||
def grant_permission(self, stream_id: str, user_id: str, role: str):
|
def grant_permission(self, stream_id: str, user_id: str, role: str):
|
||||||
"""Grant permissions to a user on a given stream
|
"""Grant permissions to a user on a given stream
|
||||||
|
|
||||||
|
Valid for Speckle Server version < 2.6.4
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
stream_id {str} -- the id of the stream to grant permissions to
|
stream_id {str} -- the id of the stream to grant permissions to
|
||||||
user_id {str} -- the id of the user to grant permissions for
|
user_id {str} -- the id of the user to grant permissions for
|
||||||
@@ -354,6 +363,19 @@ class Resource(ResourceBase):
|
|||||||
bool -- True if the operation was successful
|
bool -- True if the operation was successful
|
||||||
"""
|
"""
|
||||||
metrics.track(metrics.PERMISSION, self.account, {"name": "add", "role": role})
|
metrics.track(metrics.PERMISSION, self.account, {"name": "add", "role": role})
|
||||||
|
# we're checking for the actual version info, and if the version is 'dev' we treat it
|
||||||
|
# as an up to date instance
|
||||||
|
if self.server_version and (
|
||||||
|
self.server_version == ("dev",) or self.server_version >= (2, 6, 4)
|
||||||
|
):
|
||||||
|
raise UnsupportedException(
|
||||||
|
(
|
||||||
|
"Server mutation `grant_permission` is no longer supported as of Speckle Server v2.6.4. "
|
||||||
|
"Please use the new `update_permission` method to change an existing user's permission "
|
||||||
|
"or use the `invite` method to invite a user to a stream."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
query = gql(
|
query = gql(
|
||||||
"""
|
"""
|
||||||
mutation StreamGrantPermission($permission_params: StreamGrantPermissionInput !) {
|
mutation StreamGrantPermission($permission_params: StreamGrantPermissionInput !) {
|
||||||
@@ -377,6 +399,284 @@ class Resource(ResourceBase):
|
|||||||
parse_response=False,
|
parse_response=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_all_pending_invites(
|
||||||
|
self, stream_id: str
|
||||||
|
) -> List[PendingStreamCollaborator]:
|
||||||
|
"""Get all of the pending invites on a stream.
|
||||||
|
You must be a `stream:owner` to query this.
|
||||||
|
|
||||||
|
Requires Speckle Server version >= 2.6.4
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
stream_id {str} -- the stream id from which to get the pending invites
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[PendingStreamCollaborator] -- a list of pending invites for the specified stream
|
||||||
|
"""
|
||||||
|
metrics.track(metrics.INVITE, self.account, {"name": "get"})
|
||||||
|
self._check_invites_supported()
|
||||||
|
|
||||||
|
query = gql(
|
||||||
|
"""
|
||||||
|
query StreamInvites($streamId: String!) {
|
||||||
|
stream(id: $streamId){
|
||||||
|
pendingCollaborators {
|
||||||
|
id
|
||||||
|
token
|
||||||
|
inviteId
|
||||||
|
streamId
|
||||||
|
streamName
|
||||||
|
title
|
||||||
|
role
|
||||||
|
invitedBy{
|
||||||
|
id
|
||||||
|
name
|
||||||
|
company
|
||||||
|
avatar
|
||||||
|
}
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
company
|
||||||
|
avatar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
params = {"streamId": stream_id}
|
||||||
|
|
||||||
|
return self.make_request(
|
||||||
|
query=query,
|
||||||
|
params=params,
|
||||||
|
return_type=["stream", "pendingCollaborators"],
|
||||||
|
schema=PendingStreamCollaborator,
|
||||||
|
)
|
||||||
|
|
||||||
|
def invite(
|
||||||
|
self,
|
||||||
|
stream_id: str,
|
||||||
|
email: str = None,
|
||||||
|
user_id: str = None,
|
||||||
|
role: str = "stream:contributor", # should default be reviewer?
|
||||||
|
message: str = None,
|
||||||
|
):
|
||||||
|
"""Invite someone to a stream using either their email or user id
|
||||||
|
|
||||||
|
Requires Speckle Server version >= 2.6.4
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
stream_id {str} -- the id of the stream to invite the user to
|
||||||
|
email {str} -- the email of the user to invite (use this OR `user_id`)
|
||||||
|
user_id {str} -- the id of the user to invite (use this OR `email`)
|
||||||
|
role {str} -- the role to assign to the user (defaults to `stream:contributor`)
|
||||||
|
message {str} -- a message to send along with this invite to the specified user
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool -- True if the operation was successful
|
||||||
|
"""
|
||||||
|
metrics.track(metrics.INVITE, self.account, {"name": "create"})
|
||||||
|
self._check_invites_supported()
|
||||||
|
|
||||||
|
if email is None and user_id is None:
|
||||||
|
raise SpeckleException(
|
||||||
|
"You must provide either an email or a user id to use the `stream.invite` method"
|
||||||
|
)
|
||||||
|
|
||||||
|
query = gql(
|
||||||
|
"""
|
||||||
|
mutation StreamInviteCreate($input: StreamInviteCreateInput!) {
|
||||||
|
streamInviteCreate(input: $input)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"email": email,
|
||||||
|
"userId": user_id,
|
||||||
|
"streamId": stream_id,
|
||||||
|
"message": message,
|
||||||
|
"role": role,
|
||||||
|
}
|
||||||
|
params = {"input": {k: v for k, v in params.items() if v is not None}}
|
||||||
|
|
||||||
|
return self.make_request(
|
||||||
|
query=query,
|
||||||
|
params=params,
|
||||||
|
return_type="streamInviteCreate",
|
||||||
|
parse_response=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def invite_batch(
|
||||||
|
self,
|
||||||
|
stream_id: str,
|
||||||
|
emails: List[str] = None,
|
||||||
|
user_ids: List[None] = None,
|
||||||
|
message: str = None,
|
||||||
|
) -> bool:
|
||||||
|
"""Invite a batch of users to a specified stream.
|
||||||
|
|
||||||
|
Requires Speckle Server version >= 2.6.4
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
stream_id {str} -- the id of the stream to invite the user to
|
||||||
|
emails {List[str]} -- the email of the user to invite (use this and/or `user_ids`)
|
||||||
|
user_id {List[str]} -- the id of the user to invite (use this and/or `emails`)
|
||||||
|
message {str} -- a message to send along with this invite to the specified user
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool -- True if the operation was successful
|
||||||
|
"""
|
||||||
|
metrics.track(metrics.INVITE, self.account, {"name": "batch create"})
|
||||||
|
self._check_invites_supported()
|
||||||
|
if emails is None and user_ids is None:
|
||||||
|
raise SpeckleException(
|
||||||
|
"You must provide either an email or a user id to use the `stream.invite` method"
|
||||||
|
)
|
||||||
|
|
||||||
|
query = gql(
|
||||||
|
"""
|
||||||
|
mutation StreamInviteBatchCreate($input: [StreamInviteCreateInput!]!) {
|
||||||
|
streamInviteBatchCreate(input: $input)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
email_invites = [
|
||||||
|
{"streamId": stream_id, "message": message, "email": email}
|
||||||
|
for email in emails
|
||||||
|
if emails is not None
|
||||||
|
]
|
||||||
|
|
||||||
|
user_invites = [
|
||||||
|
{"streamId": stream_id, "message": message, "userId": user_id}
|
||||||
|
for user_id in user_ids
|
||||||
|
if user_ids is not None
|
||||||
|
]
|
||||||
|
|
||||||
|
params = {"input": [*email_invites, *user_invites]}
|
||||||
|
|
||||||
|
return self.make_request(
|
||||||
|
query=query,
|
||||||
|
params=params,
|
||||||
|
return_type="streamInviteBatchCreate",
|
||||||
|
parse_response=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def invite_cancel(self, stream_id: str, invite_id: str) -> bool:
|
||||||
|
"""Cancel an existing stream invite
|
||||||
|
|
||||||
|
Requires Speckle Server version >= 2.6.4
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
stream_id {str} -- the id of the stream invite
|
||||||
|
invite_id {str} -- the id of the invite to use
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool -- true if the operation was successful
|
||||||
|
"""
|
||||||
|
metrics.track(metrics.INVITE, self.account, {"name": "cancel"})
|
||||||
|
self._check_invites_supported()
|
||||||
|
|
||||||
|
query = gql(
|
||||||
|
"""
|
||||||
|
mutation StreamInviteCancel($streamId: String!, $inviteId: String!) {
|
||||||
|
streamInviteCancel(streamId: $streamId, inviteId: $inviteId)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
params = {"streamId": stream_id, "inviteId": invite_id}
|
||||||
|
|
||||||
|
return self.make_request(
|
||||||
|
query=query,
|
||||||
|
params=params,
|
||||||
|
return_type="streamInviteCancel",
|
||||||
|
parse_response=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def invite_use(self, stream_id: str, token: str, accept: bool = True) -> bool:
|
||||||
|
"""Accept or decline a stream invite
|
||||||
|
|
||||||
|
Requires Speckle Server version >= 2.6.4
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
stream_id {str} -- the id of the stream for which the user has a pending invite
|
||||||
|
token {str} -- the token of the invite to use
|
||||||
|
accept {bool} -- whether or not to accept the invite (defaults to True)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool -- true if the operation was successful
|
||||||
|
"""
|
||||||
|
metrics.track(metrics.INVITE, self.account, {"name": "use"})
|
||||||
|
self._check_invites_supported()
|
||||||
|
|
||||||
|
query = gql(
|
||||||
|
"""
|
||||||
|
mutation StreamInviteUse($accept: Boolean!, $streamId: String!, $token: String!) {
|
||||||
|
streamInviteUse(accept: $accept, streamId: $streamId, token: $token)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
params = {"streamId": stream_id, "token": token, "accept": accept}
|
||||||
|
|
||||||
|
return self.make_request(
|
||||||
|
query=query,
|
||||||
|
params=params,
|
||||||
|
return_type="streamInviteUse",
|
||||||
|
parse_response=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_permission(self, stream_id: str, user_id: str, role: str):
|
||||||
|
"""Updates permissions for a user on a given stream
|
||||||
|
|
||||||
|
Valid for Speckle Server >=2.6.4
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
stream_id {str} -- the id of the stream to grant permissions to
|
||||||
|
user_id {str} -- the id of the user to grant permissions for
|
||||||
|
role {str} -- the role to grant the user
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool -- True if the operation was successful
|
||||||
|
"""
|
||||||
|
metrics.track(
|
||||||
|
metrics.PERMISSION, self.account, {"name": "update", "role": role}
|
||||||
|
)
|
||||||
|
if self.server_version and (
|
||||||
|
self.server_version != ("dev",) and self.server_version < (2, 6, 4)
|
||||||
|
):
|
||||||
|
raise UnsupportedException(
|
||||||
|
(
|
||||||
|
"Server mutation `update_permission` is only supported as of Speckle Server v2.6.4. "
|
||||||
|
"Please update your Speckle Server to use this method or use the `grant_permission` method instead."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
query = gql(
|
||||||
|
"""
|
||||||
|
mutation StreamUpdatePermission($permission_params: StreamUpdatePermissionInput !) {
|
||||||
|
streamUpdatePermission(permissionParams: $permission_params)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"permission_params": {
|
||||||
|
"streamId": stream_id,
|
||||||
|
"userId": user_id,
|
||||||
|
"role": role,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.make_request(
|
||||||
|
query=query,
|
||||||
|
params=params,
|
||||||
|
return_type="streamUpdatePermission",
|
||||||
|
parse_response=False,
|
||||||
|
)
|
||||||
|
|
||||||
def revoke_permission(self, stream_id: str, user_id: str):
|
def revoke_permission(self, stream_id: str, user_id: str):
|
||||||
"""Revoke permissions from a user on a given stream
|
"""Revoke permissions from a user on a given stream
|
||||||
|
|
||||||
+5
-10
@@ -1,16 +1,12 @@
|
|||||||
from typing import Callable, Dict, List
|
from typing import Callable, Dict, List, Union
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from gql import gql
|
from gql import gql
|
||||||
|
from graphql import DocumentNode
|
||||||
from specklepy.api.resource import ResourceBase
|
from specklepy.api.resource import ResourceBase
|
||||||
from specklepy.api.resources.stream import Stream
|
from specklepy.api.resources.stream import Stream
|
||||||
from specklepy.logging.exceptions import SpeckleException
|
from specklepy.logging.exceptions import SpeckleException
|
||||||
|
|
||||||
NAME = "subscribe"
|
NAME = "subscribe"
|
||||||
METHODS = [
|
|
||||||
"stream_added",
|
|
||||||
"stream_updated",
|
|
||||||
"stream_removed",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def check_wsclient(function):
|
def check_wsclient(function):
|
||||||
@@ -35,7 +31,6 @@ class Resource(ResourceBase):
|
|||||||
basepath=basepath,
|
basepath=basepath,
|
||||||
client=client,
|
client=client,
|
||||||
name=NAME,
|
name=NAME,
|
||||||
methods=METHODS,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@check_wsclient
|
@check_wsclient
|
||||||
@@ -109,15 +104,15 @@ class Resource(ResourceBase):
|
|||||||
@check_wsclient
|
@check_wsclient
|
||||||
async def subscribe(
|
async def subscribe(
|
||||||
self,
|
self,
|
||||||
query: gql,
|
query: DocumentNode,
|
||||||
params: Dict = None,
|
params: Dict = None,
|
||||||
callback: Callable = None,
|
callback: Callable = None,
|
||||||
return_type: str or List = None,
|
return_type: Union[str, List] = None,
|
||||||
schema=None,
|
schema=None,
|
||||||
parse_response: bool = True,
|
parse_response: bool = True,
|
||||||
):
|
):
|
||||||
# if self.client.transport.websocket is None:
|
# if self.client.transport.websocket is None:
|
||||||
# TODO: add multiple subs to the same ws connection
|
# TODO: add multiple subs to the same ws connection
|
||||||
async with self.client as session:
|
async with self.client as session:
|
||||||
async for res in session.subscribe(query, variable_values=params):
|
async for res in session.subscribe(query, variable_values=params):
|
||||||
res = self._step_into_response(response=res, return_type=return_type)
|
res = self._step_into_response(response=res, return_type=return_type)
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
from typing import List, Optional, Union
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from gql import gql
|
||||||
|
from specklepy.logging import metrics
|
||||||
|
from specklepy.logging.exceptions import SpeckleException
|
||||||
|
from specklepy.api.resource import ResourceBase
|
||||||
|
from specklepy.api.models import ActivityCollection, PendingStreamCollaborator, User
|
||||||
|
from deprecated import deprecated
|
||||||
|
|
||||||
|
|
||||||
|
NAME = "user"
|
||||||
|
|
||||||
|
DEPRECATION_VERSION = "2.9.0"
|
||||||
|
DEPRECATION_TEXT = "The user resource is deprecated, please use the active_user or other_user resources"
|
||||||
|
|
||||||
|
|
||||||
|
class Resource(ResourceBase):
|
||||||
|
"""API Access class for users"""
|
||||||
|
|
||||||
|
def __init__(self, account, basepath, client, server_version) -> None:
|
||||||
|
super().__init__(
|
||||||
|
account=account,
|
||||||
|
basepath=basepath,
|
||||||
|
client=client,
|
||||||
|
name=NAME,
|
||||||
|
server_version=server_version,
|
||||||
|
)
|
||||||
|
self.schema = User
|
||||||
|
|
||||||
|
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
|
||||||
|
def get(self, id: Optional[str] = None) -> User:
|
||||||
|
"""
|
||||||
|
Gets the profile of a user.
|
||||||
|
If no id argument is provided, will return the current authenticated
|
||||||
|
user's profile (as extracted from the authorization header).
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
id {str} -- the user id
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
User -- the retrieved user
|
||||||
|
"""
|
||||||
|
metrics.track(metrics.USER, self.account, {"name": "get"})
|
||||||
|
query = gql(
|
||||||
|
"""
|
||||||
|
query User($id: String) {
|
||||||
|
user(id: $id) {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
name
|
||||||
|
bio
|
||||||
|
company
|
||||||
|
avatar
|
||||||
|
verified
|
||||||
|
profiles
|
||||||
|
role
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
params = {"id": id}
|
||||||
|
|
||||||
|
return self.make_request(query=query, params=params, return_type="user")
|
||||||
|
|
||||||
|
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
|
||||||
|
def search(
|
||||||
|
self, search_query: str, limit: int = 25
|
||||||
|
) -> Union[List[User], SpeckleException]:
|
||||||
|
"""Searches for user by name or email. The search query must be at least 3 characters long
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
search_query {str} -- a string to search for
|
||||||
|
limit {int} -- the maximum number of results to return
|
||||||
|
Returns:
|
||||||
|
List[User] -- a list of User objects that match the search query
|
||||||
|
"""
|
||||||
|
if len(search_query) < 3:
|
||||||
|
return SpeckleException(
|
||||||
|
message="User search query must be at least 3 characters"
|
||||||
|
)
|
||||||
|
|
||||||
|
metrics.track(metrics.USER, self.account, {"name": "search"})
|
||||||
|
query = gql(
|
||||||
|
"""
|
||||||
|
query UserSearch($search_query: String!, $limit: Int!) {
|
||||||
|
userSearch(query: $search_query, limit: $limit) {
|
||||||
|
items {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
bio
|
||||||
|
company
|
||||||
|
avatar
|
||||||
|
verified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
params = {"search_query": search_query, "limit": limit}
|
||||||
|
|
||||||
|
return self.make_request(
|
||||||
|
query=query, params=params, return_type=["userSearch", "items"]
|
||||||
|
)
|
||||||
|
|
||||||
|
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
|
||||||
|
def update(
|
||||||
|
self,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
company: Optional[str] = None,
|
||||||
|
bio: Optional[str] = None,
|
||||||
|
avatar: Optional[str] = None,
|
||||||
|
):
|
||||||
|
"""Updates your user profile. All arguments are optional.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
name {str} -- your name
|
||||||
|
company {str} -- the company you may or may not work for
|
||||||
|
bio {str} -- tell us about yourself
|
||||||
|
avatar {str} -- a nice photo of yourself
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool -- True if your profile was updated successfully
|
||||||
|
"""
|
||||||
|
metrics.track(metrics.USER, self.account, {"name": "update"})
|
||||||
|
query = gql(
|
||||||
|
"""
|
||||||
|
mutation UserUpdate($user: UserUpdateInput!) {
|
||||||
|
userUpdate(user: $user)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
params = {"name": name, "company": company, "bio": bio, "avatar": avatar}
|
||||||
|
|
||||||
|
params = {"user": {k: v for k, v in params.items() if v is not None}}
|
||||||
|
|
||||||
|
if not params["user"]:
|
||||||
|
return SpeckleException(
|
||||||
|
message="You must provide at least one field to update your user profile"
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.make_request(
|
||||||
|
query=query, params=params, return_type="userUpdate", parse_response=False
|
||||||
|
)
|
||||||
|
|
||||||
|
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
|
||||||
|
def activity(
|
||||||
|
self,
|
||||||
|
user_id: Optional[str] = None,
|
||||||
|
limit: int = 20,
|
||||||
|
action_type: Optional[str] = None,
|
||||||
|
before: Optional[datetime] = None,
|
||||||
|
after: Optional[datetime] = None,
|
||||||
|
cursor: Optional[datetime] = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get the activity from a given stream in an Activity collection. Step into the activity `items` for the list of activity.
|
||||||
|
If no id argument is provided, will return the current authenticated user's activity (as extracted from the authorization header).
|
||||||
|
|
||||||
|
Note: all timestamps arguments should be `datetime` of any tz as they will be converted to UTC ISO format strings
|
||||||
|
|
||||||
|
user_id {str} -- the id of the user to get the activity from
|
||||||
|
action_type {str} -- filter results to a single action type (eg: `commit_create` or `commit_receive`)
|
||||||
|
limit {int} -- max number of Activity items to return
|
||||||
|
before {datetime} -- latest cutoff for activity (ie: return all activity _before_ this time)
|
||||||
|
after {datetime} -- oldest cutoff for activity (ie: return all activity _after_ this time)
|
||||||
|
cursor {datetime} -- timestamp cursor for pagination
|
||||||
|
"""
|
||||||
|
|
||||||
|
query = gql(
|
||||||
|
"""
|
||||||
|
query UserActivity($user_id: String, $action_type: String, $before:DateTime, $after: DateTime, $cursor: DateTime, $limit: Int){
|
||||||
|
user(id: $user_id) {
|
||||||
|
activity(actionType: $action_type, before: $before, after: $after, cursor: $cursor, limit: $limit) {
|
||||||
|
totalCount
|
||||||
|
cursor
|
||||||
|
items {
|
||||||
|
actionType
|
||||||
|
info
|
||||||
|
userId
|
||||||
|
streamId
|
||||||
|
resourceId
|
||||||
|
resourceType
|
||||||
|
message
|
||||||
|
time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"user_id": user_id,
|
||||||
|
"limit": limit,
|
||||||
|
"action_type": action_type,
|
||||||
|
"before": before.astimezone(timezone.utc).isoformat() if before else before,
|
||||||
|
"after": after.astimezone(timezone.utc).isoformat() if after else after,
|
||||||
|
"cursor": cursor.astimezone(timezone.utc).isoformat() if cursor else cursor,
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.make_request(
|
||||||
|
query=query,
|
||||||
|
params=params,
|
||||||
|
return_type=["user", "activity"],
|
||||||
|
schema=ActivityCollection,
|
||||||
|
)
|
||||||
|
|
||||||
|
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
|
||||||
|
def get_all_pending_invites(self) -> List[PendingStreamCollaborator]:
|
||||||
|
"""Get all of the active user's pending stream invites
|
||||||
|
|
||||||
|
Requires Speckle Server version >= 2.6.4
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[PendingStreamCollaborator] -- a list of pending invites for the current user
|
||||||
|
"""
|
||||||
|
metrics.track(metrics.INVITE, self.account, {"name": "get"})
|
||||||
|
self._check_invites_supported()
|
||||||
|
|
||||||
|
query = gql(
|
||||||
|
"""
|
||||||
|
query StreamInvites {
|
||||||
|
streamInvites{
|
||||||
|
id
|
||||||
|
token
|
||||||
|
inviteId
|
||||||
|
streamId
|
||||||
|
streamName
|
||||||
|
title
|
||||||
|
role
|
||||||
|
invitedBy {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
company
|
||||||
|
avatar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.make_request(
|
||||||
|
query=query,
|
||||||
|
return_type="streamInvites",
|
||||||
|
schema=PendingStreamCollaborator,
|
||||||
|
)
|
||||||
|
|
||||||
|
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
|
||||||
|
def get_pending_invite(
|
||||||
|
self, stream_id: str, token: Optional[str] = None
|
||||||
|
) -> Optional[PendingStreamCollaborator]:
|
||||||
|
"""Get a particular pending invite for the active user on a given stream.
|
||||||
|
If no invite_id is provided, any valid invite will be returned.
|
||||||
|
|
||||||
|
Requires Speckle Server version >= 2.6.4
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
stream_id {str} -- the id of the stream to look for invites on
|
||||||
|
token {str} -- the token of the invite to look for (optional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PendingStreamCollaborator -- the invite for the given stream (or None if it isn't found)
|
||||||
|
"""
|
||||||
|
metrics.track(metrics.INVITE, self.account, {"name": "get"})
|
||||||
|
self._check_invites_supported()
|
||||||
|
|
||||||
|
query = gql(
|
||||||
|
"""
|
||||||
|
query StreamInvite($streamId: String!, $token: String) {
|
||||||
|
streamInvite(streamId: $streamId, token: $token) {
|
||||||
|
id
|
||||||
|
token
|
||||||
|
streamId
|
||||||
|
streamName
|
||||||
|
title
|
||||||
|
role
|
||||||
|
invitedBy {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
company
|
||||||
|
avatar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
params = {"streamId": stream_id}
|
||||||
|
if token:
|
||||||
|
params["token"] = token
|
||||||
|
|
||||||
|
return self.make_request(
|
||||||
|
query=query,
|
||||||
|
params=params,
|
||||||
|
return_type="streamInvite",
|
||||||
|
schema=PendingStreamCollaborator,
|
||||||
|
)
|
||||||
@@ -110,7 +110,11 @@ class StreamWrapper:
|
|||||||
return self._account
|
return self._account
|
||||||
|
|
||||||
self._account = next(
|
self._account = next(
|
||||||
(a for a in get_local_accounts() if self.host in a.serverInfo.url),
|
(
|
||||||
|
a
|
||||||
|
for a in get_local_accounts()
|
||||||
|
if self.host == urlparse(a.serverInfo.url).netloc
|
||||||
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
from typing import Any, List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class SpeckleException(Exception):
|
||||||
|
def __init__(self, message: str, exception: Exception = None) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.message = message
|
||||||
|
self.exception = exception
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"SpeckleException: {self.message}"
|
||||||
|
|
||||||
|
|
||||||
|
class SpeckleInvalidUnitException(SpeckleException):
|
||||||
|
def __init__(self, invalid_unit: Any) -> None:
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
message=f"Invalid units: expected type str but received {type(invalid_unit)} ({invalid_unit}).",
|
||||||
|
exception=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SerializationException(SpeckleException):
|
||||||
|
def __init__(self, message: str, obj: Any, exception: Exception = None) -> None:
|
||||||
|
super().__init__(message=message, exception=exception)
|
||||||
|
self.obj = obj
|
||||||
|
self.unhandled_type = type(obj)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"SpeckleException: Could not serialize object of type {self.unhandled_type}"
|
||||||
|
|
||||||
|
|
||||||
|
class GraphQLException(SpeckleException):
|
||||||
|
def __init__(
|
||||||
|
self, message: str, errors: Optional[List[Any]] = None, data=None
|
||||||
|
) -> None:
|
||||||
|
super().__init__(message=message)
|
||||||
|
self.errors = errors
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"GraphQLException: {self.message}"
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedException(SpeckleException):
|
||||||
|
def __init__(self, message: str) -> None:
|
||||||
|
super().__init__(message=message)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"UnsupportedException: {self.message}"
|
||||||
|
|
||||||
|
|
||||||
|
class SpeckleWarning(Warning):
|
||||||
|
def __init__(self, *args: object) -> None:
|
||||||
|
super().__init__(*args)
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
import socket
|
|
||||||
import sys
|
import sys
|
||||||
import queue
|
import queue
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import getpass
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Optional
|
||||||
import requests
|
import requests
|
||||||
import threading
|
import threading
|
||||||
|
import platform
|
||||||
|
import contextlib
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Anonymous telemetry to help us understand how to make a better Speckle.
|
Anonymous telemetry to help us understand how to make a better Speckle.
|
||||||
@@ -12,7 +16,7 @@ This really helps us to deliver a better open source project and product!
|
|||||||
"""
|
"""
|
||||||
TRACK = True
|
TRACK = True
|
||||||
HOST_APP = "python"
|
HOST_APP = "python"
|
||||||
HOST_APP_VERSION = f"python {'.'.join(map(str, sys.version_info[:3]))}"
|
HOST_APP_VERSION = f"python {'.'.join(map(str, sys.version_info[:2]))}"
|
||||||
PLATFORMS = {"win32": "Windows", "cygwin": "Windows", "darwin": "Mac OS X"}
|
PLATFORMS = {"win32": "Windows", "cygwin": "Windows", "darwin": "Mac OS X"}
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
@@ -23,9 +27,11 @@ RECEIVE = "Receive"
|
|||||||
SEND = "Send"
|
SEND = "Send"
|
||||||
STREAM = "Stream Action"
|
STREAM = "Stream Action"
|
||||||
PERMISSION = "Permission Action"
|
PERMISSION = "Permission Action"
|
||||||
|
INVITE = "Invite Action"
|
||||||
COMMIT = "Commit Action"
|
COMMIT = "Commit Action"
|
||||||
BRANCH = "Branch Action"
|
BRANCH = "Branch Action"
|
||||||
USER = "User Action"
|
USER = "User Action"
|
||||||
|
OTHER_USER = "Other User Action"
|
||||||
SERVER = "Server Action"
|
SERVER = "Server Action"
|
||||||
CLIENT = "Speckle Client"
|
CLIENT = "Speckle Client"
|
||||||
STREAM_WRAPPER = "Stream Wrapper"
|
STREAM_WRAPPER = "Stream Wrapper"
|
||||||
@@ -46,13 +52,13 @@ def enable():
|
|||||||
TRACK = True
|
TRACK = True
|
||||||
|
|
||||||
|
|
||||||
def set_host_app(host_app: str, host_app_version: str = None):
|
def set_host_app(host_app: str, host_app_version: Optional[str] = None):
|
||||||
global HOST_APP, HOST_APP_VERSION
|
global HOST_APP, HOST_APP_VERSION
|
||||||
HOST_APP = host_app
|
HOST_APP = host_app
|
||||||
HOST_APP_VERSION = host_app_version or HOST_APP_VERSION
|
HOST_APP_VERSION = host_app_version or HOST_APP_VERSION
|
||||||
|
|
||||||
|
|
||||||
def track(action: str, account: "Account" = None, custom_props: dict = None):
|
def track(action: str, account: "Account" = None, custom_props: Optional[dict] = None):
|
||||||
if not TRACK:
|
if not TRACK:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
@@ -75,7 +81,7 @@ def track(action: str, account: "Account" = None, custom_props: dict = None):
|
|||||||
METRICS_TRACKER.queue.put_nowait(event_params)
|
METRICS_TRACKER.queue.put_nowait(event_params)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
# wrapping this whole thing in a try except as we never want a failure here to annoy users!
|
# 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))
|
LOG.debug(f"Error queueing metrics request: {str(ex)}")
|
||||||
|
|
||||||
|
|
||||||
def initialise_tracker(account: "Account" = None):
|
def initialise_tracker(account: "Account" = None):
|
||||||
@@ -101,8 +107,7 @@ class Singleton(type):
|
|||||||
class MetricsTracker(metaclass=Singleton):
|
class MetricsTracker(metaclass=Singleton):
|
||||||
analytics_url = "https://analytics.speckle.systems/track?ip=1"
|
analytics_url = "https://analytics.speckle.systems/track?ip=1"
|
||||||
analytics_token = "acd87c5a50b56df91a795e999812a3a4"
|
analytics_token = "acd87c5a50b56df91a795e999812a3a4"
|
||||||
user_ip = None
|
last_user = ""
|
||||||
last_user = None
|
|
||||||
last_server = None
|
last_server = None
|
||||||
platform = None
|
platform = None
|
||||||
sending_thread = None
|
sending_thread = None
|
||||||
@@ -114,12 +119,15 @@ class MetricsTracker(metaclass=Singleton):
|
|||||||
)
|
)
|
||||||
self.platform = PLATFORMS.get(sys.platform, "linux")
|
self.platform = PLATFORMS.get(sys.platform, "linux")
|
||||||
self.sending_thread.start()
|
self.sending_thread.start()
|
||||||
self.user_ip = socket.gethostbyname(socket.gethostname())
|
with contextlib.suppress(Exception):
|
||||||
|
node, user = platform.node(), getpass.getuser()
|
||||||
|
if node and user:
|
||||||
|
self.last_user = f"@{self.hash(f'{node}-{user}')}"
|
||||||
|
|
||||||
def set_last_user(self, email: str):
|
def set_last_user(self, email: str):
|
||||||
if not email:
|
if not email:
|
||||||
return
|
return
|
||||||
self.last_user = "@" + self.hash(email)
|
self.last_user = f"@{self.hash(email)}"
|
||||||
|
|
||||||
def set_last_server(self, server: str):
|
def set_last_server(self, server: str):
|
||||||
if not server:
|
if not server:
|
||||||
@@ -137,6 +145,6 @@ class MetricsTracker(metaclass=Singleton):
|
|||||||
try:
|
try:
|
||||||
session.post(self.analytics_url, json=event_params)
|
session.post(self.analytics_url, json=event_params)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
LOG.error("Error sending metrics request: " + str(ex))
|
LOG.debug(f"Error sending metrics request: {str(ex)}")
|
||||||
|
|
||||||
self.queue.task_done()
|
self.queue.task_done()
|
||||||
@@ -14,8 +14,8 @@ import contextlib
|
|||||||
from enum import EnumMeta
|
from enum import EnumMeta
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
|
||||||
from specklepy.logging.exceptions import SpeckleException
|
from specklepy.logging.exceptions import SpeckleException, SpeckleInvalidUnitException
|
||||||
from specklepy.objects.units import get_units_from_string
|
from specklepy.objects.units import get_units_from_string, Units
|
||||||
from specklepy.transports.memory import MemoryTransport
|
from specklepy.transports.memory import MemoryTransport
|
||||||
|
|
||||||
PRIMITIVES = (int, float, str, bool)
|
PRIMITIVES = (int, float, str, bool)
|
||||||
@@ -92,12 +92,19 @@ class _RegisteringBase:
|
|||||||
speckle_type: ClassVar[str]
|
speckle_type: ClassVar[str]
|
||||||
_type_registry: ClassVar[Dict[str, "Base"]] = {}
|
_type_registry: ClassVar[Dict[str, "Base"]] = {}
|
||||||
_attr_types: ClassVar[Dict[str, Type]] = {}
|
_attr_types: ClassVar[Dict[str, Type]] = {}
|
||||||
|
# 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()
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
validate_assignment = True
|
validate_assignment = True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_registered_type(cls, speckle_type: str) -> Optional[Type["Base"]]:
|
def get_registered_type(
|
||||||
|
cls, speckle_type: str
|
||||||
|
) -> Union["Base", Type["Base"], None]:
|
||||||
"""Get the registered type from the protected mapping via the `speckle_type`"""
|
"""Get the registered type from the protected mapping via the `speckle_type`"""
|
||||||
return cls._type_registry.get(speckle_type, None)
|
return cls._type_registry.get(speckle_type, None)
|
||||||
|
|
||||||
@@ -139,15 +146,10 @@ class _RegisteringBase:
|
|||||||
|
|
||||||
|
|
||||||
class Base(_RegisteringBase):
|
class Base(_RegisteringBase):
|
||||||
id: Optional[str] = None
|
id: Union[str, None] = None
|
||||||
totalChildrenCount: Optional[int] = None
|
totalChildrenCount: Union[int, None] = None
|
||||||
applicationId: Optional[str] = None
|
applicationId: Union[str, None] = None
|
||||||
_units: str = "m"
|
_units: Union[Units, None] = None
|
||||||
# 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:
|
def __init__(self, **kwargs) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -311,14 +313,23 @@ class Base(_RegisteringBase):
|
|||||||
self._detachable = self._detachable.union(names)
|
self._detachable = self._detachable.union(names)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def units(self):
|
def units(self) -> Union[str, None]:
|
||||||
return self._units
|
if self._units:
|
||||||
|
return self._units.value
|
||||||
|
return None
|
||||||
|
|
||||||
@units.setter
|
@units.setter
|
||||||
def units(self, value: str):
|
def units(self, value: Union[str, Units, None]):
|
||||||
units = get_units_from_string(value)
|
if value == None:
|
||||||
if units:
|
units = value
|
||||||
self._units = units
|
elif isinstance(value, Units):
|
||||||
|
units: Units = value
|
||||||
|
else:
|
||||||
|
units = get_units_from_string(value)
|
||||||
|
self._units = units
|
||||||
|
# except SpeckleInvalidUnitException as ex:
|
||||||
|
# warn(f"Units are reset to None. Reason {ex.message}")
|
||||||
|
# self._units = None
|
||||||
|
|
||||||
def get_member_names(self) -> List[str]:
|
def get_member_names(self) -> List[str]:
|
||||||
"""Get all of the property names on this object, dynamic or not"""
|
"""Get all of the property names on this object, dynamic or not"""
|
||||||
@@ -331,7 +342,7 @@ class Base(_RegisteringBase):
|
|||||||
|
|
||||||
def get_serializable_attributes(self) -> List[str]:
|
def get_serializable_attributes(self) -> List[str]:
|
||||||
"""Get the attributes that should be serialized"""
|
"""Get the attributes that should be serialized"""
|
||||||
return list(set(self.get_member_names()) - self._serialize_ignore)
|
return sorted(list(set(self.get_member_names()) - self._serialize_ignore))
|
||||||
|
|
||||||
def get_typed_member_names(self) -> List[str]:
|
def get_typed_member_names(self) -> List[str]:
|
||||||
"""Get all of the names of the defined (typed) properties of this object"""
|
"""Get all of the names of the defined (typed) properties of this object"""
|
||||||
@@ -378,6 +389,7 @@ class Base(_RegisteringBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _handle_object_count(self, obj: Any, parsed: List) -> int:
|
def _handle_object_count(self, obj: Any, parsed: List) -> int:
|
||||||
|
# pylint: disable=isinstance-second-argument-not-valid-type
|
||||||
count = 0
|
count = 0
|
||||||
if obj is None:
|
if obj is None:
|
||||||
return count
|
return count
|
||||||
@@ -406,7 +418,7 @@ Base.update_forward_refs()
|
|||||||
|
|
||||||
|
|
||||||
class DataChunk(Base, speckle_type="Speckle.Core.Models.DataChunk"):
|
class DataChunk(Base, speckle_type="Speckle.Core.Models.DataChunk"):
|
||||||
data: List[Any] = None
|
data: Union[List[Any], None] = None
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Callable, List, Type
|
from typing import Any, Callable, List, Type, Dict
|
||||||
|
|
||||||
from specklepy.logging.exceptions import SpeckleException
|
from specklepy.logging.exceptions import SpeckleException
|
||||||
from specklepy.objects.base import Base
|
from specklepy.objects.base import Base
|
||||||
@@ -43,8 +43,8 @@ def curve_from_list(args: List[float]):
|
|||||||
|
|
||||||
|
|
||||||
class ObjectArray:
|
class ObjectArray:
|
||||||
def __init__(self) -> None:
|
def __init__(self, data: list = None) -> None:
|
||||||
self.data = []
|
self.data = data or []
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_objects(cls, objects: List[Base]) -> "ObjectArray":
|
def from_objects(cls, objects: List[Base]) -> "ObjectArray":
|
||||||
@@ -60,18 +60,17 @@ class ObjectArray:
|
|||||||
"All objects in chunk should have the same speckle_type. "
|
"All objects in chunk should have the same speckle_type. "
|
||||||
f"Found {speckle_type} and {obj.speckle_type}"
|
f"Found {speckle_type} and {obj.speckle_type}"
|
||||||
)
|
)
|
||||||
data_list.encode_object(object=obj)
|
data_list.encode_object(obj=obj)
|
||||||
|
|
||||||
return data_list
|
return data_list
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def decode_data(
|
def decode_data(
|
||||||
data: List[Any], decoder: Callable[[List[Any]], Base]
|
data: List[Any], decoder: Callable[[List[Any]], Base], **kwargs: Dict[str, Any]
|
||||||
) -> List[Base]:
|
) -> List[Base]:
|
||||||
bases = []
|
bases = []
|
||||||
if not data:
|
if not data:
|
||||||
return bases
|
return bases
|
||||||
|
|
||||||
index = 0
|
index = 0
|
||||||
while index < len(data):
|
while index < len(data):
|
||||||
item_length = int(data[index])
|
item_length = int(data[index])
|
||||||
@@ -79,19 +78,16 @@ class ObjectArray:
|
|||||||
item_end = item_start + item_length
|
item_end = item_start + item_length
|
||||||
item_data = data[item_start:item_end]
|
item_data = data[item_start:item_end]
|
||||||
index = item_end
|
index = item_end
|
||||||
# TODO: investigate what's going on w this fail
|
decoded_data = decoder(item_data, **kwargs)
|
||||||
try:
|
bases.append(decoded_data)
|
||||||
decoded_data = decoder(item_data)
|
|
||||||
bases.append(decoded_data)
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
return bases
|
return bases
|
||||||
|
|
||||||
def decode(self, decoder: Callable[[List[Any]], Any]):
|
def decode(self, decoder: Callable[[List[Any]], Any], **kwargs: Dict[str, Any]):
|
||||||
return self.decode_data(data=self.data, decoder=decoder)
|
return self.decode_data(data=self.data, decoder=decoder, **kwargs)
|
||||||
|
|
||||||
def encode_object(self, object: Base):
|
def encode_object(self, obj: Base):
|
||||||
encoded = object.to_list()
|
encoded = obj.to_list()
|
||||||
encoded.insert(0, len(encoded))
|
encoded.insert(0, len(encoded))
|
||||||
self.data.extend(encoded)
|
self.data.extend(encoded)
|
||||||
|
|
||||||
@@ -128,8 +124,7 @@ class CurveArray(ObjectArray):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _curve_decoder(cls, data: List[float]) -> Base:
|
def _curve_decoder(cls, data: List[float]) -> Base:
|
||||||
crv_array = cls()
|
crv_array = cls(data)
|
||||||
crv_array.data = data
|
|
||||||
return crv_array.to_curve()
|
return crv_array.to_curve()
|
||||||
|
|
||||||
def to_curves(self) -> List[Base]:
|
def to_curves(self) -> List[Base]:
|
||||||
@@ -64,19 +64,21 @@ class Plane(Base, speckle_type=GEOMETRY + "Plane"):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_list(cls, args: List[Any]) -> "Plane":
|
def from_list(cls, args: List[Any]) -> "Plane":
|
||||||
return cls(
|
return cls(
|
||||||
origin=Point.from_list(args[0:3]),
|
origin=Point.from_list(args[:3]),
|
||||||
normal=Vector.from_list(args[3:6]),
|
normal=Vector.from_list(args[3:6]),
|
||||||
xdir=Vector.from_list(args[6:9]),
|
xdir=Vector.from_list(args[6:9]),
|
||||||
ydir=Vector.from_list(args[9:12]),
|
ydir=Vector.from_list(args[9:12]),
|
||||||
|
units=get_units_from_encoding(args[-1]),
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_list(self) -> List[Any]:
|
def to_list(self) -> List[Any]:
|
||||||
encoded = []
|
return [
|
||||||
encoded.extend(self.origin.to_list())
|
*self.origin.to_list(),
|
||||||
encoded.extend(self.normal.to_list())
|
*self.normal.to_list(),
|
||||||
encoded.extend(self.xdir.to_list())
|
*self.xdir.to_list(),
|
||||||
encoded.extend(self.ydir.to_list())
|
*self.ydir.to_list(),
|
||||||
return encoded
|
get_encoding_from_units(self._units),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class Box(Base, speckle_type=GEOMETRY + "Box"):
|
class Box(Base, speckle_type=GEOMETRY + "Box"):
|
||||||
@@ -98,17 +100,21 @@ class Line(Base, speckle_type=GEOMETRY + "Line"):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_list(cls, args: List[Any]) -> "Line":
|
def from_list(cls, args: List[Any]) -> "Line":
|
||||||
return cls(
|
return cls(
|
||||||
start=Point.from_list(args[0:3]),
|
start=Point.from_list(args[1:4]),
|
||||||
end=Point.from_list(args[3:6]),
|
end=Point.from_list(args[4:7]),
|
||||||
domain=Interval.from_list(args[6:9]),
|
domain=Interval.from_list(args[7:10]),
|
||||||
|
units=get_units_from_encoding(args[-1]),
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_list(self) -> List[Any]:
|
def to_list(self) -> List[Any]:
|
||||||
encoded = []
|
domain = self.domain.to_list() if self.domain else [0, 1]
|
||||||
encoded.extend(self.start.to_list())
|
return [
|
||||||
encoded.extend(self.end.to_list())
|
CurveTypeEncoding.Line.value,
|
||||||
encoded.extend(self.domain.to_list())
|
*self.start.to_list(),
|
||||||
return encoded
|
*self.end.to_list(),
|
||||||
|
*domain,
|
||||||
|
get_encoding_from_units(self._units),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class Arc(Base, speckle_type=GEOMETRY + "Arc"):
|
class Arc(Base, speckle_type=GEOMETRY + "Arc"):
|
||||||
@@ -134,20 +140,26 @@ class Arc(Base, speckle_type=GEOMETRY + "Arc"):
|
|||||||
angleRadians=args[4],
|
angleRadians=args[4],
|
||||||
domain=Interval.from_list(args[5:7]),
|
domain=Interval.from_list(args[5:7]),
|
||||||
plane=Plane.from_list(args[7:20]),
|
plane=Plane.from_list(args[7:20]),
|
||||||
|
startPoint=Point.from_list(args[20:23]),
|
||||||
|
midPoint=Point.from_list(args[23:26]),
|
||||||
|
endPoint=Point.from_list(args[26:29]),
|
||||||
units=get_units_from_encoding(args[-1]),
|
units=get_units_from_encoding(args[-1]),
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_list(self) -> List[Any]:
|
def to_list(self) -> List[Any]:
|
||||||
encoded = []
|
return [
|
||||||
encoded.append(CurveTypeEncoding.Arc.value)
|
CurveTypeEncoding.Arc.value,
|
||||||
encoded.append(self.radius)
|
self.radius,
|
||||||
encoded.append(self.startAngle)
|
self.startAngle,
|
||||||
encoded.append(self.endAngle)
|
self.endAngle,
|
||||||
encoded.append(self.angleRadians)
|
self.angleRadians,
|
||||||
encoded.extend(self.domain.to_list())
|
*self.domain.to_list(),
|
||||||
encoded.extend(self.plane.to_list())
|
*self.plane.to_list(),
|
||||||
encoded.append(get_encoding_from_units(self.units))
|
*self.startPoint.to_list(),
|
||||||
return encoded
|
*self.midPoint.to_list(),
|
||||||
|
*self.endPoint.to_list(),
|
||||||
|
get_encoding_from_units(self._units),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class Circle(Base, speckle_type=GEOMETRY + "Circle"):
|
class Circle(Base, speckle_type=GEOMETRY + "Circle"):
|
||||||
@@ -168,13 +180,13 @@ class Circle(Base, speckle_type=GEOMETRY + "Circle"):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def to_list(self) -> List[Any]:
|
def to_list(self) -> List[Any]:
|
||||||
encoded = []
|
return [
|
||||||
encoded.append(CurveTypeEncoding.Circle.value)
|
CurveTypeEncoding.Circle.value,
|
||||||
encoded.append(self.radius),
|
self.radius,
|
||||||
encoded.extend(self.domain.to_list())
|
*self.domain.to_list(),
|
||||||
encoded.extend(self.plane.to_list())
|
*self.plane.to_list(),
|
||||||
encoded.append(get_encoding_from_units(self.units))
|
get_encoding_from_units(self._units),
|
||||||
return encoded
|
]
|
||||||
|
|
||||||
|
|
||||||
class Ellipse(Base, speckle_type=GEOMETRY + "Ellipse"):
|
class Ellipse(Base, speckle_type=GEOMETRY + "Ellipse"):
|
||||||
@@ -198,14 +210,14 @@ class Ellipse(Base, speckle_type=GEOMETRY + "Ellipse"):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def to_list(self) -> List[Any]:
|
def to_list(self) -> List[Any]:
|
||||||
encoded = []
|
return [
|
||||||
encoded.append(CurveTypeEncoding.Ellipse.value)
|
CurveTypeEncoding.Ellipse.value,
|
||||||
encoded.append(self.firstRadius)
|
self.firstRadius,
|
||||||
encoded.append(self.secondRadius)
|
self.secondRadius,
|
||||||
encoded.extend(self.domain.to_list())
|
*self.domain.to_list(),
|
||||||
encoded.extend(self.plane.to_list())
|
*self.plane.to_list(),
|
||||||
encoded.append(get_encoding_from_units(self.units))
|
get_encoding_from_units(self._units),
|
||||||
return encoded
|
]
|
||||||
|
|
||||||
|
|
||||||
class Polyline(Base, speckle_type=GEOMETRY + "Polyline", chunkable={"value": 20000}):
|
class Polyline(Base, speckle_type=GEOMETRY + "Polyline", chunkable={"value": 20000}):
|
||||||
@@ -237,14 +249,14 @@ class Polyline(Base, speckle_type=GEOMETRY + "Polyline", chunkable={"value": 200
|
|||||||
)
|
)
|
||||||
|
|
||||||
def to_list(self) -> List[Any]:
|
def to_list(self) -> List[Any]:
|
||||||
encoded = []
|
return [
|
||||||
encoded.append(CurveTypeEncoding.Polyline.value)
|
CurveTypeEncoding.Polyline.value,
|
||||||
encoded.append(int(self.closed))
|
int(self.closed),
|
||||||
encoded.extend(self.domain.to_list())
|
*self.domain.to_list(),
|
||||||
encoded.append(len(self.value))
|
len(self.value),
|
||||||
encoded.extend(self.value)
|
*self.value,
|
||||||
encoded.append(get_encoding_from_units(self.units))
|
get_encoding_from_units(self._units),
|
||||||
return encoded
|
]
|
||||||
|
|
||||||
def as_points(self) -> List[Point]:
|
def as_points(self) -> List[Point]:
|
||||||
"""Converts the `value` attribute to a list of Points"""
|
"""Converts the `value` attribute to a list of Points"""
|
||||||
@@ -315,21 +327,21 @@ class Curve(
|
|||||||
)
|
)
|
||||||
|
|
||||||
def to_list(self) -> List[Any]:
|
def to_list(self) -> List[Any]:
|
||||||
encoded = []
|
return [
|
||||||
encoded.append(CurveTypeEncoding.Curve.value)
|
CurveTypeEncoding.Curve.value,
|
||||||
encoded.append(self.degree)
|
self.degree,
|
||||||
encoded.append(int(self.periodic))
|
int(self.periodic),
|
||||||
encoded.append(int(self.rational))
|
int(self.rational),
|
||||||
encoded.append(int(self.closed))
|
int(self.closed),
|
||||||
encoded.extend(self.domain.to_list())
|
*self.domain.to_list(),
|
||||||
encoded.append(len(self.points))
|
len(self.points),
|
||||||
encoded.append(len(self.weights))
|
len(self.weights),
|
||||||
encoded.append(len(self.knots))
|
len(self.knots),
|
||||||
encoded.extend(self.points)
|
*self.points,
|
||||||
encoded.extend(self.weights)
|
*self.weights,
|
||||||
encoded.extend(self.knots)
|
*self.knots,
|
||||||
encoded.append(get_encoding_from_units(self.units))
|
get_encoding_from_units(self._units),
|
||||||
return encoded
|
]
|
||||||
|
|
||||||
|
|
||||||
class Polycurve(Base, speckle_type=GEOMETRY + "Polycurve"):
|
class Polycurve(Base, speckle_type=GEOMETRY + "Polycurve"):
|
||||||
@@ -342,8 +354,7 @@ class Polycurve(Base, speckle_type=GEOMETRY + "Polycurve"):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_list(cls, args: List[Any]) -> "Polycurve":
|
def from_list(cls, args: List[Any]) -> "Polycurve":
|
||||||
curve_arrays = CurveArray()
|
curve_arrays = CurveArray(args[5:-1])
|
||||||
curve_arrays.data = args[4:-1]
|
|
||||||
return cls(
|
return cls(
|
||||||
closed=bool(args[1]),
|
closed=bool(args[1]),
|
||||||
domain=Interval.from_list(args[2:4]),
|
domain=Interval.from_list(args[2:4]),
|
||||||
@@ -352,14 +363,15 @@ class Polycurve(Base, speckle_type=GEOMETRY + "Polycurve"):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def to_list(self) -> List[Any]:
|
def to_list(self) -> List[Any]:
|
||||||
encoded = []
|
curve_array = CurveArray.from_curves(self.segments).data
|
||||||
encoded.append(CurveTypeEncoding.Polycurve.value)
|
return [
|
||||||
encoded.append(int(self.closed))
|
CurveTypeEncoding.Polycurve.value,
|
||||||
encoded.extend(self.domain.to_list())
|
int(self.closed),
|
||||||
curve_array = CurveArray.from_curves(self.segments)
|
*self.domain.to_list(),
|
||||||
encoded.extend(curve_array.data)
|
len(curve_array),
|
||||||
encoded.append(get_encoding_from_units(self.units))
|
*curve_array,
|
||||||
return encoded
|
get_encoding_from_units(self._units),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class Extrusion(Base, speckle_type=GEOMETRY + "Extrusion"):
|
class Extrusion(Base, speckle_type=GEOMETRY + "Extrusion"):
|
||||||
@@ -460,46 +472,65 @@ class Surface(Base, speckle_type=GEOMETRY + "Surface"):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def to_list(self) -> List[Any]:
|
def to_list(self) -> List[Any]:
|
||||||
encoded = []
|
return [
|
||||||
encoded.append(self.degreeU)
|
self.degreeU,
|
||||||
encoded.append(self.degreeV)
|
self.degreeV,
|
||||||
encoded.append(self.countU)
|
self.countU,
|
||||||
encoded.append(self.countV)
|
self.countV,
|
||||||
encoded.append(int(self.rational))
|
int(self.rational),
|
||||||
encoded.append(int(self.closedU))
|
int(self.closedU),
|
||||||
encoded.append(int(self.closedV))
|
int(self.closedV),
|
||||||
encoded.extend(self.domainU.to_list())
|
*self.domainU.to_list(),
|
||||||
encoded.extend(self.domainV.to_list())
|
*self.domainV.to_list(),
|
||||||
encoded.append(len(self.pointData))
|
len(self.pointData),
|
||||||
encoded.append(len(self.knotsU))
|
len(self.knotsU),
|
||||||
encoded.append(len(self.knotsV))
|
len(self.knotsV),
|
||||||
encoded.extend(self.pointData)
|
*self.pointData,
|
||||||
encoded.extend(self.knotsU)
|
*self.knotsU,
|
||||||
encoded.extend(self.knotsV)
|
*self.knotsV,
|
||||||
encoded.append(get_encoding_from_units(self.units))
|
get_encoding_from_units(self._units),
|
||||||
return encoded
|
]
|
||||||
|
|
||||||
|
|
||||||
class BrepFace(Base, speckle_type=GEOMETRY + "BrepFace"):
|
class BrepFace(Base, speckle_type=GEOMETRY + "BrepFace"):
|
||||||
_Brep: "Brep" = None
|
_Brep: "Brep" = None
|
||||||
SurfaceIndex: int = None
|
SurfaceIndex: int = None
|
||||||
LoopIndices: List[int] = None
|
|
||||||
OuterLoopIndex: int = None
|
OuterLoopIndex: int = None
|
||||||
OrientationReversed: bool = None
|
OrientationReversed: bool = None
|
||||||
|
LoopIndices: List[int] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _outer_loop(self):
|
def _outer_loop(self):
|
||||||
return self._Brep.Loops[self.OuterLoopIndex]
|
return self._Brep.Loops[self.OuterLoopIndex] # pylint: disable=no-member
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _surface(self):
|
def _surface(self):
|
||||||
return self._Brep.Surfaces[self.SurfaceIndex]
|
return self._Brep.Surfaces[self.SurfaceIndex] # pylint: disable=no-member
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _loops(self):
|
def _loops(self):
|
||||||
if self.LoopIndices:
|
if self.LoopIndices:
|
||||||
|
# pylint: disable=not-an-iterable, no-member
|
||||||
return [self._Brep.Loops[i] for i in self.LoopIndices]
|
return [self._Brep.Loops[i] for i in self.LoopIndices]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_list(cls, args: List[Any], brep: "Brep" = None) -> "BrepFace":
|
||||||
|
return cls(
|
||||||
|
_Brep=brep,
|
||||||
|
SurfaceIndex=args[0],
|
||||||
|
OuterLoopIndex=args[1],
|
||||||
|
OrientationReversed=bool(args[2]),
|
||||||
|
LoopIndices=args[3:],
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_list(self) -> List[Any]:
|
||||||
|
return [
|
||||||
|
self.SurfaceIndex,
|
||||||
|
self.OuterLoopIndex,
|
||||||
|
int(self.OrientationReversed),
|
||||||
|
*self.LoopIndices,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class BrepEdge(Base, speckle_type=GEOMETRY + "BrepEdge"):
|
class BrepEdge(Base, speckle_type=GEOMETRY + "BrepEdge"):
|
||||||
_Brep: "Brep" = None
|
_Brep: "Brep" = None
|
||||||
@@ -521,18 +552,58 @@ class BrepEdge(Base, speckle_type=GEOMETRY + "BrepEdge"):
|
|||||||
@property
|
@property
|
||||||
def _trims(self):
|
def _trims(self):
|
||||||
if self.TrimIndices:
|
if self.TrimIndices:
|
||||||
|
# pylint: disable=not-an-iterable
|
||||||
return [self._Brep.Trims[i] for i in self.TrimIndices]
|
return [self._Brep.Trims[i] for i in self.TrimIndices]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _curve(self):
|
def _curve(self):
|
||||||
return self._Brep.Curve3D[self.Curve3dIndex]
|
return self._Brep.Curve3D[self.Curve3dIndex]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_list(cls, args: List[Any], brep: "Brep" = None) -> "BrepEdge":
|
||||||
|
domain_start = args[4]
|
||||||
|
domain_end = args[5]
|
||||||
|
domain = (
|
||||||
|
Interval(start=domain_start, end=domain_end)
|
||||||
|
if None not in (domain_start, domain_end)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
return cls(
|
||||||
|
_Brep=brep,
|
||||||
|
Curve3dIndex=int(args[0]),
|
||||||
|
TrimIndices=[int(t) for t in args[6:]],
|
||||||
|
StartIndex=int(args[1]),
|
||||||
|
EndIndex=int(args[2]),
|
||||||
|
ProxyCurveIsReversed=bool(args[3]),
|
||||||
|
Domain=domain,
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_list(self) -> List[Any]:
|
||||||
|
return [
|
||||||
|
self.Curve3dIndex,
|
||||||
|
self.StartIndex,
|
||||||
|
self.EndIndex,
|
||||||
|
int(self.ProxyCurveIsReversed),
|
||||||
|
self.Domain.start,
|
||||||
|
self.Domain.end,
|
||||||
|
*self.TrimIndices,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class BrepLoopType(int, Enum):
|
||||||
|
Unknown = 0
|
||||||
|
Outer = 1
|
||||||
|
Inner = 2
|
||||||
|
Slit = 3
|
||||||
|
CurveOnSurface = 4
|
||||||
|
PointOnSurface = 5
|
||||||
|
|
||||||
|
|
||||||
class BrepLoop(Base, speckle_type=GEOMETRY + "BrepLoop"):
|
class BrepLoop(Base, speckle_type=GEOMETRY + "BrepLoop"):
|
||||||
_Brep: "Brep" = None
|
_Brep: "Brep" = None
|
||||||
FaceIndex: int = None
|
FaceIndex: int = None
|
||||||
TrimIndices: List[int] = None
|
TrimIndices: List[int] = None
|
||||||
Type: str = None
|
Type: BrepLoopType = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _face(self):
|
def _face(self):
|
||||||
@@ -541,10 +612,27 @@ class BrepLoop(Base, speckle_type=GEOMETRY + "BrepLoop"):
|
|||||||
@property
|
@property
|
||||||
def _trims(self):
|
def _trims(self):
|
||||||
if self.TrimIndices:
|
if self.TrimIndices:
|
||||||
|
# pylint: disable=not-an-iterable
|
||||||
return [self._Brep.Trims[i] for i in self.TrimIndices]
|
return [self._Brep.Trims[i] for i in self.TrimIndices]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_list(cls, args: List[any], brep: "Brep" = None):
|
||||||
|
return cls(
|
||||||
|
_Brep=brep,
|
||||||
|
FaceIndex=args[0],
|
||||||
|
Type=BrepLoopType(args[1]),
|
||||||
|
TrimIndices=args[2:],
|
||||||
|
)
|
||||||
|
|
||||||
class BrepTrimTypeEnum(int, Enum):
|
def to_list(self) -> List[int]:
|
||||||
|
return [
|
||||||
|
self.FaceIndex,
|
||||||
|
self.Type.value,
|
||||||
|
*self.TrimIndices,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class BrepTrimType(int, Enum):
|
||||||
Unknown = 0
|
Unknown = 0
|
||||||
Boundary = 1
|
Boundary = 1
|
||||||
Mated = 2
|
Mated = 2
|
||||||
@@ -564,29 +652,35 @@ class BrepTrim(Base, speckle_type=GEOMETRY + "BrepTrim"):
|
|||||||
LoopIndex: int = None
|
LoopIndex: int = None
|
||||||
CurveIndex: int = None
|
CurveIndex: int = None
|
||||||
IsoStatus: int = None
|
IsoStatus: int = None
|
||||||
TrimType: str = None
|
TrimType: BrepTrimType = None
|
||||||
IsReversed: bool = None
|
IsReversed: bool = None
|
||||||
Domain: Interval = None
|
Domain: Interval = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _face(self):
|
def _face(self):
|
||||||
return self._Brep.Faces[self.FaceIndex]
|
if self._Brep:
|
||||||
|
return self._Brep.Faces[self.FaceIndex] # pylint: disable=no-member
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _loop(self):
|
def _loop(self):
|
||||||
return self._Brep.Loops[self.LoopIndex]
|
if self._Brep:
|
||||||
|
return self._Brep.Loops[self.LoopIndex] # pylint: disable=no-member
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _edge(self):
|
def _edge(self):
|
||||||
return self._Brep.Edges[self.EdgeIndex] if self.EdgeIndex != -1 else None
|
if self._Brep:
|
||||||
|
# pylint: disable=no-member
|
||||||
|
return self._Brep.Edges[self.EdgeIndex] if self.EdgeIndex != -1 else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _curve_2d(self):
|
def _curve_2d(self):
|
||||||
return self._Brep.Curve2D[self.CurveIndex]
|
if self._Brep:
|
||||||
|
return self._Brep.Curve2D[self.CurveIndex] # pylint: disable=no-member
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_list(cls, args: List[Any]) -> "BrepTrim":
|
def from_list(cls, args: List[Any], brep: "Brep" = None) -> "BrepTrim":
|
||||||
return cls(
|
return cls(
|
||||||
|
_Brep=brep,
|
||||||
EdgeIndex=args[0],
|
EdgeIndex=args[0],
|
||||||
StartIndex=args[1],
|
StartIndex=args[1],
|
||||||
EndIndex=args[2],
|
EndIndex=args[2],
|
||||||
@@ -594,39 +688,48 @@ class BrepTrim(Base, speckle_type=GEOMETRY + "BrepTrim"):
|
|||||||
LoopIndex=args[4],
|
LoopIndex=args[4],
|
||||||
CurveIndex=args[5],
|
CurveIndex=args[5],
|
||||||
IsoStatus=args[6],
|
IsoStatus=args[6],
|
||||||
TrimType=BrepTrimTypeEnum(args[7]).name,
|
TrimType=BrepTrimType(args[7]),
|
||||||
IsReversed=bool(args[8]),
|
IsReversed=bool(args[8]),
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_list(self) -> List[Any]:
|
def to_list(self) -> List[Any]:
|
||||||
encoded = []
|
return [
|
||||||
encoded.append(self.EdgeIndex)
|
self.EdgeIndex,
|
||||||
encoded.append(self.StartIndex)
|
self.StartIndex,
|
||||||
encoded.append(self.EndIndex)
|
self.EndIndex,
|
||||||
encoded.append(self.FaceIndex)
|
self.FaceIndex,
|
||||||
encoded.append(self.LoopIndex)
|
self.LoopIndex,
|
||||||
encoded.append(self.CurveIndex)
|
self.CurveIndex,
|
||||||
encoded.append(self.IsoStatus)
|
self.IsoStatus,
|
||||||
encoded.append(getattr(BrepTrimTypeEnum, self.TrimType).value)
|
self.TrimType.value,
|
||||||
encoded.append(self.IsReversed)
|
int(self.IsReversed),
|
||||||
return encoded
|
]
|
||||||
|
|
||||||
|
|
||||||
class Brep(
|
class Brep(
|
||||||
Base,
|
Base,
|
||||||
speckle_type=GEOMETRY + "Brep",
|
speckle_type=GEOMETRY + "Brep",
|
||||||
chunkable={
|
chunkable={
|
||||||
"SurfacesValue": 200,
|
"SurfacesValue": 31250,
|
||||||
"Curve3DValues": 200,
|
"Curve3DValues": 31250,
|
||||||
"Curve2DValues": 200,
|
"Curve2DValues": 31250,
|
||||||
"VerticesValue": 5000,
|
"VerticesValue": 31250,
|
||||||
"Edges": 5000,
|
"EdgesValue": 62500,
|
||||||
"Loops": 5000,
|
"LoopsValue": 62500,
|
||||||
"TrimsValue": 5000,
|
"FacesValue": 62500,
|
||||||
"Faces": 5000,
|
"TrimsValue": 62500,
|
||||||
},
|
},
|
||||||
detachable={"displayValue"},
|
detachable={"displayValue"},
|
||||||
serialize_ignore={"Surfaces", "Curve3D", "Curve2D", "Vertices", "Trims"},
|
serialize_ignore={
|
||||||
|
"Surfaces",
|
||||||
|
"Curve3D",
|
||||||
|
"Curve2D",
|
||||||
|
"Vertices",
|
||||||
|
"Trims",
|
||||||
|
"Edges",
|
||||||
|
"Loops",
|
||||||
|
"Faces",
|
||||||
|
},
|
||||||
):
|
):
|
||||||
provenance: str = None
|
provenance: str = None
|
||||||
bbox: Box = None
|
bbox: Box = None
|
||||||
@@ -637,6 +740,10 @@ class Brep(
|
|||||||
Curve3D: List[Base] = None
|
Curve3D: List[Base] = None
|
||||||
Curve2D: List[Base] = None
|
Curve2D: List[Base] = None
|
||||||
Vertices: List[Point] = None
|
Vertices: List[Point] = None
|
||||||
|
Edges: List[BrepEdge] = None
|
||||||
|
Loops: List[BrepLoop] = None
|
||||||
|
Faces: List[BrepFace] = None
|
||||||
|
Trims: List[BrepTrim] = None
|
||||||
IsClosed: bool = None
|
IsClosed: bool = None
|
||||||
Orientation: int = None
|
Orientation: int = None
|
||||||
|
|
||||||
@@ -645,7 +752,7 @@ class Brep(
|
|||||||
return children
|
return children
|
||||||
|
|
||||||
for child in children:
|
for child in children:
|
||||||
child._Brep = self
|
child._Brep = self # pylint: disable=protected-access
|
||||||
return children
|
return children
|
||||||
|
|
||||||
# set as prop for now for backwards compatibility
|
# set as prop for now for backwards compatibility
|
||||||
@@ -661,68 +768,80 @@ class Brep(
|
|||||||
self._displayValue = value
|
self._displayValue = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def Edges(self) -> List[BrepEdge]:
|
def EdgesValue(self) -> List[BrepEdge]:
|
||||||
return self._inject_self_into_children(self._Edges)
|
return None if self.Edges is None else ObjectArray.from_objects(self.Edges).data
|
||||||
|
|
||||||
@Edges.setter
|
@EdgesValue.setter
|
||||||
def Edges(self, value: List[BrepEdge]):
|
def EdgesValue(self, value: List[float]):
|
||||||
self._Edges = value
|
if not value:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.Edges = ObjectArray.decode_data(value, BrepEdge.from_list, brep=self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def Loops(self) -> List[BrepLoop]:
|
def LoopsValue(self) -> List[BrepLoop]:
|
||||||
return self._inject_self_into_children(self._Loops)
|
return None if self.Loops is None else ObjectArray.from_objects(self.Loops).data
|
||||||
|
|
||||||
@Loops.setter
|
@LoopsValue.setter
|
||||||
def Loops(self, value: List[BrepLoop]):
|
def LoopsValue(self, value: List[int]):
|
||||||
self._Loops = value
|
if not value:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.Loops = ObjectArray.decode_data(value, BrepLoop.from_list, brep=self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def Faces(self) -> List[BrepFace]:
|
def FacesValue(self) -> List[int]:
|
||||||
return self._inject_self_into_children(self._Faces)
|
return None if self.Faces is None else ObjectArray.from_objects(self.Faces).data
|
||||||
|
|
||||||
@Faces.setter
|
@FacesValue.setter
|
||||||
def Faces(self, value: List[BrepFace]):
|
def FacesValue(self, value: List[int]):
|
||||||
self._Faces = value
|
if not value:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.Faces = ObjectArray.decode_data(value, BrepFace.from_list, brep=self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def SurfacesValue(self) -> List[float]:
|
def SurfacesValue(self) -> List[float]:
|
||||||
if self.Surfaces is None:
|
return (
|
||||||
return None
|
None
|
||||||
return ObjectArray.from_objects(self.Surfaces).data
|
if self.Surfaces is None
|
||||||
|
else ObjectArray.from_objects(self.Surfaces).data
|
||||||
|
)
|
||||||
|
|
||||||
@SurfacesValue.setter
|
@SurfacesValue.setter
|
||||||
def SurfacesValue(self, value: List[float]):
|
def SurfacesValue(self, value: List[float]):
|
||||||
|
if not value:
|
||||||
|
return
|
||||||
|
|
||||||
self.Surfaces = ObjectArray.decode_data(value, Surface.from_list)
|
self.Surfaces = ObjectArray.decode_data(value, Surface.from_list)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def Curve3DValues(self) -> List[float]:
|
def Curve3DValues(self) -> List[float]:
|
||||||
if self.Curve3D is None:
|
return (
|
||||||
return None
|
None if self.Curve3D is None else CurveArray.from_curves(self.Curve3D).data
|
||||||
return CurveArray.from_curves(self.Curve3D).data
|
)
|
||||||
|
|
||||||
@Curve3DValues.setter
|
@Curve3DValues.setter
|
||||||
def Curve3DValues(self, value: List[float]):
|
def Curve3DValues(self, value: List[float]):
|
||||||
crv_array = CurveArray()
|
crv_array = CurveArray(value)
|
||||||
crv_array.data = value
|
|
||||||
self.Curve3D = crv_array.to_curves()
|
self.Curve3D = crv_array.to_curves()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def Curve2DValues(self) -> List[Base]:
|
def Curve2DValues(self) -> List[Base]:
|
||||||
if self.Curve2D is None:
|
return (
|
||||||
return None
|
None if self.Curve2D is None else CurveArray.from_curves(self.Curve2D).data
|
||||||
return CurveArray.from_curves(self.Curve2D).data
|
)
|
||||||
|
|
||||||
@Curve2DValues.setter
|
@Curve2DValues.setter
|
||||||
def Curve2DValues(self, value: List[float]):
|
def Curve2DValues(self, value: List[float]):
|
||||||
crv_array = CurveArray()
|
crv_array = CurveArray(value)
|
||||||
crv_array.data = value
|
|
||||||
self.Curve2D = crv_array.to_curves()
|
self.Curve2D = crv_array.to_curves()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def VerticesValue(self) -> List[Point]:
|
def VerticesValue(self) -> List[Point]:
|
||||||
if self.Vertices is None:
|
if self.Vertices is None:
|
||||||
return None
|
return None
|
||||||
encoded_unit = get_encoding_from_units(self.Vertices[0].units)
|
encoded_unit = get_encoding_from_units(self.Vertices[0]._units)
|
||||||
values = [encoded_unit]
|
values = [encoded_unit]
|
||||||
for vertex in self.Vertices:
|
for vertex in self.Vertices:
|
||||||
values.extend(vertex.to_list())
|
values.extend(vertex.to_list())
|
||||||
@@ -742,27 +861,25 @@ class Brep(
|
|||||||
|
|
||||||
self.Vertices = vertices
|
self.Vertices = vertices
|
||||||
|
|
||||||
@property
|
# TODO: can this be consistent with loops, edges, faces, curves, etc and prepend with the chunk list? needs to happen in sharp first
|
||||||
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
|
@property
|
||||||
def TrimsValue(self) -> List[float]:
|
def TrimsValue(self) -> List[float]:
|
||||||
if self.Trims is None:
|
# return None if self.Trims is None else ObjectArray.from_objects(self.Trims).data
|
||||||
return None
|
if not self.Trims:
|
||||||
values = []
|
return
|
||||||
|
value = []
|
||||||
for trim in self.Trims:
|
for trim in self.Trims:
|
||||||
values.extend(trim.to_list())
|
value.extend(trim.to_list())
|
||||||
return values
|
return value
|
||||||
|
|
||||||
@TrimsValue.setter
|
@TrimsValue.setter
|
||||||
def TrimsValue(self, value: List[float]):
|
def TrimsValue(self, value: List[float]):
|
||||||
|
if not value:
|
||||||
|
return
|
||||||
|
|
||||||
|
# self.Trims = ObjectArray.decode_data(value, BrepTrim.from_list, brep=self)
|
||||||
self.Trims = [
|
self.Trims = [
|
||||||
BrepTrim.from_list(value[i : i + 9]) for i in range(0, len(value), 9)
|
BrepTrim.from_list(value[i : i + 9], self) for i in range(0, len(value), 9)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -5,4 +5,4 @@ from ..geometry import Plane
|
|||||||
class Axis(Base, speckle_type="Objects.Structural.Geometry.Axis"):
|
class Axis(Base, speckle_type="Objects.Structural.Geometry.Axis"):
|
||||||
name: str = None
|
name: str = None
|
||||||
axisType: str = None
|
axisType: str = None
|
||||||
plane: Plane = None
|
plane: Plane = None
|
||||||
+2
-2
@@ -42,8 +42,8 @@ class Concrete(Material, speckle_type=STRUCTURAL_MATERIALS + ".Concrete"):
|
|||||||
compressiveStrength: float = 0.0
|
compressiveStrength: float = 0.0
|
||||||
tensileStrength: float = 0.0
|
tensileStrength: float = 0.0
|
||||||
flexuralStrength: float = 0.0
|
flexuralStrength: float = 0.0
|
||||||
maxCompressiveStrength: float = 0.0
|
maxCompressiveStrain: float = 0.0
|
||||||
maxTensileStrength: float = 0.0
|
maxTensileStrain: float = 0.0
|
||||||
maxAggregateSize: float = 0.0
|
maxAggregateSize: float = 0.0
|
||||||
lightweight: bool = None
|
lightweight: bool = None
|
||||||
|
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
from typing import Union
|
||||||
|
from specklepy.logging.exceptions import SpeckleException, SpeckleInvalidUnitException
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class Units(Enum):
|
||||||
|
mm = "mm"
|
||||||
|
cm = "cm"
|
||||||
|
m = "m"
|
||||||
|
km = "km"
|
||||||
|
inches = "in"
|
||||||
|
feet = "ft"
|
||||||
|
yards = "yd"
|
||||||
|
miles = "mi"
|
||||||
|
none = "none"
|
||||||
|
|
||||||
|
|
||||||
|
UNITS_STRINGS = {
|
||||||
|
Units.mm: ["mm", "mil", "millimeters", "millimetres"],
|
||||||
|
Units.cm: ["cm", "centimetre", "centimeter", "centimetres", "centimeters"],
|
||||||
|
Units.m: ["m", "meter", "meters", "metre", "metres"],
|
||||||
|
Units.km: ["km", "kilometer", "kilometre", "kilometers", "kilometres"],
|
||||||
|
Units.inches: ["in", "inch", "inches"],
|
||||||
|
Units.feet: ["ft", "foot", "feet"],
|
||||||
|
Units.yards: ["yd", "yard", "yards"],
|
||||||
|
Units.miles: ["mi", "mile", "miles"],
|
||||||
|
Units.none: ["none", "null"],
|
||||||
|
}
|
||||||
|
|
||||||
|
UNITS_ENCODINGS = {
|
||||||
|
Units.none: 0,
|
||||||
|
None: 0,
|
||||||
|
Units.mm: 1,
|
||||||
|
Units.cm: 2,
|
||||||
|
Units.m: 3,
|
||||||
|
Units.km: 4,
|
||||||
|
Units.inches: 5,
|
||||||
|
Units.feet: 6,
|
||||||
|
Units.yards: 7,
|
||||||
|
Units.miles: 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_units_from_string(unit: str) -> Units:
|
||||||
|
if not isinstance(unit, str):
|
||||||
|
raise SpeckleInvalidUnitException(unit)
|
||||||
|
unit = str.lower(unit)
|
||||||
|
for name, alternates in UNITS_STRINGS.items():
|
||||||
|
if unit in alternates:
|
||||||
|
return name
|
||||||
|
raise SpeckleInvalidUnitException(unit)
|
||||||
|
|
||||||
|
|
||||||
|
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: Union[Units, None]):
|
||||||
|
try:
|
||||||
|
return UNITS_ENCODINGS[unit]
|
||||||
|
except KeyError as e:
|
||||||
|
raise SpeckleException(
|
||||||
|
message=f"No encoding exists for unit {unit}. Please enter a valid unit to encode (eg {UNITS_ENCODINGS})."
|
||||||
|
) from e
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from appdirs import user_data_dir
|
||||||
|
|
||||||
|
|
||||||
|
def base_path(app_name) -> Path:
|
||||||
|
# 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
|
||||||
|
if system.startswith("java"):
|
||||||
|
import platform
|
||||||
|
|
||||||
|
os_name = platform.java_ver()[3][0]
|
||||||
|
if os_name.startswith("Mac"):
|
||||||
|
system = "darwin"
|
||||||
|
|
||||||
|
if system == "darwin":
|
||||||
|
return Path(Path.home(), ".config", app_name)
|
||||||
|
|
||||||
|
return Path(user_data_dir(appname=app_name, appauthor=False, roaming=True))
|
||||||
|
|
||||||
|
|
||||||
|
def accounts_path(app_name: str = "Speckle") -> Path:
|
||||||
|
"""
|
||||||
|
Gets the path where the Speckle applications are looking for accounts.
|
||||||
|
"""
|
||||||
|
return base_path(app_name).joinpath("Accounts")
|
||||||
+91
-51
@@ -1,17 +1,19 @@
|
|||||||
|
import re
|
||||||
import ujson
|
import ujson
|
||||||
import hashlib
|
import hashlib
|
||||||
import re
|
import warnings
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
from typing import Any, Dict, List, Tuple
|
from typing import Any, Dict, List, Tuple
|
||||||
from specklepy.objects.base import Base, DataChunk
|
from specklepy.objects.base import Base, DataChunk
|
||||||
from specklepy.logging.exceptions import (
|
from specklepy.logging.exceptions import (
|
||||||
SerializationException,
|
|
||||||
SpeckleException,
|
SpeckleException,
|
||||||
SpeckleWarning,
|
SpeckleWarning,
|
||||||
)
|
)
|
||||||
from specklepy.transports.abstract_transport import AbstractTransport
|
from specklepy.transports.abstract_transport import AbstractTransport
|
||||||
|
|
||||||
|
# import for serialization
|
||||||
import specklepy.objects.geometry
|
import specklepy.objects.geometry
|
||||||
import specklepy.objects.other
|
import specklepy.objects.other
|
||||||
|
|
||||||
@@ -38,22 +40,37 @@ def safe_json_loads(obj: str, obj_id=None) -> Any:
|
|||||||
class BaseObjectSerializer:
|
class BaseObjectSerializer:
|
||||||
read_transport: AbstractTransport
|
read_transport: AbstractTransport
|
||||||
write_transports: List[AbstractTransport]
|
write_transports: List[AbstractTransport]
|
||||||
detach_lineage: List[bool] = [] # tracks depth and whether or not to detach
|
detach_lineage: List[bool] # tracks depth and whether or not to detach
|
||||||
lineage: List[str] = [] # keeps track of hash chain through the object tree
|
lineage: List[str] # keeps track of hash chain through the object tree
|
||||||
family_tree: Dict[str, Dict[str, int]] = {}
|
family_tree: Dict[str, Dict[str, int]]
|
||||||
closure_table: Dict[str, Dict[str, int]] = {}
|
closure_table: Dict[str, Dict[str, int]]
|
||||||
|
deserialized: Dict[
|
||||||
|
str, Base
|
||||||
|
] # holds deserialized objects so objects with same id return the same instance
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, write_transports: List[AbstractTransport] = [], read_transport=None
|
self, write_transports: List[AbstractTransport] = None, read_transport=None
|
||||||
) -> None:
|
) -> None:
|
||||||
self.write_transports = write_transports
|
self.write_transports = write_transports or []
|
||||||
self.read_transport = read_transport
|
self.read_transport = read_transport
|
||||||
|
self.detach_lineage = []
|
||||||
|
self.lineage = []
|
||||||
|
self.family_tree = {}
|
||||||
|
self.closure_table = {}
|
||||||
|
self.deserialized = {}
|
||||||
|
|
||||||
def write_json(self, base: Base):
|
def write_json(self, base: Base):
|
||||||
self.__reset_writer()
|
"""Serializes a given base object into a json string
|
||||||
self.detach_lineage = [True]
|
Arguments:
|
||||||
hash, obj = self.traverse_base(base)
|
base {Base} -- the base object to be decomposed and serialized
|
||||||
return hash, ujson.dumps(obj)
|
|
||||||
|
Returns:
|
||||||
|
(str, str) -- a tuple containing the object id of the base object and the serialized object string
|
||||||
|
"""
|
||||||
|
|
||||||
|
obj_id, obj = self.traverse_base(base)
|
||||||
|
|
||||||
|
return obj_id, ujson.dumps(obj)
|
||||||
|
|
||||||
def traverse_base(self, base: Base) -> Tuple[str, Dict]:
|
def traverse_base(self, base: Base) -> Tuple[str, Dict]:
|
||||||
"""Decomposes the given base object and builds a serializable dictionary
|
"""Decomposes the given base object and builds a serializable dictionary
|
||||||
@@ -62,8 +79,23 @@ class BaseObjectSerializer:
|
|||||||
base {Base} -- the base object to be decomposed and serialized
|
base {Base} -- the base object to be decomposed and serialized
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(str, dict) -- a tuple containing the hash (id) of the base object and the constructed serializable dictionary
|
(str, dict) -- a tuple containing the object id of the base object and the constructed serializable dictionary
|
||||||
"""
|
"""
|
||||||
|
self.__reset_writer()
|
||||||
|
|
||||||
|
if self.write_transports:
|
||||||
|
for wt in self.write_transports:
|
||||||
|
wt.begin_write()
|
||||||
|
|
||||||
|
obj_id, obj = self._traverse_base(base)
|
||||||
|
|
||||||
|
if self.write_transports:
|
||||||
|
for wt in self.write_transports:
|
||||||
|
wt.end_write()
|
||||||
|
|
||||||
|
return obj_id, obj
|
||||||
|
|
||||||
|
def _traverse_base(self, base: Base) -> Tuple[str, Dict]:
|
||||||
if not self.detach_lineage:
|
if not self.detach_lineage:
|
||||||
self.detach_lineage = [True]
|
self.detach_lineage = [True]
|
||||||
|
|
||||||
@@ -86,11 +118,6 @@ class BaseObjectSerializer:
|
|||||||
if prop == "id":
|
if prop == "id":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# allow serialisation of nulls
|
|
||||||
if value is None:
|
|
||||||
object_builder[prop] = value
|
|
||||||
continue
|
|
||||||
|
|
||||||
# only bother with chunking and detaching if there is a write transport
|
# only bother with chunking and detaching if there is a write transport
|
||||||
if self.write_transports:
|
if self.write_transports:
|
||||||
dynamic_chunk_match = prop.startswith("@") and re.match(
|
dynamic_chunk_match = prop.startswith("@") and re.match(
|
||||||
@@ -107,8 +134,8 @@ class BaseObjectSerializer:
|
|||||||
prop.startswith("@") or prop in base._detachable or chunkable
|
prop.startswith("@") or prop in base._detachable or chunkable
|
||||||
)
|
)
|
||||||
|
|
||||||
# 1. handle primitives (ints, floats, strings, and bools)
|
# 1. handle None and primitives (ints, floats, strings, and bools)
|
||||||
if isinstance(value, PRIMITIVES):
|
if value is None or isinstance(value, PRIMITIVES):
|
||||||
object_builder[prop] = value
|
object_builder[prop] = value
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -121,8 +148,8 @@ class BaseObjectSerializer:
|
|||||||
elif isinstance(value, Base):
|
elif isinstance(value, Base):
|
||||||
child_obj = self.traverse_value(value, detach=detach)
|
child_obj = self.traverse_value(value, detach=detach)
|
||||||
if detach and self.write_transports:
|
if detach and self.write_transports:
|
||||||
ref_hash = child_obj["id"]
|
ref_id = child_obj["id"]
|
||||||
object_builder[prop] = self.detach_helper(ref_hash=ref_hash)
|
object_builder[prop] = self.detach_helper(ref_id=ref_id)
|
||||||
else:
|
else:
|
||||||
object_builder[prop] = child_obj
|
object_builder[prop] = child_obj
|
||||||
|
|
||||||
@@ -141,8 +168,8 @@ class BaseObjectSerializer:
|
|||||||
chunk_refs = []
|
chunk_refs = []
|
||||||
for c in chunks:
|
for c in chunks:
|
||||||
self.detach_lineage.append(detach)
|
self.detach_lineage.append(detach)
|
||||||
ref_hash, _ = self.traverse_base(c)
|
ref_id, _ = self._traverse_base(c)
|
||||||
ref_obj = self.detach_helper(ref_hash=ref_hash)
|
ref_obj = self.detach_helper(ref_id=ref_id)
|
||||||
chunk_refs.append(ref_obj)
|
chunk_refs.append(ref_obj)
|
||||||
object_builder[prop] = chunk_refs
|
object_builder[prop] = chunk_refs
|
||||||
|
|
||||||
@@ -161,20 +188,20 @@ class BaseObjectSerializer:
|
|||||||
}
|
}
|
||||||
object_builder["totalChildrenCount"] = len(closure)
|
object_builder["totalChildrenCount"] = len(closure)
|
||||||
|
|
||||||
hash = hash_obj(object_builder)
|
obj_id = hash_obj(object_builder)
|
||||||
|
|
||||||
object_builder["id"] = hash
|
object_builder["id"] = obj_id
|
||||||
if closure:
|
if closure:
|
||||||
object_builder["__closure"] = self.closure_table[hash] = closure
|
object_builder["__closure"] = self.closure_table[obj_id] = closure
|
||||||
|
|
||||||
# write detached or root objects to transports
|
# write detached or root objects to transports
|
||||||
if detached and self.write_transports:
|
if detached and self.write_transports:
|
||||||
for t in self.write_transports:
|
for t in self.write_transports:
|
||||||
t.save_object(id=hash, serialized_object=ujson.dumps(object_builder))
|
t.save_object(id=obj_id, serialized_object=ujson.dumps(object_builder))
|
||||||
|
|
||||||
del self.lineage[-1]
|
del self.lineage[-1]
|
||||||
|
|
||||||
return hash, object_builder
|
return obj_id, object_builder
|
||||||
|
|
||||||
def traverse_value(self, obj: Any, detach: bool = False) -> Any:
|
def traverse_value(self, obj: Any, detach: bool = False) -> Any:
|
||||||
"""Decomposes a given object and constructs a serializable object or dictionary
|
"""Decomposes a given object and constructs a serializable object or dictionary
|
||||||
@@ -200,8 +227,8 @@ class BaseObjectSerializer:
|
|||||||
for o in obj:
|
for o in obj:
|
||||||
if isinstance(o, Base):
|
if isinstance(o, Base):
|
||||||
self.detach_lineage.append(detach)
|
self.detach_lineage.append(detach)
|
||||||
hash, _ = self.traverse_base(o)
|
ref_id, _ = self._traverse_base(o)
|
||||||
detached_list.append(self.detach_helper(ref_hash=hash))
|
detached_list.append(self.detach_helper(ref_id=ref_id))
|
||||||
else:
|
else:
|
||||||
detached_list.append(self.traverse_value(o, detach))
|
detached_list.append(self.traverse_value(o, detach))
|
||||||
return detached_list
|
return detached_list
|
||||||
@@ -216,7 +243,7 @@ class BaseObjectSerializer:
|
|||||||
|
|
||||||
elif isinstance(obj, Base):
|
elif isinstance(obj, Base):
|
||||||
self.detach_lineage.append(detach)
|
self.detach_lineage.append(detach)
|
||||||
_, base_obj = self.traverse_base(obj)
|
_, base_obj = self._traverse_base(obj)
|
||||||
return base_obj
|
return base_obj
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -230,11 +257,11 @@ class BaseObjectSerializer:
|
|||||||
|
|
||||||
return str(obj)
|
return str(obj)
|
||||||
|
|
||||||
def detach_helper(self, ref_hash: str) -> Dict[str, str]:
|
def detach_helper(self, ref_id: str) -> Dict[str, str]:
|
||||||
"""Helper to keep track of detached objects and their depth in the family tree and create reference objects to place in the parent object
|
"""Helper to keep track of detached objects and their depth in the family tree and create reference objects to place in the parent object
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
ref_hash {str} -- the hash of the fully traversed object
|
ref_id {str} -- the id of the fully traversed object
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict -- a reference object to be inserted into the given object's parent
|
dict -- a reference object to be inserted into the given object's parent
|
||||||
@@ -243,19 +270,19 @@ class BaseObjectSerializer:
|
|||||||
for parent in self.lineage:
|
for parent in self.lineage:
|
||||||
if parent not in self.family_tree:
|
if parent not in self.family_tree:
|
||||||
self.family_tree[parent] = {}
|
self.family_tree[parent] = {}
|
||||||
if ref_hash not in self.family_tree[parent] or self.family_tree[parent][
|
if ref_id not in self.family_tree[parent] or self.family_tree[parent][
|
||||||
ref_hash
|
ref_id
|
||||||
] > len(self.detach_lineage):
|
] > len(self.detach_lineage):
|
||||||
self.family_tree[parent][ref_hash] = len(self.detach_lineage)
|
self.family_tree[parent][ref_id] = len(self.detach_lineage)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"referencedId": ref_hash,
|
"referencedId": ref_id,
|
||||||
"speckle_type": "reference",
|
"speckle_type": "reference",
|
||||||
}
|
}
|
||||||
|
|
||||||
def __reset_writer(self) -> None:
|
def __reset_writer(self) -> None:
|
||||||
"""Reinitializes the lineage, and other variables that get used during the json writing process"""
|
"""Reinitializes the lineage, and other variables that get used during the json writing process"""
|
||||||
self.detach_lineage = []
|
self.detach_lineage = [True]
|
||||||
self.lineage = []
|
self.lineage = []
|
||||||
self.family_tree = {}
|
self.family_tree = {}
|
||||||
self.closure_table = {}
|
self.closure_table = {}
|
||||||
@@ -271,6 +298,8 @@ class BaseObjectSerializer:
|
|||||||
"""
|
"""
|
||||||
if not obj_string:
|
if not obj_string:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
self.deserialized = {}
|
||||||
obj = safe_json_loads(obj_string)
|
obj = safe_json_loads(obj_string)
|
||||||
return self.recompose_base(obj=obj)
|
return self.recompose_base(obj=obj)
|
||||||
|
|
||||||
@@ -289,6 +318,9 @@ class BaseObjectSerializer:
|
|||||||
if isinstance(obj, str):
|
if isinstance(obj, str):
|
||||||
obj = safe_json_loads(obj)
|
obj = safe_json_loads(obj)
|
||||||
|
|
||||||
|
if "id" in obj and obj["id"] in self.deserialized:
|
||||||
|
return self.deserialized[obj["id"]]
|
||||||
|
|
||||||
if "speckle_type" in obj and obj["speckle_type"] == "reference":
|
if "speckle_type" in obj and obj["speckle_type"] == "reference":
|
||||||
obj = self.get_child(obj=obj)
|
obj = self.get_child(obj=obj)
|
||||||
|
|
||||||
@@ -319,19 +351,25 @@ class BaseObjectSerializer:
|
|||||||
|
|
||||||
# 2. handle referenced child objects
|
# 2. handle referenced child objects
|
||||||
elif "referencedId" in value:
|
elif "referencedId" in value:
|
||||||
ref_hash = value["referencedId"]
|
ref_id = value["referencedId"]
|
||||||
ref_obj_str = self.read_transport.get_object(id=ref_hash)
|
ref_obj_str = self.read_transport.get_object(id=ref_id)
|
||||||
if not ref_obj_str:
|
if ref_obj_str:
|
||||||
raise SpeckleException(
|
ref_obj = safe_json_loads(ref_obj_str, ref_id)
|
||||||
f"Could not find the referenced child object of id `{ref_hash}` in the given read transport: {self.read_transport.name}"
|
base.__setattr__(prop, self.recompose_base(obj=ref_obj))
|
||||||
|
else:
|
||||||
|
warnings.warn(
|
||||||
|
f"Could not find the referenced child object of id `{ref_id}` in the given read transport: {self.read_transport.name}",
|
||||||
|
SpeckleWarning,
|
||||||
)
|
)
|
||||||
ref_obj = safe_json_loads(ref_obj_str, ref_hash)
|
base.__setattr__(prop, self.handle_value(value))
|
||||||
base.__setattr__(prop, self.recompose_base(obj=ref_obj))
|
|
||||||
|
|
||||||
# 3. handle all other cases (base objects, lists, and dicts)
|
# 3. handle all other cases (base objects, lists, and dicts)
|
||||||
else:
|
else:
|
||||||
base.__setattr__(prop, self.handle_value(value))
|
base.__setattr__(prop, self.handle_value(value))
|
||||||
|
|
||||||
|
if "id" in obj:
|
||||||
|
self.deserialized[obj["id"]] = base
|
||||||
|
|
||||||
return base
|
return base
|
||||||
|
|
||||||
def handle_value(self, obj: Any):
|
def handle_value(self, obj: Any):
|
||||||
@@ -377,11 +415,13 @@ class BaseObjectSerializer:
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
def get_child(self, obj: Dict):
|
def get_child(self, obj: Dict):
|
||||||
ref_hash = obj["referencedId"]
|
ref_id = obj["referencedId"]
|
||||||
ref_obj_str = self.read_transport.get_object(id=ref_hash)
|
ref_obj_str = self.read_transport.get_object(id=ref_id)
|
||||||
if not ref_obj_str:
|
if not ref_obj_str:
|
||||||
raise SpeckleException(
|
warnings.warn(
|
||||||
f"Could not find the referenced child object of id `{ref_hash}` in the given read transport: {self.read_transport.name}"
|
f"Could not find the referenced child object of id `{ref_id}` in the given read transport: {self.read_transport.name}",
|
||||||
|
SpeckleWarning,
|
||||||
)
|
)
|
||||||
|
return obj
|
||||||
|
|
||||||
return safe_json_loads(ref_obj_str, ref_hash)
|
return safe_json_loads(ref_obj_str, ref_id)
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Optional, List, Dict
|
from typing import Optional, List, Dict
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from pydantic.main import Extra
|
from pydantic.config import Extra
|
||||||
|
|
||||||
# __________________
|
# __________________
|
||||||
# | |
|
# | |
|
||||||
@@ -156,29 +156,13 @@ class ServerTransport(AbstractTransport):
|
|||||||
lines = r.iter_lines(decode_unicode=True)
|
lines = r.iter_lines(decode_unicode=True)
|
||||||
|
|
||||||
# iter through returned objects saving them as we go
|
# iter through returned objects saving them as we go
|
||||||
|
target_transport.begin_write()
|
||||||
for line in lines:
|
for line in lines:
|
||||||
if line:
|
if line:
|
||||||
hash, obj = line.split("\t")
|
hash, obj = line.split("\t")
|
||||||
target_transport.save_object(hash, obj)
|
target_transport.save_object(hash, obj)
|
||||||
|
|
||||||
target_transport.save_object(id, root_obj_serialized)
|
target_transport.save_object(id, root_obj_serialized)
|
||||||
|
target_transport.end_write()
|
||||||
|
|
||||||
return root_obj_serialized
|
return root_obj_serialized
|
||||||
|
|
||||||
# async def stream_res(self, endpoint: str) -> str:
|
|
||||||
# data = b""
|
|
||||||
# async with aiohttp.ClientSession() as session:
|
|
||||||
# session.headers.update(
|
|
||||||
# {
|
|
||||||
# "Authorization": f"{self.session.headers['Authorization']}",
|
|
||||||
# "Accept": "text/plain",
|
|
||||||
# }
|
|
||||||
# )
|
|
||||||
# async with session.get(endpoint) as res:
|
|
||||||
# while True:
|
|
||||||
# chunk = await res.content.read(self.chunk_size)
|
|
||||||
# if not chunk:
|
|
||||||
# break
|
|
||||||
# data += chunk
|
|
||||||
|
|
||||||
# return data.decode("utf-8")
|
|
||||||
@@ -1,38 +1,39 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import sched
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from typing import Any, List, Dict
|
from typing import Any, List, Dict, Optional, Tuple
|
||||||
from appdirs import user_data_dir
|
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
from specklepy.transports.abstract_transport import AbstractTransport
|
from specklepy.transports.abstract_transport import AbstractTransport
|
||||||
from specklepy.logging.exceptions import SpeckleException
|
from specklepy.logging.exceptions import SpeckleException
|
||||||
|
from specklepy.paths import base_path
|
||||||
|
|
||||||
|
|
||||||
class SQLiteTransport(AbstractTransport):
|
class SQLiteTransport(AbstractTransport):
|
||||||
_name = "SQLite"
|
_name = "SQLite"
|
||||||
_base_path: str = None
|
_base_path: str = None
|
||||||
_root_path: str = None
|
_root_path: str = None
|
||||||
_is_writing: bool = False
|
|
||||||
_scheduler = sched.scheduler(time.time, time.sleep)
|
|
||||||
_polling_interval = 0.5 # seconds
|
|
||||||
__connection: sqlite3.Connection = None
|
__connection: sqlite3.Connection = None
|
||||||
app_name: str = ""
|
app_name: str = ""
|
||||||
scope: str = ""
|
scope: str = ""
|
||||||
saved_obj_count: int = 0
|
saved_obj_count: int = 0
|
||||||
|
max_size: int = None
|
||||||
|
_current_batch: List[Tuple[str, str]] = None
|
||||||
|
_current_batch_size: int = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
base_path: str = None,
|
base_path: Optional[str] = None,
|
||||||
app_name: str = None,
|
app_name: Optional[str] = None,
|
||||||
scope: str = None,
|
scope: str = None,
|
||||||
|
max_batch_size_mb: float = 10.0,
|
||||||
**data: Any,
|
**data: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(**data)
|
super().__init__(**data)
|
||||||
self.app_name = app_name or "Speckle"
|
self.app_name = app_name or "Speckle"
|
||||||
self.scope = scope or "Objects"
|
self.scope = scope or "Objects"
|
||||||
self._base_path = base_path or self.get_base_path(self.app_name)
|
self._base_path = base_path or self.get_base_path(self.app_name)
|
||||||
|
self.max_size = int(max_batch_size_mb * 1000 * 1000)
|
||||||
|
self._current_batch = []
|
||||||
|
self._current_batch_size = 0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.makedirs(self._base_path, exist_ok=True)
|
os.makedirs(self._base_path, exist_ok=True)
|
||||||
@@ -50,59 +51,25 @@ class SQLiteTransport(AbstractTransport):
|
|||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"SQLiteTransport(app: '{self.app_name}', scope: '{self.scope}')"
|
return f"SQLiteTransport(app: '{self.app_name}', scope: '{self.scope}')"
|
||||||
|
|
||||||
# def __write_timer_elapsed(self):
|
|
||||||
# print("WRITE TIMER ELAPSED")
|
|
||||||
# proc = Process(target=_run_queue, args=(self.__queue, self._root_path))
|
|
||||||
# proc.start()
|
|
||||||
# proc.join()
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_base_path(app_name):
|
def get_base_path(app_name):
|
||||||
# from appdirs https://github.com/ActiveState/appdirs/blob/master/appdirs.py
|
# # 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
|
# # default mac path is not the one we use (we use unix path), so using special case for this
|
||||||
system = sys.platform
|
# system = sys.platform
|
||||||
if system.startswith("java"):
|
# if system.startswith("java"):
|
||||||
import platform
|
# import platform
|
||||||
|
|
||||||
os_name = platform.java_ver()[3][0]
|
# os_name = platform.java_ver()[3][0]
|
||||||
if os_name.startswith("Mac"):
|
# if os_name.startswith("Mac"):
|
||||||
system = "darwin"
|
# system = "darwin"
|
||||||
|
|
||||||
if system != "darwin":
|
# if system != "darwin":
|
||||||
return user_data_dir(appname=app_name, appauthor=False, roaming=True)
|
# return user_data_dir(appname=app_name, appauthor=False, roaming=True)
|
||||||
|
|
||||||
path = os.path.expanduser("~/.config/")
|
# path = os.path.expanduser("~/.config/")
|
||||||
return os.path.join(path, app_name)
|
# return os.path.join(path, app_name)
|
||||||
|
|
||||||
# def __consume_queue(self):
|
return str(base_path(app_name))
|
||||||
# if self._is_writing or self.__queue.empty():
|
|
||||||
# return
|
|
||||||
# print("CONSUME QUEUE")
|
|
||||||
# self._is_writing = True
|
|
||||||
# while not self.__queue.empty():
|
|
||||||
# data = self.__queue.get()
|
|
||||||
# self.save_object(data[0], data[1])
|
|
||||||
# self._is_writing = False
|
|
||||||
|
|
||||||
# self._scheduler.enter(
|
|
||||||
# delay=self._polling_interval, priority=1, action=self.__consume_queue
|
|
||||||
# )
|
|
||||||
# self._scheduler.run(blocking=True)
|
|
||||||
|
|
||||||
# def save_object(self, id: str, serialized_object: str) -> None:
|
|
||||||
# """Adds an object to the queue and schedules it to be saved.
|
|
||||||
|
|
||||||
# Arguments:
|
|
||||||
# id {str} -- the object id
|
|
||||||
# serialized_object {str} -- the full string representation of the object
|
|
||||||
# """
|
|
||||||
# print("SAVE OBJECT")
|
|
||||||
# self.__queue.put((id, serialized_object))
|
|
||||||
|
|
||||||
# self._scheduler.enter(
|
|
||||||
# delay=self._polling_interval, priority=1, action=self.__consume_queue
|
|
||||||
# )
|
|
||||||
# self._scheduler.run(blocking=True)
|
|
||||||
|
|
||||||
def save_object_from_transport(
|
def save_object_from_transport(
|
||||||
self, id: str, source_transport: AbstractTransport
|
self, id: str, source_transport: AbstractTransport
|
||||||
@@ -117,23 +84,41 @@ class SQLiteTransport(AbstractTransport):
|
|||||||
self.save_object(id, serialized_object)
|
self.save_object(id, serialized_object)
|
||||||
|
|
||||||
def save_object(self, id: str, serialized_object: str) -> None:
|
def save_object(self, id: str, serialized_object: str) -> None:
|
||||||
"""Directly saves an object into the database.
|
"""
|
||||||
|
Adds an object to the current batch to be written to the db. If the current batch is full,
|
||||||
|
the batch is written to the db and the current batch is reset.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
id {str} -- the object id
|
id {str} -- the object id
|
||||||
serialized_object {str} -- the full string representation of the object
|
serialized_object {str} -- the full string representation of the object
|
||||||
"""
|
"""
|
||||||
|
obj_size = len(serialized_object)
|
||||||
|
if (
|
||||||
|
not self._current_batch
|
||||||
|
or self._current_batch_size + obj_size < self.max_size
|
||||||
|
):
|
||||||
|
self._current_batch.append((id, serialized_object))
|
||||||
|
self._current_batch_size += obj_size
|
||||||
|
return
|
||||||
|
|
||||||
|
self.save_current_batch()
|
||||||
|
self._current_batch = [(id, serialized_object)]
|
||||||
|
self._current_batch_size = obj_size
|
||||||
|
|
||||||
|
def save_current_batch(self) -> None:
|
||||||
|
"""Save the current batch of objects to the local db"""
|
||||||
self.__check_connection()
|
self.__check_connection()
|
||||||
try:
|
try:
|
||||||
with closing(self.__connection.cursor()) as c:
|
with closing(self.__connection.cursor()) as c:
|
||||||
c.execute(
|
c.executemany(
|
||||||
"INSERT OR IGNORE INTO objects(hash, content) VALUES(?,?)",
|
"INSERT OR IGNORE INTO objects(hash, content) VALUES(?,?)",
|
||||||
(id, serialized_object),
|
self._current_batch,
|
||||||
)
|
)
|
||||||
self.__connection.commit()
|
self.__connection.commit()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
raise SpeckleException(
|
raise SpeckleException(
|
||||||
f"Could not save the object to the local db. Inner exception: {ex}", ex
|
f"Could not save the batch of objects to the local db. Inner exception: {ex}",
|
||||||
|
ex,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_object(self, id: str) -> str or None:
|
def get_object(self, id: str) -> str or None:
|
||||||
@@ -156,10 +141,14 @@ class SQLiteTransport(AbstractTransport):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
def begin_write(self):
|
def begin_write(self):
|
||||||
|
self._object_cache = []
|
||||||
self.saved_obj_count = 0
|
self.saved_obj_count = 0
|
||||||
|
|
||||||
def end_write(self):
|
def end_write(self):
|
||||||
pass
|
if self._current_batch:
|
||||||
|
self.save_current_batch()
|
||||||
|
self._current_batch = []
|
||||||
|
self._current_batch_size = 0
|
||||||
|
|
||||||
def copy_object_and_children(
|
def copy_object_and_children(
|
||||||
self, id: str, target_transport: AbstractTransport
|
self, id: str, target_transport: AbstractTransport
|
||||||
@@ -198,20 +187,4 @@ class SQLiteTransport(AbstractTransport):
|
|||||||
self.__connection = sqlite3.connect(self._root_path)
|
self.__connection = sqlite3.connect(self._root_path)
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
self.__connection.close()
|
self.close()
|
||||||
|
|
||||||
|
|
||||||
# def _run_queue(queue: Queue, root_path: str):
|
|
||||||
# if queue.empty():
|
|
||||||
# return
|
|
||||||
# print("RUN QUEUE")
|
|
||||||
# conn = sqlite3.connect(root_path)
|
|
||||||
# while not queue.empty():
|
|
||||||
# data = queue.get()
|
|
||||||
# with closing(conn.cursor()) as c:
|
|
||||||
# c.execute(
|
|
||||||
# "INSERT OR IGNORE INTO objects(hash, content) VALUES(?,?)",
|
|
||||||
# (data[0], data[1]),
|
|
||||||
# )
|
|
||||||
# conn.commit()
|
|
||||||
# conn.close()
|
|
||||||
+15
-3
@@ -29,9 +29,14 @@ def seed_user(host):
|
|||||||
r = requests.post(
|
r = requests.post(
|
||||||
url=f"http://{host}/auth/local/register?challenge=pyspeckletests",
|
url=f"http://{host}/auth/local/register?challenge=pyspeckletests",
|
||||||
data=user_dict,
|
data=user_dict,
|
||||||
|
# do not follow redirects here, they lead to the frontend, which might not be
|
||||||
|
# running in a test environment
|
||||||
|
# causing the response to not be OK in the end
|
||||||
|
allow_redirects=False
|
||||||
)
|
)
|
||||||
print(r.url)
|
if not r.ok:
|
||||||
access_code = r.url.split("access_code=")[1]
|
raise Exception(f"Cannot seed user: {r.reason}")
|
||||||
|
access_code = r.text.split("access_code=")[1]
|
||||||
|
|
||||||
r_tokens = requests.post(
|
r_tokens = requests.post(
|
||||||
url=f"http://{host}/auth/token",
|
url=f"http://{host}/auth/token",
|
||||||
@@ -61,7 +66,14 @@ def second_user_dict(host):
|
|||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def client(host, user_dict):
|
def client(host, user_dict):
|
||||||
client = SpeckleClient(host=host, use_ssl=False)
|
client = SpeckleClient(host=host, use_ssl=False)
|
||||||
client.authenticate(user_dict["token"])
|
client.authenticate_with_token(user_dict["token"])
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def second_client(host, second_user_dict):
|
||||||
|
client = SpeckleClient(host=host, use_ssl=False)
|
||||||
|
client.authenticate_with_token(second_user_dict["token"])
|
||||||
return client
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import pytest
|
||||||
|
from specklepy.api.client import SpeckleClient
|
||||||
|
from specklepy.api.models import Activity, ActivityCollection, User
|
||||||
|
from specklepy.logging.exceptions import SpeckleException
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.run(order=2)
|
||||||
|
class TestUser:
|
||||||
|
def test_user_get_self(self, client: SpeckleClient, user_dict):
|
||||||
|
fetched_user = client.active_user.get()
|
||||||
|
|
||||||
|
assert isinstance(fetched_user, User)
|
||||||
|
assert fetched_user.name == user_dict["name"]
|
||||||
|
assert fetched_user.email == user_dict["email"]
|
||||||
|
|
||||||
|
user_dict["id"] = fetched_user.id
|
||||||
|
|
||||||
|
def test_user_update(self, client: SpeckleClient):
|
||||||
|
bio = "i am a ghost in the machine"
|
||||||
|
|
||||||
|
failed_update = client.active_user.update()
|
||||||
|
assert isinstance(failed_update, SpeckleException)
|
||||||
|
|
||||||
|
updated = client.active_user.update(bio=bio)
|
||||||
|
|
||||||
|
me = client.active_user.get()
|
||||||
|
|
||||||
|
assert updated is True
|
||||||
|
assert me.bio == bio
|
||||||
|
|
||||||
|
def test_user_activity(self, client: SpeckleClient, second_user_dict):
|
||||||
|
my_activity = client.active_user.activity(limit=10)
|
||||||
|
their_activity = client.other_user.activity(second_user_dict["id"])
|
||||||
|
|
||||||
|
assert isinstance(my_activity, ActivityCollection)
|
||||||
|
assert my_activity.items
|
||||||
|
assert isinstance(my_activity.items[0], Activity)
|
||||||
|
assert my_activity.totalCount
|
||||||
|
assert isinstance(their_activity, ActivityCollection)
|
||||||
|
|
||||||
|
older_activity = client.user.activity(before=my_activity.items[0].time)
|
||||||
|
|
||||||
|
assert isinstance(older_activity, ActivityCollection)
|
||||||
|
assert older_activity.totalCount
|
||||||
|
assert older_activity.totalCount < my_activity.totalCount
|
||||||
+37
-5
@@ -1,11 +1,13 @@
|
|||||||
|
from codecs import ascii_encode
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional, Union
|
||||||
from contextlib import ExitStack as does_not_raise
|
from contextlib import ExitStack as does_not_raise
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from specklepy.api import operations
|
from specklepy.api import operations
|
||||||
from specklepy.logging.exceptions import SpeckleException
|
from specklepy.logging.exceptions import SpeckleException, SpeckleInvalidUnitException
|
||||||
from specklepy.objects.base import Base, DataChunk
|
from specklepy.objects.base import Base, DataChunk
|
||||||
|
from specklepy.objects.units import Units
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -82,13 +84,22 @@ def test_setting_units():
|
|||||||
b = Base(units="foot")
|
b = Base(units="foot")
|
||||||
assert b.units == "ft"
|
assert b.units == "ft"
|
||||||
|
|
||||||
with pytest.raises(SpeckleException):
|
with pytest.raises(SpeckleInvalidUnitException):
|
||||||
b.units = "big"
|
b.units = "big"
|
||||||
|
|
||||||
b.units = None # invalid args are skipped
|
with pytest.raises(SpeckleInvalidUnitException):
|
||||||
b.units = 7
|
b.units = 7 # invalid args are skipped
|
||||||
assert b.units == "ft"
|
assert b.units == "ft"
|
||||||
|
|
||||||
|
b.units = None # None should be a valid arg
|
||||||
|
assert b.units == None
|
||||||
|
|
||||||
|
b.units = Units.none
|
||||||
|
assert b.units == "none"
|
||||||
|
|
||||||
|
b.units = Units.cm
|
||||||
|
assert b.units == Units.cm.value
|
||||||
|
|
||||||
|
|
||||||
def test_base_of_custom_speckle_type() -> None:
|
def test_base_of_custom_speckle_type() -> None:
|
||||||
b1 = Base.of_type("BirdHouse", name="Tweety's Crib")
|
b1 = Base.of_type("BirdHouse", name="Tweety's Crib")
|
||||||
@@ -111,6 +122,7 @@ class FrozenYoghurt(Base):
|
|||||||
add_ons: Optional[Dict[str, float]] # dict item types won't be checked
|
add_ons: Optional[Dict[str, float]] # dict item types won't be checked
|
||||||
price: float = 0.0
|
price: float = 0.0
|
||||||
dietary: DietaryRestrictions
|
dietary: DietaryRestrictions
|
||||||
|
tag: Union[int, str]
|
||||||
|
|
||||||
|
|
||||||
def test_type_checking() -> None:
|
def test_type_checking() -> None:
|
||||||
@@ -120,6 +132,8 @@ def test_type_checking() -> None:
|
|||||||
order.price = "7" # will get converted
|
order.price = "7" # will get converted
|
||||||
order.customer = "izzy"
|
order.customer = "izzy"
|
||||||
order.dietary = DietaryRestrictions.VEGAN
|
order.dietary = DietaryRestrictions.VEGAN
|
||||||
|
order.tag = "preorder"
|
||||||
|
order.tag = 4411
|
||||||
|
|
||||||
with pytest.raises(SpeckleException):
|
with pytest.raises(SpeckleException):
|
||||||
order.flavours = "not a list"
|
order.flavours = "not a list"
|
||||||
@@ -129,8 +143,26 @@ def test_type_checking() -> None:
|
|||||||
order.add_ons = ["sprinkles"]
|
order.add_ons = ["sprinkles"]
|
||||||
with pytest.raises(SpeckleException):
|
with pytest.raises(SpeckleException):
|
||||||
order.dietary = "no nuts plz"
|
order.dietary = "no nuts plz"
|
||||||
|
with pytest.raises(SpeckleException):
|
||||||
|
order.tag = ["tag01", "tag02"]
|
||||||
|
|
||||||
order.add_ons = {"sprinkles": 0.2, "chocolate": 1.0}
|
order.add_ons = {"sprinkles": 0.2, "chocolate": 1.0}
|
||||||
order.flavours = ["strawberry", "lychee", "peach", "pineapple"]
|
order.flavours = ["strawberry", "lychee", "peach", "pineapple"]
|
||||||
|
|
||||||
assert order.price == 7.0
|
assert order.price == 7.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_cached_deserialization() -> None:
|
||||||
|
material = Base(color="blue", opacity=0.5)
|
||||||
|
|
||||||
|
a = Base(name="a")
|
||||||
|
a["@material"] = material
|
||||||
|
b = Base(name="b")
|
||||||
|
b["@material"] = material
|
||||||
|
|
||||||
|
root = Base(a=a, b=b)
|
||||||
|
|
||||||
|
serialized = operations.serialize(root)
|
||||||
|
deserialized = operations.deserialize(serialized)
|
||||||
|
|
||||||
|
assert deserialized["a"]["@material"] is deserialized["b"]["@material"]
|
||||||
|
|||||||
@@ -45,4 +45,4 @@ def test_account_from_token_and_url():
|
|||||||
acct = get_account_from_token(token, url)
|
acct = get_account_from_token(token, url)
|
||||||
|
|
||||||
assert acct.token == token
|
assert acct.token == token
|
||||||
assert acct.serverInfo.url == url
|
assert acct.serverInfo.url == url
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from specklepy.api.models import Commit, Stream
|
|||||||
from specklepy.transports.server.server import ServerTransport
|
from specklepy.transports.server.server import ServerTransport
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.run(order=4)
|
@pytest.mark.run(order=6)
|
||||||
class TestCommit:
|
class TestCommit:
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def commit(self):
|
def commit(self):
|
||||||
|
|||||||
+112
-54
@@ -1,10 +1,11 @@
|
|||||||
|
# pylint: disable=redefined-outer-name
|
||||||
import json
|
import json
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from specklepy.api import operations
|
from specklepy.api import operations
|
||||||
from specklepy.logging.exceptions import SpeckleException
|
from specklepy.logging.exceptions import SpeckleException
|
||||||
from specklepy.objects.base import Base
|
from specklepy.objects.base import Base
|
||||||
|
from specklepy.objects.units import Units
|
||||||
from specklepy.objects.encoding import CurveArray, ObjectArray
|
from specklepy.objects.encoding import CurveArray, ObjectArray
|
||||||
from specklepy.objects.geometry import (
|
from specklepy.objects.geometry import (
|
||||||
Arc,
|
Arc,
|
||||||
@@ -13,8 +14,9 @@ from specklepy.objects.geometry import (
|
|||||||
BrepEdge,
|
BrepEdge,
|
||||||
BrepFace,
|
BrepFace,
|
||||||
BrepLoop,
|
BrepLoop,
|
||||||
|
BrepLoopType,
|
||||||
BrepTrim,
|
BrepTrim,
|
||||||
BrepTrimTypeEnum,
|
BrepTrimType,
|
||||||
Circle,
|
Circle,
|
||||||
Curve,
|
Curve,
|
||||||
Ellipse,
|
Ellipse,
|
||||||
@@ -48,12 +50,7 @@ def vector():
|
|||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def plane(point, vector):
|
def plane(point, vector):
|
||||||
return Plane(
|
return Plane(origin=point, normal=vector, xdir=vector, ydir=vector, units="m")
|
||||||
origin=point,
|
|
||||||
normal=vector,
|
|
||||||
xdir=vector,
|
|
||||||
ydir=vector,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
@@ -74,6 +71,7 @@ def line(point, interval):
|
|||||||
start=point,
|
start=point,
|
||||||
end=point,
|
end=point,
|
||||||
domain=interval,
|
domain=interval,
|
||||||
|
units="none"
|
||||||
# These attributes are not handled in C#
|
# These attributes are not handled in C#
|
||||||
# bbox=None,
|
# bbox=None,
|
||||||
# length=None
|
# length=None
|
||||||
@@ -81,7 +79,7 @@ def line(point, interval):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def arc(plane, interval):
|
def arc(plane, interval, point):
|
||||||
return Arc(
|
return Arc(
|
||||||
radius=2.3,
|
radius=2.3,
|
||||||
startAngle=22.1,
|
startAngle=22.1,
|
||||||
@@ -90,13 +88,13 @@ def arc(plane, interval):
|
|||||||
plane=plane,
|
plane=plane,
|
||||||
domain=interval,
|
domain=interval,
|
||||||
units="m",
|
units="m",
|
||||||
|
startPoint=point,
|
||||||
|
midPoint=point,
|
||||||
|
endPoint=point,
|
||||||
# These attributes are not handled in C#
|
# These attributes are not handled in C#
|
||||||
# bbox=None,
|
# bbox=None,
|
||||||
# area=None,
|
# area=None,
|
||||||
# length=None,
|
# length=None,
|
||||||
# startPoint=None,
|
|
||||||
# midPoint=None,
|
|
||||||
# endPoint=None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -236,7 +234,7 @@ def brep_edge(interval):
|
|||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def brep_loop():
|
def brep_loop():
|
||||||
return BrepLoop(FaceIndex=5, TrimIndices=[3, 4, 5], Type="unknown")
|
return BrepLoop(FaceIndex=5, TrimIndices=[3, 4, 5], Type=BrepLoopType.Unknown)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
@@ -249,7 +247,7 @@ def brep_trim():
|
|||||||
LoopIndex=4,
|
LoopIndex=4,
|
||||||
CurveIndex=7,
|
CurveIndex=7,
|
||||||
IsoStatus=6,
|
IsoStatus=6,
|
||||||
TrimType="Mated",
|
TrimType=BrepTrimType.Mated,
|
||||||
IsReversed=False,
|
IsReversed=False,
|
||||||
# These attributes are not handled in C#
|
# These attributes are not handled in C#
|
||||||
# Domain=None,
|
# Domain=None,
|
||||||
@@ -338,22 +336,22 @@ def geometry_objects_dict(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_to_and_from_list(object_name: str, geometry_objects_dict):
|
def test_to_and_from_list(object_name: str, geometry_objects_dict):
|
||||||
object = geometry_objects_dict[object_name]
|
obj = geometry_objects_dict[object_name]
|
||||||
assert hasattr(object, "to_list")
|
assert hasattr(obj, "to_list")
|
||||||
assert hasattr(object, "from_list")
|
assert hasattr(obj, "from_list")
|
||||||
|
|
||||||
chunks = object.to_list()
|
chunks = obj.to_list()
|
||||||
assert isinstance(chunks, list)
|
assert isinstance(chunks, list)
|
||||||
|
|
||||||
object_class = object.__class__
|
object_class = obj.__class__
|
||||||
decoded_object: Base = object_class.from_list(chunks)
|
decoded_object: Base = object_class.from_list(chunks)
|
||||||
assert decoded_object.get_id() == object.get_id()
|
assert decoded_object.get_id() == obj.get_id()
|
||||||
|
|
||||||
|
|
||||||
def test_brep_surfaces_value_serialization(surface):
|
def test_brep_surfaces_value_serialization(surface):
|
||||||
brep = Brep()
|
brep = Brep()
|
||||||
assert brep.Surfaces == None
|
assert brep.Surfaces is None
|
||||||
assert brep.SurfacesValue == None
|
assert brep.SurfacesValue is None
|
||||||
brep.Surfaces = [surface, surface]
|
brep.Surfaces = [surface, surface]
|
||||||
assert brep.SurfacesValue == ObjectArray.from_objects([surface, surface]).data
|
assert brep.SurfacesValue == ObjectArray.from_objects([surface, surface]).data
|
||||||
|
|
||||||
@@ -364,8 +362,8 @@ def test_brep_surfaces_value_serialization(surface):
|
|||||||
|
|
||||||
def test_brep_curve2d_values_serialization(curve, polyline, circle):
|
def test_brep_curve2d_values_serialization(curve, polyline, circle):
|
||||||
brep = Brep()
|
brep = Brep()
|
||||||
assert brep.Curve2D == None
|
assert brep.Curve2D is None
|
||||||
assert brep.Curve2DValues == None
|
assert brep.Curve2DValues is None
|
||||||
brep.Curve2D = [curve, polyline]
|
brep.Curve2D = [curve, polyline]
|
||||||
assert brep.Curve2DValues == CurveArray.from_curves([curve, polyline]).data
|
assert brep.Curve2DValues == CurveArray.from_curves([curve, polyline]).data
|
||||||
|
|
||||||
@@ -376,8 +374,8 @@ def test_brep_curve2d_values_serialization(curve, polyline, circle):
|
|||||||
|
|
||||||
def test_brep_curve3d_values_serialization(curve, polyline, circle):
|
def test_brep_curve3d_values_serialization(curve, polyline, circle):
|
||||||
brep = Brep()
|
brep = Brep()
|
||||||
assert brep.Curve3D == None
|
assert brep.Curve3D is None
|
||||||
assert brep.Curve3DValues == None
|
assert brep.Curve3DValues is None
|
||||||
brep.Curve3D = [curve, polyline]
|
brep.Curve3D = [curve, polyline]
|
||||||
assert brep.Curve3DValues == CurveArray.from_curves([curve, polyline]).data
|
assert brep.Curve3DValues == CurveArray.from_curves([curve, polyline]).data
|
||||||
|
|
||||||
@@ -389,9 +387,9 @@ def test_brep_curve3d_values_serialization(curve, polyline, circle):
|
|||||||
def test_brep_vertices_values_serialization():
|
def test_brep_vertices_values_serialization():
|
||||||
brep = Brep()
|
brep = Brep()
|
||||||
brep.VerticesValue = [1, 1, 1, 1, 2, 2, 2, 3, 3, 3]
|
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()
|
assert brep.Vertices[0].get_id() == Point(x=1, y=1, z=1, _units=Units.mm).get_id()
|
||||||
brep.Vertices[1].get_id() == Point(x=2, y=2, z=2, _units="mm").get_id()
|
assert brep.Vertices[1].get_id() == Point(x=2, y=2, z=2, _units=Units.mm).get_id()
|
||||||
brep.Vertices[2].get_id() == Point(x=3, y=3, z=3, _units="mm").get_id()
|
assert brep.Vertices[2].get_id() == Point(x=3, y=3, z=3, _units=Units.mm).get_id()
|
||||||
|
|
||||||
|
|
||||||
def test_trims_value_serialization():
|
def test_trims_value_serialization():
|
||||||
@@ -405,7 +403,7 @@ def test_trims_value_serialization():
|
|||||||
0,
|
0,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
1,
|
0,
|
||||||
1,
|
1,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
@@ -414,32 +412,82 @@ def test_trims_value_serialization():
|
|||||||
1,
|
1,
|
||||||
2,
|
2,
|
||||||
1,
|
1,
|
||||||
0,
|
1,
|
||||||
]
|
]
|
||||||
|
|
||||||
brep.Trims[0].get_id() == BrepTrim(
|
assert (
|
||||||
EdgeIndex=0,
|
brep.Trims[0].get_id()
|
||||||
StartIndex=0,
|
== BrepTrim(
|
||||||
EndIndex=0,
|
EdgeIndex=0,
|
||||||
FaceIndex=0,
|
StartIndex=0,
|
||||||
LoopIndex=0,
|
EndIndex=0,
|
||||||
CurveIndex=0,
|
FaceIndex=0,
|
||||||
IsoStatus=1,
|
LoopIndex=0,
|
||||||
TrimType=BrepTrimTypeEnum.Boundary,
|
CurveIndex=0,
|
||||||
IsReversed=False,
|
IsoStatus=1,
|
||||||
).get_id()
|
TrimType=BrepTrimType.Boundary,
|
||||||
|
IsReversed=False,
|
||||||
|
).get_id()
|
||||||
|
)
|
||||||
|
|
||||||
brep.Trims[1].get_id() == BrepTrim(
|
assert (
|
||||||
EdgeIndex=1,
|
brep.Trims[1].get_id()
|
||||||
StartIndex=0,
|
== BrepTrim(
|
||||||
EndIndex=0,
|
EdgeIndex=1,
|
||||||
FaceIndex=0,
|
StartIndex=0,
|
||||||
LoopIndex=0,
|
EndIndex=0,
|
||||||
CurveIndex=1,
|
FaceIndex=0,
|
||||||
IsoStatus=2,
|
LoopIndex=0,
|
||||||
TrimType=BrepTrimTypeEnum.Boundary,
|
CurveIndex=1,
|
||||||
IsReversed=True,
|
IsoStatus=2,
|
||||||
).get_id()
|
TrimType=BrepTrimType.Boundary,
|
||||||
|
IsReversed=True,
|
||||||
|
).get_id()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_loops_value_serialization():
|
||||||
|
brep = Brep()
|
||||||
|
brep.LoopsValue = [6, 0, 1, 0, 1, 2, 3]
|
||||||
|
|
||||||
|
assert brep == brep.Loops[0]._Brep # pylint: disable=protected-access
|
||||||
|
assert (
|
||||||
|
brep.Loops[0].get_id()
|
||||||
|
== BrepLoop(
|
||||||
|
FaceIndex=0, Type=BrepLoopType(1), TrimIndices=[0, 1, 2, 3]
|
||||||
|
).get_id()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_edges_value_serialization():
|
||||||
|
brep = Brep()
|
||||||
|
brep.EdgesValue = [8, 0, 0, 1, 0, -8.13345756858629, 8.13345756858629, 1, 3]
|
||||||
|
|
||||||
|
assert brep == brep.Edges[0]._Brep # pylint: disable=protected-access
|
||||||
|
assert (
|
||||||
|
brep.Edges[0].get_id()
|
||||||
|
== BrepEdge(
|
||||||
|
Curve3dIndex=0,
|
||||||
|
StartIndex=0,
|
||||||
|
EndIndex=1,
|
||||||
|
ProxyCurveIsReversed=False,
|
||||||
|
Domain=Interval(start=-8.13345756858629, end=8.13345756858629),
|
||||||
|
TrimIndices=[1, 3],
|
||||||
|
).get_id()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_faces_value_serialization():
|
||||||
|
brep = Brep()
|
||||||
|
brep.FacesValue = [4, 0, 0, 1, 0]
|
||||||
|
|
||||||
|
assert brep == brep.Faces[0]._Brep # pylint: disable=protected-access
|
||||||
|
assert (
|
||||||
|
brep.Faces[0].get_id()
|
||||||
|
== BrepFace(
|
||||||
|
SurfaceIndex=0, OuterLoopIndex=0, OrientationReversed=True, LoopIndices=[0]
|
||||||
|
).get_id()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_serialized_brep_attributes(brep: Brep):
|
def test_serialized_brep_attributes(brep: Brep):
|
||||||
@@ -447,7 +495,16 @@ def test_serialized_brep_attributes(brep: Brep):
|
|||||||
serialized = operations.serialize(brep, [transport])
|
serialized = operations.serialize(brep, [transport])
|
||||||
serialized_dict = json.loads(serialized)
|
serialized_dict = json.loads(serialized)
|
||||||
|
|
||||||
removed_keys = ["Surfaces", "Curve3D", "Curve2D", "Vertices", "Trims"]
|
removed_keys = [
|
||||||
|
"Surfaces",
|
||||||
|
"Curve3D",
|
||||||
|
"Curve2D",
|
||||||
|
"Vertices",
|
||||||
|
"Trims",
|
||||||
|
"Loops",
|
||||||
|
"Edges",
|
||||||
|
"Faces",
|
||||||
|
]
|
||||||
|
|
||||||
for k in removed_keys:
|
for k in removed_keys:
|
||||||
assert k not in serialized_dict.keys()
|
assert k not in serialized_dict.keys()
|
||||||
@@ -459,6 +516,7 @@ def test_mesh_create():
|
|||||||
mesh = Mesh.create(vertices, faces)
|
mesh = Mesh.create(vertices, faces)
|
||||||
|
|
||||||
with pytest.raises(SpeckleException):
|
with pytest.raises(SpeckleException):
|
||||||
|
# pylint: disable=unused-variable
|
||||||
bad_mesh = Mesh.create(vertices=7, faces=faces)
|
bad_mesh = Mesh.create(vertices=7, faces=faces)
|
||||||
|
|
||||||
assert mesh.vertices == vertices
|
assert mesh.vertices == vertices
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import pytest
|
||||||
|
from specklepy.api.host_applications import (
|
||||||
|
get_host_app_from_string,
|
||||||
|
_app_name_host_app_mapping,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_host_app_from_string_returns_fallback_app():
|
||||||
|
not_existing_app_name = "gmail"
|
||||||
|
host_app = get_host_app_from_string(not_existing_app_name)
|
||||||
|
assert host_app.name == not_existing_app_name
|
||||||
|
assert host_app.slug == not_existing_app_name
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("app_name", _app_name_host_app_mapping.keys())
|
||||||
|
def test_get_host_app_from_string_matches_for_predefined_apps(app_name) -> None:
|
||||||
|
host_app = get_host_app_from_string(app_name)
|
||||||
|
assert app_name in host_app.slug.lower()
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import pytest
|
||||||
|
from specklepy.api.client import SpeckleClient
|
||||||
|
from specklepy.api.models import Activity, ActivityCollection, LimitedUser
|
||||||
|
from specklepy.logging.exceptions import SpeckleException
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.run(order=4)
|
||||||
|
class TestOtherUser:
|
||||||
|
def test_user_get_self(self, client):
|
||||||
|
"""
|
||||||
|
Test, that a limited user query cannot query the active user.
|
||||||
|
"""
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
client.other_user.get()
|
||||||
|
|
||||||
|
def test_user_search(self, client, second_user_dict):
|
||||||
|
search_results = client.other_user.search(
|
||||||
|
search_query=second_user_dict["name"][:5]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(search_results, list)
|
||||||
|
assert len(search_results) > 0
|
||||||
|
result_user = search_results[0]
|
||||||
|
assert isinstance(result_user, LimitedUser)
|
||||||
|
assert result_user.name == second_user_dict["name"]
|
||||||
|
|
||||||
|
second_user_dict["id"] = result_user.id
|
||||||
|
assert getattr(result_user, "email", None) is None
|
||||||
|
|
||||||
|
def test_user_get(self, client, second_user_dict):
|
||||||
|
fetched_user = client.other_user.get(id=second_user_dict["id"])
|
||||||
|
|
||||||
|
assert isinstance(fetched_user, LimitedUser)
|
||||||
|
assert fetched_user.name == second_user_dict["name"]
|
||||||
|
# changed in the server, now you cannot get emails of other users
|
||||||
|
# not checking this, since the first user could or could not be an admin on the server
|
||||||
|
# admins can get emails of others, regular users can't
|
||||||
|
# assert fetched_user.email == None
|
||||||
|
|
||||||
|
second_user_dict["id"] = fetched_user.id
|
||||||
|
|
||||||
|
def test_user_activity(self, client: SpeckleClient, second_user_dict):
|
||||||
|
their_activity = client.other_user.activity(second_user_dict["id"])
|
||||||
|
|
||||||
|
assert isinstance(their_activity, ActivityCollection)
|
||||||
|
assert isinstance(their_activity.items, list)
|
||||||
|
assert isinstance(their_activity.items[0], Activity)
|
||||||
|
assert their_activity.totalCount
|
||||||
|
assert their_activity.totalCount > 0
|
||||||
@@ -9,7 +9,7 @@ from specklepy.objects.geometry import Point
|
|||||||
from specklepy.objects.fakemesh import FakeMesh
|
from specklepy.objects.fakemesh import FakeMesh
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.run(order=3)
|
@pytest.mark.run(order=5)
|
||||||
class TestSerialization:
|
class TestSerialization:
|
||||||
def test_serialize(self, base):
|
def test_serialize(self, base):
|
||||||
serialized = operations.serialize(base)
|
serialized = operations.serialize(base)
|
||||||
@@ -91,7 +91,7 @@ class TestSerialization:
|
|||||||
assert deserialised == {"foo": "bar"}
|
assert deserialised == {"foo": "bar"}
|
||||||
|
|
||||||
def test_big_int(self):
|
def test_big_int(self):
|
||||||
big_int = '{"big": ' + str(2 ** 64) + "}"
|
big_int = '{"big": ' + str(2**64) + "}"
|
||||||
deserialised = operations.deserialize(big_int)
|
deserialised = operations.deserialize(big_int)
|
||||||
|
|
||||||
assert deserialised == {"big": 2 ** 64}
|
assert deserialised == {"big": 2**64}
|
||||||
|
|||||||
+13
-2
@@ -1,5 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from specklepy.api.models import ServerInfo
|
from specklepy.api.models import ServerInfo
|
||||||
|
from specklepy.api.client import SpeckleClient
|
||||||
|
|
||||||
|
|
||||||
class TestServer:
|
class TestServer:
|
||||||
@@ -12,12 +13,22 @@ class TestServer:
|
|||||||
"lifespan": 9001,
|
"lifespan": 9001,
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_server_get(self, client):
|
def test_server_get(self, client: SpeckleClient):
|
||||||
server = client.server.get()
|
server = client.server.get()
|
||||||
|
|
||||||
assert isinstance(server, ServerInfo)
|
assert isinstance(server, ServerInfo)
|
||||||
|
|
||||||
def test_server_apps(self, client):
|
def test_server_version(self, client: SpeckleClient):
|
||||||
|
version = client.server.version()
|
||||||
|
|
||||||
|
assert isinstance(version, tuple)
|
||||||
|
if len(version) == 1:
|
||||||
|
assert version[0] == "dev"
|
||||||
|
else:
|
||||||
|
assert isinstance(version[0], int)
|
||||||
|
assert len(version) >= 3
|
||||||
|
|
||||||
|
def test_server_apps(self, client: SpeckleClient):
|
||||||
apps = client.server.apps()
|
apps = client.server.apps()
|
||||||
|
|
||||||
assert isinstance(apps, list)
|
assert isinstance(apps, list)
|
||||||
|
|||||||
+118
-13
@@ -1,11 +1,21 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from specklepy.api.models import ActivityCollection, Activity, Stream
|
from specklepy.api.models import (
|
||||||
|
ActivityCollection,
|
||||||
|
Activity,
|
||||||
|
PendingStreamCollaborator,
|
||||||
|
Stream,
|
||||||
|
User,
|
||||||
|
)
|
||||||
from specklepy.api.client import SpeckleClient
|
from specklepy.api.client import SpeckleClient
|
||||||
from specklepy.logging.exceptions import GraphQLException
|
from specklepy.logging.exceptions import (
|
||||||
|
GraphQLException,
|
||||||
|
SpeckleException,
|
||||||
|
UnsupportedException,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.run(order=2)
|
@pytest.mark.run(order=3)
|
||||||
class TestStream:
|
class TestStream:
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def stream(self):
|
def stream(self):
|
||||||
@@ -25,6 +35,10 @@ class TestStream:
|
|||||||
isPublic=False,
|
isPublic=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def second_user(self, second_client: SpeckleClient):
|
||||||
|
return second_client.user.get()
|
||||||
|
|
||||||
def test_stream_create(self, client, stream, updated_stream):
|
def test_stream_create(self, client, stream, updated_stream):
|
||||||
stream.id = updated_stream.id = client.stream.create(
|
stream.id = updated_stream.id = client.stream.create(
|
||||||
name=stream.name,
|
name=stream.name,
|
||||||
@@ -79,22 +93,67 @@ class TestStream:
|
|||||||
assert isinstance(favorited, Stream)
|
assert isinstance(favorited, Stream)
|
||||||
assert unfavorited.favoritedDate is None
|
assert unfavorited.favoritedDate is None
|
||||||
|
|
||||||
def test_stream_grant_permission(self, client, stream, second_user_dict):
|
def test_stream_grant_permission(self, client, stream, second_user):
|
||||||
granted = client.stream.grant_permission(
|
# deprecated as of Speckle Server 2.6.4
|
||||||
|
with pytest.raises(UnsupportedException):
|
||||||
|
client.stream.grant_permission(
|
||||||
|
stream_id=stream.id,
|
||||||
|
user_id=second_user.id,
|
||||||
|
role="stream:contributor",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_stream_invite(
|
||||||
|
self, client: SpeckleClient, stream: Stream, second_user_dict: dict
|
||||||
|
):
|
||||||
|
invited = client.stream.invite(
|
||||||
stream_id=stream.id,
|
stream_id=stream.id,
|
||||||
user_id=second_user_dict["id"],
|
email=second_user_dict["email"],
|
||||||
role="stream:contributor",
|
role="stream:reviewer",
|
||||||
|
message="welcome to my stream!",
|
||||||
)
|
)
|
||||||
|
|
||||||
fetched_stream = client.stream.get(stream.id)
|
assert invited is True
|
||||||
|
|
||||||
assert granted is True
|
# fail if no email or id
|
||||||
assert len(fetched_stream.collaborators) == 2
|
with pytest.raises(SpeckleException):
|
||||||
assert fetched_stream.collaborators[0].name == second_user_dict["name"]
|
client.stream.invite(stream_id=stream.id)
|
||||||
|
|
||||||
def test_stream_revoke_permission(self, client, stream, second_user_dict):
|
def test_stream_invite_get_all_for_user(
|
||||||
|
self, second_client: SpeckleClient, stream: Stream
|
||||||
|
):
|
||||||
|
# NOTE: these are user queries, but testing here to contain the flow
|
||||||
|
invites = second_client.user.get_all_pending_invites()
|
||||||
|
|
||||||
|
assert isinstance(invites, list)
|
||||||
|
assert isinstance(invites[0], PendingStreamCollaborator)
|
||||||
|
assert len(invites) == 1
|
||||||
|
|
||||||
|
invite = second_client.user.get_pending_invite(stream_id=stream.id)
|
||||||
|
assert isinstance(invite, PendingStreamCollaborator)
|
||||||
|
|
||||||
|
def test_stream_invite_use(self, second_client: SpeckleClient, stream: Stream):
|
||||||
|
invite: PendingStreamCollaborator = (
|
||||||
|
second_client.user.get_all_pending_invites()[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
accepted = second_client.stream.invite_use(
|
||||||
|
stream_id=stream.id, token=invite.token
|
||||||
|
)
|
||||||
|
|
||||||
|
assert accepted is True
|
||||||
|
|
||||||
|
def test_stream_update_permission(
|
||||||
|
self, client: SpeckleClient, stream: Stream, second_user: User
|
||||||
|
):
|
||||||
|
updated = client.stream.update_permission(
|
||||||
|
stream_id=stream.id, user_id=second_user.id, role="stream:contributor"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert updated is True
|
||||||
|
|
||||||
|
def test_stream_revoke_permission(self, client, stream, second_user):
|
||||||
revoked = client.stream.revoke_permission(
|
revoked = client.stream.revoke_permission(
|
||||||
stream_id=stream.id, user_id=second_user_dict["id"]
|
stream_id=stream.id, user_id=second_user.id
|
||||||
)
|
)
|
||||||
|
|
||||||
fetched_stream = client.stream.get(stream.id)
|
fetched_stream = client.stream.get(stream.id)
|
||||||
@@ -102,6 +161,52 @@ class TestStream:
|
|||||||
assert revoked is True
|
assert revoked is True
|
||||||
assert len(fetched_stream.collaborators) == 1
|
assert len(fetched_stream.collaborators) == 1
|
||||||
|
|
||||||
|
def test_stream_invite_cancel(
|
||||||
|
self,
|
||||||
|
client: SpeckleClient,
|
||||||
|
stream: Stream,
|
||||||
|
second_user: User,
|
||||||
|
):
|
||||||
|
invited = client.stream.invite(
|
||||||
|
stream_id=stream.id,
|
||||||
|
user_id=second_user.id,
|
||||||
|
message="welcome to my stream!",
|
||||||
|
)
|
||||||
|
assert invited is True
|
||||||
|
|
||||||
|
invites = client.stream.get_all_pending_invites(stream_id=stream.id)
|
||||||
|
|
||||||
|
cancelled = client.stream.invite_cancel(
|
||||||
|
invite_id=invites[0].inviteId, stream_id=stream.id
|
||||||
|
)
|
||||||
|
|
||||||
|
assert cancelled is True
|
||||||
|
|
||||||
|
def test_stream_invite_batch(
|
||||||
|
self, client: SpeckleClient, stream: Stream, second_user: User
|
||||||
|
):
|
||||||
|
# NOTE: only works for server admins
|
||||||
|
# invited = client.stream.invite_batch(
|
||||||
|
# stream_id=stream.id,
|
||||||
|
# emails=["userA@speckle.xyz", "userB@speckle.xyz"],
|
||||||
|
# user_ids=[second_user.id],
|
||||||
|
# message="yeehaw 🤠",
|
||||||
|
# )
|
||||||
|
|
||||||
|
# assert invited is True
|
||||||
|
|
||||||
|
# invited_only_email = client.stream.invite_batch(
|
||||||
|
# stream_id=stream.id,
|
||||||
|
# emails=["userC@speckle.xyz"],
|
||||||
|
# message="yeehaw 🤠",
|
||||||
|
# )
|
||||||
|
|
||||||
|
# assert invited_only_email is True
|
||||||
|
|
||||||
|
# fail if no emails or user ids
|
||||||
|
with pytest.raises(SpeckleException):
|
||||||
|
client.stream.invite_batch(stream_id=stream.id)
|
||||||
|
|
||||||
def test_stream_activity(self, client: SpeckleClient, stream: Stream):
|
def test_stream_activity(self, client: SpeckleClient, stream: Stream):
|
||||||
activity = client.stream.activity(stream.id)
|
activity = client.stream.activity(stream.id)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import json
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from specklepy.api import operations
|
||||||
|
from specklepy.logging.exceptions import SpeckleException
|
||||||
|
from specklepy.objects.base import Base
|
||||||
|
from specklepy.objects.encoding import CurveArray, ObjectArray
|
||||||
|
from specklepy.objects.geometry import (
|
||||||
|
Line,
|
||||||
|
Mesh,
|
||||||
|
Point,
|
||||||
|
Vector,
|
||||||
|
)
|
||||||
|
from specklepy.transports.memory import MemoryTransport
|
||||||
|
from specklepy.objects.structural.geometry import (
|
||||||
|
Node,
|
||||||
|
Element1D,
|
||||||
|
Element2D,
|
||||||
|
Restraint,
|
||||||
|
ElementType1D,
|
||||||
|
ElementType2D,
|
||||||
|
)
|
||||||
|
from specklepy.objects.structural.properties import (
|
||||||
|
Property1D,
|
||||||
|
Property2D,
|
||||||
|
SectionProfile,
|
||||||
|
MemberType,
|
||||||
|
ShapeType,
|
||||||
|
)
|
||||||
|
from specklepy.objects.structural.material import (
|
||||||
|
Material,
|
||||||
|
)
|
||||||
|
from specklepy.objects.structural.analysis import Model
|
||||||
|
|
||||||
|
from specklepy.objects.structural.loading import LoadGravity
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def point():
|
||||||
|
return Point(x=1, y=10, z=0)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def vector():
|
||||||
|
return Vector(x=0, y=0, z=-1)
|
||||||
|
|
||||||
|
|
||||||
|
@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 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 restraint():
|
||||||
|
return Restraint(code="FFFFFF")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def node(restraint, point):
|
||||||
|
return Node(basePoint=point, restraint=restraint, name="node1")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def material():
|
||||||
|
return Material(name="TestMaterial")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def memberType():
|
||||||
|
return MemberType(0)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def shapeType():
|
||||||
|
return ShapeType(8)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def sectionProfile(shapeType):
|
||||||
|
return SectionProfile(name="Test", shapeType=shapeType)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def property1D(memberType, sectionProfile, material):
|
||||||
|
return Property1D(
|
||||||
|
Material=material,
|
||||||
|
SectionProfile=sectionProfile,
|
||||||
|
memberType=memberType,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def elementType1D():
|
||||||
|
return ElementType1D(0)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def element1D(line, restraint, elementType1D, property1D):
|
||||||
|
return Element1D(
|
||||||
|
baseLine=line,
|
||||||
|
end1Releases=restraint,
|
||||||
|
end2Releases=restraint,
|
||||||
|
type=elementType1D,
|
||||||
|
property=property1D,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def property2D(material):
|
||||||
|
return Property2D(Material=material)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def elementType2D():
|
||||||
|
return ElementType2D(0)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def element2D(point, elementType2D):
|
||||||
|
return Element2D(
|
||||||
|
topology=[point],
|
||||||
|
type=elementType2D,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def loadGravity(element1D, element2D, vector):
|
||||||
|
return LoadGravity(elements=[element1D, element2D], gravityFactors=vector)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def model(loadGravity, element1D, element2D, material, property1D, property2D):
|
||||||
|
return Model(
|
||||||
|
loads=[loadGravity],
|
||||||
|
elements=[element1D, element2D],
|
||||||
|
materials=[material],
|
||||||
|
properties=[property1D, property2D],
|
||||||
|
)
|
||||||
@@ -129,4 +129,4 @@ def test_transform_serialisation(transform: Transform):
|
|||||||
serialized = operations.serialize(transform)
|
serialized = operations.serialize(transform)
|
||||||
deserialized = operations.deserialize(serialized)
|
deserialized = operations.deserialize(serialized)
|
||||||
|
|
||||||
assert transform.get_id() == deserialized.get_id()
|
assert transform.get_id() == deserialized.get_id()
|
||||||
|
|||||||
+43
-31
@@ -6,55 +6,67 @@ from specklepy.logging.exceptions import SpeckleException
|
|||||||
|
|
||||||
@pytest.mark.run(order=1)
|
@pytest.mark.run(order=1)
|
||||||
class TestUser:
|
class TestUser:
|
||||||
def test_user_get_self(self, client, user_dict):
|
def test_user_get_self(self, client: SpeckleClient, user_dict):
|
||||||
fetched_user = client.user.get()
|
with pytest.deprecated_call():
|
||||||
|
fetched_user = client.user.get()
|
||||||
|
|
||||||
assert isinstance(fetched_user, User)
|
assert isinstance(fetched_user, User)
|
||||||
assert fetched_user.name == user_dict["name"]
|
assert fetched_user.name == user_dict["name"]
|
||||||
assert fetched_user.email == user_dict["email"]
|
assert fetched_user.email == user_dict["email"]
|
||||||
|
|
||||||
user_dict["id"] = fetched_user.id
|
user_dict["id"] = fetched_user.id
|
||||||
|
|
||||||
def test_user_search(self, client, second_user_dict):
|
def test_user_search(self, client, second_user_dict):
|
||||||
search_results = client.user.search(search_query=second_user_dict["name"][:5])
|
with pytest.deprecated_call():
|
||||||
|
search_results = client.user.search(search_query=second_user_dict["name"][:5])
|
||||||
|
|
||||||
assert isinstance(search_results, list)
|
assert isinstance(search_results, list)
|
||||||
assert isinstance(search_results[0], User)
|
assert isinstance(search_results[0], User)
|
||||||
assert search_results[0].name == second_user_dict["name"]
|
assert search_results[0].name == second_user_dict["name"]
|
||||||
|
|
||||||
second_user_dict["id"] = search_results[0].id
|
second_user_dict["id"] = search_results[0].id
|
||||||
|
|
||||||
def test_user_get(self, client, second_user_dict):
|
def test_user_get(self, client, second_user_dict):
|
||||||
fetched_user = client.user.get(id=second_user_dict["id"])
|
with pytest.deprecated_call():
|
||||||
|
fetched_user = client.user.get(id=second_user_dict["id"])
|
||||||
|
|
||||||
assert isinstance(fetched_user, User)
|
assert isinstance(fetched_user, User)
|
||||||
assert fetched_user.name == second_user_dict["name"]
|
assert fetched_user.name == second_user_dict["name"]
|
||||||
assert fetched_user.email == second_user_dict["email"]
|
# changed in the server, now you cannot get emails of other users
|
||||||
|
# not checking this, since the first user could or could not be an admin on the server
|
||||||
|
# admins can get emails of others, regular users can't
|
||||||
|
# assert fetched_user.email == None
|
||||||
|
|
||||||
second_user_dict["id"] = fetched_user.id
|
second_user_dict["id"] = fetched_user.id
|
||||||
|
|
||||||
def test_user_update(self, client):
|
def test_user_update(self, client):
|
||||||
bio = "i am a ghost in the machine"
|
bio = "i am a ghost in the machine"
|
||||||
|
|
||||||
failed_update = client.user.update()
|
with pytest.deprecated_call():
|
||||||
updated = client.user.update(bio=bio)
|
failed_update = client.user.update()
|
||||||
|
assert isinstance(failed_update, SpeckleException)
|
||||||
|
with pytest.deprecated_call():
|
||||||
|
updated = client.user.update(bio=bio)
|
||||||
|
assert updated is True
|
||||||
|
|
||||||
me = client.user.get()
|
|
||||||
|
|
||||||
assert isinstance(failed_update, SpeckleException)
|
with pytest.deprecated_call():
|
||||||
assert updated is True
|
me = client.user.get()
|
||||||
assert me.bio == bio
|
assert me.bio == bio
|
||||||
|
|
||||||
def test_user_activity(self, client: SpeckleClient, second_user_dict):
|
def test_user_activity(self, client: SpeckleClient, second_user_dict):
|
||||||
my_activity = client.user.activity(limit=10)
|
with pytest.deprecated_call():
|
||||||
their_activity = client.user.activity(second_user_dict["id"])
|
my_activity = client.user.activity(limit=10)
|
||||||
|
their_activity = client.user.activity(second_user_dict["id"])
|
||||||
|
|
||||||
assert isinstance(my_activity, ActivityCollection)
|
assert isinstance(my_activity, ActivityCollection)
|
||||||
assert isinstance(my_activity.items[0], Activity)
|
assert my_activity.items
|
||||||
assert my_activity.totalCount > 0
|
assert isinstance(my_activity.items[0], Activity)
|
||||||
assert isinstance(their_activity, ActivityCollection)
|
assert my_activity.totalCount
|
||||||
|
assert isinstance(their_activity, ActivityCollection)
|
||||||
|
|
||||||
older_activity = client.user.activity(before=my_activity.items[0].time)
|
older_activity = client.user.activity(before=my_activity.items[0].time)
|
||||||
|
|
||||||
assert isinstance(older_activity, ActivityCollection)
|
assert isinstance(older_activity, ActivityCollection)
|
||||||
assert older_activity.totalCount < my_activity.totalCount
|
assert older_activity.totalCount
|
||||||
|
assert older_activity.totalCount < my_activity.totalCount
|
||||||
|
|||||||
+41
-1
@@ -1,5 +1,9 @@
|
|||||||
import pytest
|
import json
|
||||||
from specklepy.api.wrapper import StreamWrapper
|
from specklepy.api.wrapper import StreamWrapper
|
||||||
|
from specklepy.transports.sqlite import SQLiteTransport
|
||||||
|
from specklepy.paths import accounts_path
|
||||||
|
from pathlib import Path
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
def test_parse_stream():
|
def test_parse_stream():
|
||||||
@@ -79,3 +83,39 @@ def test_get_transport_with_token():
|
|||||||
|
|
||||||
assert transport is not None
|
assert transport is not None
|
||||||
assert client.account.token == "super-secret-token"
|
assert client.account.token == "super-secret-token"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def user_path() -> Path:
|
||||||
|
path = accounts_path().joinpath("test_acc.json")
|
||||||
|
# hey, py37 doesn't support the missing_ok argument
|
||||||
|
try:
|
||||||
|
path.unlink()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
path.unlink(missing_ok=True)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
path.parent.absolute().mkdir(exist_ok=True)
|
||||||
|
yield path
|
||||||
|
path.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
def test_wrapper_url_match(user_path) -> None:
|
||||||
|
"""
|
||||||
|
The stream wrapper should only recognize exact url matches for the account
|
||||||
|
definitions and not match for subdomains.
|
||||||
|
"""
|
||||||
|
account = {
|
||||||
|
"token": "foobar",
|
||||||
|
"serverInfo": {"name": "foo", "url": "http://foo.bar.baz", "company": "Foo"},
|
||||||
|
"userInfo": {"id": "bla", "name": "A rando tester", "email": "rando@tester.me"},
|
||||||
|
}
|
||||||
|
|
||||||
|
user_path.write_text(json.dumps(account))
|
||||||
|
wrap = StreamWrapper("http://bar.baz/streams/bogus")
|
||||||
|
|
||||||
|
account = wrap.get_account()
|
||||||
|
|
||||||
|
assert account.userInfo.email is None
|
||||||
|
|||||||
Reference in New Issue
Block a user