Compare commits

...

248 Commits

Author SHA1 Message Date
izzy lyseggen 1e6e66a90a Merge pull request #116 from specklesystems/izzy/commit-spec
feat(client): add `branchName` to commit model
2021-08-11 09:24:49 +01:00
izzy lyseggen 09d84cf64a feat(client): add branchName to commit model 2021-08-11 09:21:04 +01:00
izzy lyseggen 3ccb0ae2a8 ci: try diff env tag variable 2021-08-10 15:49:08 +01:00
izzy lyseggen 6028a38355 Merge pull request #115 from specklesystems/ci/tags-and-versions
ci: fix workflows and patch version with git tag
2021-08-10 15:42:42 +01:00
izzy lyseggen 07418cfc9c ci: rename test job 2021-08-10 15:41:03 +01:00
izzy lyseggen 1ada797d81 ci: rename test job 2021-08-10 15:40:32 +01:00
izzy lyseggen 73703f6237 ci: patch version with git tag 2021-08-10 15:38:02 +01:00
izzy lyseggen 7644af22df fix(ci): add tag filter to build job 2021-08-10 15:37:19 +01:00
izzy lyseggen 564e1d4432 Merge pull request #114 from specklesystems/izzy/stream-wrapper-update
feat(wrapper): add get acct helper
2021-08-10 12:34:08 +01:00
izzy lyseggen fc4511ad02 chore: bump version 2021-08-10 12:03:06 +01:00
izzy lyseggen ad710b72da feat(wrapper): add acct helper 2021-08-10 12:02:42 +01:00
izzy lyseggen 041d9f56ce ci: another workflow fix 🙃 2021-08-06 17:13:16 +01:00
izzy lyseggen e1c0b705ad ci: build in deploy fix 2021-08-06 17:10:55 +01:00
izzy lyseggen 7b011b1122 ci: require build in deploy step 2021-08-06 17:06:42 +01:00
izzy lyseggen 3f09cd9d77 chore: bump version 2021-08-06 17:02:19 +01:00
izzy lyseggen 29a361892b Merge pull request #113 from specklesystems/izzy/stream-wrapper
🌯 Stream Wrapper with Client & Server Transport helper methods
2021-08-06 16:54:53 +01:00
izzy lyseggen 2672b40aff ci: remove speckle-server version 2021-08-06 16:53:09 +01:00
izzy lyseggen 35b6911b27 ci: quick fix 2021-08-06 16:43:53 +01:00
izzy lyseggen a4f0a2cc2b feat(server): transport init exception if no token 2021-08-06 16:39:02 +01:00
izzy lyseggen 1970890ecc test: stream wrapper tests 2021-08-06 16:38:40 +01:00
izzy lyseggen 13df5135b8 feat(credentials): stream wrapper!
can provide client and transport for you
2021-08-06 16:38:10 +01:00
izzy lyseggen 4e206b5c60 style: formatting 2021-08-06 16:09:12 +01:00
izzy lyseggen e696091555 feat(client): add string repr 2021-08-06 16:08:46 +01:00
izzy lyseggen a512dbb4e4 feat(logging): add SpeckleWarning class 2021-08-06 16:08:18 +01:00
izzy lyseggen 9a1f28516d chore: bump version to 2.2.5 2021-07-30 11:35:43 +01:00
izzy lyseggen 92892b83d8 Merge pull request #110 from specklesystems/izzy/sql-fix
fix(sqlite): delay and try except transport init
2021-07-30 11:31:55 +01:00
izzy lyseggen 8904e9eeb4 docs(ops): note that sqlite is default for receive 2021-07-30 11:31:18 +01:00
izzy lyseggen 68dc1794ee fix(sqlite): try catch initialisation of transport 2021-07-30 11:25:35 +01:00
izzy lyseggen 7e7940f25b refactor(ops): delay init of SQLiteTransport 2021-07-30 11:25:02 +01:00
izzy lyseggen 29c97cde45 chore: bump version to 2.2.4 2021-07-29 16:14:25 +01:00
izzy lyseggen 02f4f4fe41 Merge pull request #109 from specklesystems/izzy/sql-transport
feat(sqlite): remove unusued queue
2021-07-29 16:12:37 +01:00
izzy lyseggen b2dd5bfedd feat(sqlite): remove unusued queue 2021-07-29 16:11:04 +01:00
izzy lyseggen 09b3edcc23 Merge pull request #106 from specklesystems/izzy/smol-sparkles
fix(base): get parent props on child serialisation
2021-07-28 18:08:00 +01:00
izzy lyseggen a44036863d fix(base): get units prop on children 2021-07-28 18:01:51 +01:00
izzy lyseggen 5806c032dd feat(units): add km 2021-07-28 18:01:25 +01:00
izzy lyseggen cff20aec54 Merge pull request #105 from specklesystems/izzy/client-docstrings
docs(client): add dosctring to the `SpeckleClient`
2021-07-22 09:28:04 +01:00
izzy lyseggen 144d51b147 docs(client): add dosctring to the SpeckleClient
as alan noted, the default one is garbo lol
hopefully this will make it clearer for users
2021-07-22 09:27:32 +01:00
izzy lyseggen 09f61a6efd Update README.md 2021-07-22 09:15:26 +01:00
Matteo Cominetti e61bf0f78f Update README.md 2021-07-22 09:14:41 +01:00
izzy lyseggen 6ac72ce8ee Merge pull request #104 from specklesystems/izzy/readme
docs(readme): use xyz as example host
2021-07-22 09:12:34 +01:00
izzy lyseggen 0b9ef942f5 docs(readme): use xyz as example host 2021-07-22 09:12:20 +01:00
izzy lyseggen 6988eae46f Merge pull request #102 from specklesystems/izzy/deps-issues
chore(build): update to gql 3.0.0a6
2021-06-11 14:54:32 +01:00
izzy lyseggen 31fa619f82 chore(build): update to gql 3.0.0a6 2021-06-11 14:52:28 +01:00
izzy lyseggen 409ac68df0 Merge pull request #101 from specklesystems/izzy/json-acct-tweak
Izzy/json acct tweak
2021-06-04 17:37:25 +01:00
izzy lyseggen 81051a87c1 chore: bump version 2021-06-04 17:36:02 +01:00
izzy lyseggen fca386706b fix(credentials): tweak default base path
align with docs
2021-06-04 17:35:25 +01:00
izzy lyseggen 80036b0b98 Merge pull request #100 from specklesystems/izzy/json-accts
feat(credentials): read json files from local accts
2021-06-04 11:42:59 +01:00
izzy lyseggen 92c9a0882e chore: revert yml change 2021-06-04 10:30:20 +01:00
izzy lyseggen 239c466264 chore: bump version 2021-06-04 10:24:09 +01:00
izzy lyseggen 7dd490b24f feat(accounts): read json files from local 2021-06-04 10:23:54 +01:00
izzy lyseggen 54c3d6fbaf chore: bump version 2021-06-02 10:12:58 +01:00
izzy lyseggen 7b7cd86f50 Merge pull request #99 from specklesystems/izzy/note-on-paths
docs: data paths for diff platforms
2021-05-27 18:06:39 +01:00
izzy lyseggen 3fde452d1e docs: data paths for diff platforms 2021-05-27 18:05:26 +01:00
izzy lyseggen 113d1f1993 Merge pull request #93 from CyrilWaechter/main
Fix: path process error on Linux #96
2021-05-27 17:28:03 +01:00
izzy lyseggen d2bacb9ec2 Merge pull request #98 from specklesystems/izzy/accts-hotfix
fix(credentials): make some fields optional
2021-05-25 15:30:14 +01:00
izzy lyseggen d461e64b97 fix(credentials): make some fields optional
for simple manual account addition
2021-05-25 15:23:16 +01:00
CyrilWaechter dc923c3105 Fix: specklesystems/speckle-blender/issues/24 2021-05-24 17:55:34 +02:00
Dimitrie Stefanescu 46437e7af4 Update README.md 2021-05-23 16:28:14 +01:00
Cristian Balas 6ba632b14d Merge pull request #87 from specklesystems/cristi/diff_endpoints
switched to diff endpoints for download/upload
2021-05-18 18:42:19 +03:00
cristi8 c67eb0520e better error handling, fixed an issue 2021-05-17 22:04:43 +03:00
izzy lyseggen b7b171289c Merge pull request #90 from specklesystems/izzy/nones-on-receive
fix(serializer): handle receiving `None` vals
2021-05-17 16:02:46 +01:00
izzy lyseggen c4ac12d9de fix(serializer): handle receiving None vals 2021-05-17 15:48:46 +01:00
cristi8 6abeafdd9e Save root object after the children are saved to local transport 2021-05-17 13:37:31 +03:00
cristi8 a016ed9201 Changed parameter name 2021-05-11 19:55:24 +03:00
cristi8 9e0f71e5c0 switched to diff endpoints for download/upload
WARNING: not compatible with older/current released speckle servers
2021-05-11 15:53:45 +03:00
izzy lyseggen e02c4a76e8 Merge pull request #86 from specklesystems/izzy/ci
Automated releases to PyPI
2021-04-30 16:04:10 +01:00
izzy lyseggen 8e14cf1904 ci: remove dry run safeguard 2021-04-30 16:02:33 +01:00
izzy lyseggen 1c868b6b60 ci: trying automated publishes 2021-04-30 15:55:37 +01:00
izzy lyseggen 809dff5fb0 build: bump minor version
new materials!
2021-04-30 15:44:17 +01:00
izzy lyseggen e5074acff9 Merge pull request #85 from specklesystems/izzy/ci
ci: switch to poetry
2021-04-27 10:34:39 +01:00
izzy lyseggen 85d2065c29 chore: remove requirements.txt 2021-04-27 10:19:14 +01:00
izzy lyseggen bb48054fea ci: switch to poetry maybe? :) 2021-04-27 10:05:39 +01:00
izzy lyseggen 5a9b4137da Merge pull request #84 from specklesystems/izzy/tests-fix
Fix failing tests
2021-04-22 10:05:29 +01:00
izzy lyseggen 6c7dc1eed9 tests: upload objects to each test stream 2021-04-22 09:54:23 +01:00
Alan Rynne d47bf7d842 Merge pull request #82 from specklesystems/ci/docker-images
ci: Spin-up new server for each test run
2021-04-21 11:23:10 +02:00
Alan Rynne d07bab61e7 ci: Fixes with Izzy 2021-04-21 10:05:18 +02:00
Alan Rynne 3f7290ed12 ci: Reverting 2021-04-20 20:17:01 +02:00
Alan Rynne ed92678a31 ci: Random test with "http" 2021-04-20 20:08:14 +02:00
Alan Rynne 0428d2149f ci: Run those tests! 2021-04-20 20:03:18 +02:00
Alan Rynne da2d646f78 ci: Testing matrix versions 2021-04-20 20:00:06 +02:00
Alan Rynne a6543b122f ci: Checking build script only... 🤔 2021-04-20 19:56:34 +02:00
Alan Rynne 4e26ebfbe2 ci: First coin-flip on Spinning up a Docker server for each test 2021-04-20 19:47:44 +02:00
izzy lyseggen 4f79698987 Merge pull request #81 from specklesystems/izzy/materials
feat(objects): add RenderMaterial
2021-04-15 17:05:05 +01:00
izzy lyseggen 2c10134703 feat(objects): add RenderMaterial 2021-04-15 17:04:39 +01:00
izzy lyseggen 27346b6d38 Merge pull request #78 from specklesystems/izzy/rename-package
Rename package to `specklepy` to avoid conflicting with 1.0
2021-04-09 12:36:35 +01:00
izzy lyseggen 816f4373dd chore: update pyproject.toml 2021-04-09 12:25:51 +01:00
izzy lyseggen 434ada8bc1 feat: rename speckle to specklepy 2021-04-09 12:25:32 +01:00
izzy lyseggen 066bc44457 Merge pull request #77 from specklesystems/izzy/datachunk-fix
fix(base): specify full name for datachunk
2021-03-22 17:30:08 +00:00
izzy lyseggen 633267b025 Merge pull request #76 from specklesystems/izzy/test-fix
test: update host to use latest.speckle.dev
2021-03-22 17:28:24 +00:00
izzy lyseggen bd4ae7c5c5 fix(base): specify full name for datachunk
would bork in other connectors with name as just `DataChunk`
2021-03-22 17:28:00 +00:00
izzy lyseggen 501f9b8648 test: update host to use latest.speckle.dev 2021-03-22 15:03:22 +00:00
izzy lyseggen 7d2a03e19d Merge pull request #74 from specklesystems/cristi/server_transport_batching
ServerTransport: object batching and sending from multiple background threads
2021-03-22 14:57:09 +00:00
cristi8 c6c7d1731a ServerTransport: Added object batching and sending from multiple background threads 2021-03-22 14:24:14 +02:00
izzy lyseggen a839073968 Merge pull request #73 from specklesystems/izzy/ops
feat(operations): better (de)serialize functions
2021-03-19 20:04:06 +00:00
izzy lyseggen 883ec6e6ae feat(operations): better (de)serialize functions
allow providing transports to the serialize / deserialize functions
to see detaching and chunking
2021-03-19 15:18:06 +00:00
izzy lyseggen 87002257be Merge pull request #72 from specklesystems/izzy/detached-attrs
feat(base): improve adding chunkables/detachables
2021-03-19 10:26:54 +00:00
izzy lyseggen 1bc0cf04f3 fix(base): grammar (detached -> detachable) 2021-03-19 10:20:17 +00:00
izzy lyseggen d0e2350ab7 feat(base): improve adding chunkables/detachables
adds helper methods on `Base` that can be called in the constructors
to prevent ppl messing this up when defining their own objects
2021-03-19 10:14:42 +00:00
izzy lyseggen 255681a887 Merge pull request #70 from specklesystems/izzy/detached-lists
feat(serialisation): optimisations baBY
2021-03-18 18:47:28 +00:00
izzy lyseggen 614eefc393 fix(serialisation): EMBARASSING BUG
so i'm a bit of a dumbo here.
i didn't realise that doing `.update()` on attr would update the parent
attr as well and extend to all objects every ahhhhhh

you have to do `self.thing = self.thing + blah blah` to just update
the instance attr. the more ya know!

#roastme @cristi8
2021-03-18 18:45:24 +00:00
izzy lyseggen d5b506f298 fix(base): swap getattr_static for getattr
brought serialisation time for a big object
down from 12 seconds to 1.7 seconds!!

ty cristi
2021-03-18 17:05:18 +00:00
izzy lyseggen dcd28b5d79 feat(base): don't return set id on get_id
align with core in that `get_id` _always_ fully serializes to get hash
2021-03-18 16:35:28 +00:00
izzy lyseggen c739594ba8 fix(serializer): add children count on serialize
this was previously done _after_ hashing leading to different hashes
on two same objects due to this one field being different
2021-03-18 16:35:28 +00:00
izzy lyseggen 1d4867fb9b test: add detached list to FakeMesh 2021-03-18 16:35:28 +00:00
izzy lyseggen ec94a42ac6 Merge pull request #69 from specklesystems/izzy/detached-lists
fix(serializer): detached lists
2021-03-18 11:09:47 +00:00
izzy lyseggen 6e64770380 test(serializer): test detachment within lists 2021-03-17 17:38:05 +00:00
izzy lyseggen ef92127ed2 feat(serializer): support detachment within lists
this was a borkage on my part - oops!
2021-03-17 17:37:45 +00:00
izzy lyseggen fb8a40bf76 Merge pull request #67 from specklesystems/izzy/objects
fix(serialiser): import geometry objects
2021-03-01 11:17:22 +00:00
izzy lyseggen 81fab3ec6b fix(serialiser): import geometry objects
default to using our obj for deserialisation
2021-03-01 11:15:51 +00:00
izzy lyseggen 9a0c36c665 Merge pull request #66 from specklesystems/izzy/objects
🧊 Basic Geometry Objects
2021-02-26 18:03:41 +00:00
izzy lyseggen 995526c786 feat(objects): jk i gotchu with dem breps 😉 2021-02-26 18:01:14 +00:00
izzy lyseggen 5a66d912ae test: update to use new geo objects 2021-02-26 16:56:49 +00:00
izzy lyseggen 78abbb18c4 feat(objects): geometry yoooo
all geo except for breps
2021-02-26 16:56:10 +00:00
izzy lyseggen 26abdc952a fix(units): add new none type 2021-02-26 16:28:38 +00:00
izzy lyseggen 9a0207ba09 feat(obj): update point class
remove value attribute
2021-02-24 18:34:40 +00:00
izzy lyseggen 4369f8c5c8 Merge pull request #64 from specklesystems/izzy/props
🥠 feat(base): support serialisation of all properties
2021-02-24 17:15:18 +00:00
izzy lyseggen bc432c2f13 feat(objects): override point type 2021-02-24 17:02:47 +00:00
izzy lyseggen d1d3876902 feat(serializer): traverse properties as well 2021-02-24 17:02:11 +00:00
izzy lyseggen b1b144969b feat(base): include properties in member names 2021-02-24 17:01:32 +00:00
izzy lyseggen 4e3ee488be refactor(operations): small simplification 2021-02-24 16:59:12 +00:00
izzy lyseggen b2e2455b16 Merge pull request #61 from specklesystems/izzy/bug-fixes
🐛 Even more bug fixes!
2021-02-22 18:27:06 +00:00
izzy lyseggen ede286f4c0 fix(serialiser): generalise receiving chunk check 2021-02-22 18:23:30 +00:00
izzy lyseggen 56547a4573 fix(client): register branches methods 2021-02-22 18:23:30 +00:00
izzy lyseggen 13886b9caf Merge pull request #58 from gjedlicska/base_type_registration
Custom Base subclasses are automatically registered for deserialization
2021-02-22 18:22:08 +00:00
Gergő Jedlicska 5342cc4827 fix(recomposition): add dictionary recomposition option
If speckle type is not defined for an object, its recomposition results a python dictionary.
2021-02-22 19:17:09 +01:00
Gergő Jedlicska 6fe338628d test(deserialization): undefined behavior of deserialization
Add failing test to uncover a possible bug in deserialization
2021-02-22 17:04:46 +01:00
Gergő Jedlicska 48883466fb Merge branch 'main' of github.com:specklesystems/speckle-py into base_type_registration 2021-02-22 16:59:22 +01:00
izzy lyseggen 47917d99b0 Merge pull request #60 from specklesystems/izzy/receive-fix
🐛 fix(serialiser): smol bug fixes
2021-02-22 10:53:24 +00:00
izzy lyseggen 7703de0a37 test: unknown type deserialisation 2021-02-22 10:22:02 +00:00
izzy lyseggen 81b96cb6ac fix(serialiser): smol bug fixes
- don't overwrite `speckle_type` when receiving unknown object
- check for empty lists on deserialisation
- explicit none check for ignored vals on serialisation
(was unintentionally ignoring `0`s)
2021-02-22 10:17:09 +00:00
Matteo Cominetti 2334aefb09 docs: updates readme with link to docs 2021-02-19 18:38:51 +00:00
Gergő Jedlicska 800b0018a0 style: ran formatter on codebase 2021-02-17 23:52:14 +01:00
Gergő Jedlicska e6cf22e97a feat(base.py): provide easy subclass type registration for (de)serialization
Implement automatic type register mechanism that stores all speckle Base model subclasses in a type
register for deserialization reuse in transports. This enables the Base to be useful as a base kit
object.

fix #50
2021-02-17 23:51:07 +01:00
izzy lyseggen f5abcec6d0 Merge pull request #53 from CyrilWaechter/patch-1
Add infos and fix to README.md
2021-02-16 15:31:39 +00:00
Cyril Waechter fcd54a0899 Add info to README.md
* Add some import to make origin of objects / module used explicit
* Fix #L118:
```python
In [46]: received_base = client.object.get(hash)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-46-8171539461b3> in <module>
----> 1 received_base = client.object.get(hash)

TypeError: get() missing 1 required positional argument: 'object_id'
```
2021-02-16 15:43:36 +01:00
Gergő Jedlicska adfba846ce Merge branch 'base_type_registration' of github.com:gjedlicska/speckle-py into base_type_registration 2021-02-13 14:48:10 +01:00
Gergő Jedlicska 4933ca4abf feat(base object): enforce unique speckle_type value
It was possible to override the builint types with a duplicate speckle_type, either via a duplicate
class name of an explicit speckle_type definition. It is now checked before registering the new type
in the subclass registry, and a meaningful error is thrown.
2021-02-13 14:46:54 +01:00
Gergő Jedlicska 8ce595cb4a feat(base object): enforce unique speckle_type value
It was possible to override the builint types with a duplicate speckle_type, either via a duplicate
class name of an explicit speckle_type definition. It is now checked before registering the new type
in the subclass registry, and a meaningful error is thrown.

#50
2021-02-13 14:45:14 +01:00
Gergő Jedlicska 1114b210f9 refactor(objects module def): remove redundant subclass discovery code
With the automated subclass registry, there is no need for the magic module lookup in objects
__init__
2021-02-13 14:06:32 +01:00
Gergő Jedlicska 00c1e378d5 feat(base model and deserialization): base model subclasses can now be automatically deserialized
Subclassing types of Base are stored in a class attribute that can be looked up via the
`speckle_type`.

fix #50
2021-02-13 10:19:12 +01:00
Gergő Jedlicska 1291b5799b Merge branch 'main' of github.com:specklesystems/speckle-py into base_type_registration 2021-02-13 09:51:32 +01:00
izzy lyseggen 75fb2da7c2 Merge pull request #57 from specklesystems/izzy/obj-fix
🧺 Trash the workarounds in ops and serialiser for `speckle_type` vs `speckleType`
2021-02-12 11:33:58 +00:00
izzy lyseggen d760fa47ec refactor(base): more cleanup 2021-02-12 11:32:38 +00:00
izzy lyseggen e61968d88c refactor(ops): remove this nonsense lol
related to specklesystems
/
speckle-server#78

fixed in specklesystems/speckle-server@aca61b8
2021-02-12 10:57:56 +00:00
izzy lyseggen 01e3e5aa47 refactor(serializer): remove speckleType check
related to specklesystems
/
speckle-server#78

fixed in specklesystems/speckle-server@aca61b8
2021-02-12 10:57:35 +00:00
Gergő Jedlicska 7f2c26eb6b refactor(test_base.py): refactor base object invalid prop tests
Utilizing a few pytest features to make invalid prop tests a bit sleeker
2021-02-11 22:04:20 +01:00
Gergő Jedlicska 5f35da3be9 style(black config): added explicit black config to pyproject.toml 2021-02-11 21:43:28 +01:00
Gergő Jedlicska 98c3b89d75 Merge branch 'main' of github.com:specklesystems/speckle-py into base_type_registration 2021-02-11 20:35:22 +01:00
Gergő Jedlicska 5796ff6cdb docs(example/base_registrar.py): add example implementation for SpeckleBase class register
re #50
2021-02-11 20:34:35 +01:00
izzy lyseggen df4706aa56 Merge pull request #56 from specklesystems/revert-54-delete_requirements
Revert "Repo housekeeping"
2021-02-11 09:15:56 +00:00
izzy lyseggen 855d499eb8 Revert "Repo housekeeping" 2021-02-11 09:13:54 +00:00
izzy lyseggen a19ce15168 Merge pull request #55 from gjedlicska/base_validation_cleanup
Base object code cleanup
2021-02-11 09:09:50 +00:00
izzy lyseggen b2d89cad21 Merge pull request #54 from gjedlicska/delete_requirements
Repo housekeeping
2021-02-11 08:45:58 +00:00
Gergő Jedlicska f6c28689ce refactor(base.py): simplify some base object methods 2021-02-11 09:35:13 +01:00
Gergő Jedlicska 5b3076b775 Merge branch 'main' of github.com:specklesystems/speckle-py into delete_requirements 2021-02-11 09:22:43 +01:00
Gergő Jedlicska 16511f8dc0 chore(.gitignore): ignore .vscode folder 2021-02-11 09:19:30 +01:00
Gergő Jedlicska 8a673a54a9 chore(.vscode): remove .vscode folder, this should not be part of version control 2021-02-11 09:18:13 +01:00
izzy lyseggen f98b40cce3 Merge pull request #47 from specklesystems/izzy/doc-fix
docs: small config option for poetry venvs
2021-02-11 09:10:20 +01:00
izzy lyseggen 3a76358557 Merge pull request #48 from specklesystems/izzy/base-validation
☑ Base Attribute Name Validation
2021-02-09 09:51:56 +00:00
izzy lyseggen b6b25f824e test(base): tests for prop validaton 2021-02-05 11:23:25 +00:00
izzy lyseggen aa032895b6 feat(base): add prop validation
addresses #43
2021-02-05 11:23:03 +00:00
izzy lyseggen 6f871f0523 chore: add test launch config 2021-02-05 11:22:25 +00:00
izzy lyseggen 5e7b3712b7 Merge pull request #47 from specklesystems/izzy/doc-fix
docs: small config option for poetry venvs
2021-02-04 16:35:16 +00:00
izzy lyseggen 1db0fca68c docs: small config option for poetry venvs 2021-02-04 16:34:41 +00:00
izzy lyseggen dff3496712 Merge pull request #46 from gjedlicska/python_poetry
Switch to pyproject.toml and Poetry
2021-02-04 16:22:22 +00:00
Gergő Jedlicska dbcb691fbc chore(merge remote master): merged remote main branch. Added pytest-ordering 2021-01-31 10:29:11 +01:00
Gergő Jedlicska 4572d15b10 Merge remote-tracking branch 'upstream/main' into python_poetry 2021-01-31 10:05:13 +01:00
Gergő Jedlicska 779c4e6d70 docs(readme.md): replace Developing & Debugging with poetry instructions
re #44
2021-01-31 07:18:03 +01:00
Gergő Jedlicska 12ba5b347e feat(dependecy management): add basic pyproject.toml definition with python-poetry
This should be compatible with the current requirements.txt dependency definition.

re #44
2021-01-30 18:08:58 +01:00
izzy lyseggen 8dc3355f51 Merge pull request #42 from specklesystems/izzy/source-app
fix(commit): sourceapp change pyspeckle to python
2021-01-27 16:30:08 +00:00
izzy lyseggen 9e095e8673 fix(commit): sourceapp change pyspeckle to python
to be consistent with the other connectors
2021-01-27 16:27:04 +00:00
izzy lyseggen 089c10facd Merge pull request #41 from specklesystems/izzy/point-example
🔹 Implement `Point` example class (from core objects)
2021-01-26 11:53:25 +00:00
izzy lyseggen 236d53f78a feat(obj): finish point example 2021-01-26 11:49:36 +00:00
izzy lyseggen 257c32665e Merge pull request #40 from specklesystems/izzy/base-props
😇 Base Object Improvements
2021-01-26 10:37:04 +00:00
izzy lyseggen 82f7ee6f0a test(objects&serialisation): new base features 2021-01-26 10:04:00 +00:00
izzy lyseggen 748faf277d feat(serialiser): check defined detachable props 2021-01-26 10:02:55 +00:00
izzy lyseggen 90f5e7602c feat(base): add units prop with helper setter 2021-01-26 10:01:45 +00:00
izzy lyseggen 6b4dc9a6b2 fix(object): step into 'data' in get response 2021-01-26 09:53:56 +00:00
izzy lyseggen efa8c46b61 fix(receive): step into 'data' for server receive 2021-01-26 09:53:26 +00:00
izzy lyseggen 9527425c38 Merge pull request #39 from specklesystems/izzy/local-db
🗃 Default to SQLiteTransport as local transport
2021-01-21 15:38:08 +00:00
izzy lyseggen 6ab9252398 test(serialisation): both sqlite & server receive 2021-01-21 15:33:06 +00:00
izzy lyseggen 661c25c8fb test: smol tidy 2021-01-21 15:24:02 +00:00
izzy lyseggen 28ee9ec201 refactor(transports): comment out sqlite wip 2021-01-21 15:23:38 +00:00
izzy lyseggen 6155962bd8 feat(operations): swap out mem for sqlite transport 2021-01-21 15:23:24 +00:00
izzy lyseggen 607ad2c721 fix(models): make ServerInfo fields optional 2021-01-21 15:22:23 +00:00
izzy lyseggen 7866e1082e Merge pull request #38 from specklesystems/izzy/gql-stuff
🕸 More GQL Queries!
2021-01-21 12:16:10 +00:00
izzy lyseggen 0bd4b3b093 refactor(stream): remove unused importts 2021-01-21 12:10:59 +00:00
izzy lyseggen 94f47eeb17 test(user): user provile updates 2021-01-21 12:10:44 +00:00
izzy lyseggen 11fee54d36 feat(user): user update 2021-01-21 12:09:50 +00:00
izzy lyseggen b6943bbe6f test(server): api token create and revoke 2021-01-21 11:54:45 +00:00
izzy lyseggen 8b25fa1470 feat(server): api token create and revoke 2021-01-21 11:54:36 +00:00
izzy lyseggen 617e202a02 refactor(credentials): move ServerInfo to models 2021-01-21 11:33:18 +00:00
izzy lyseggen 76da3cacfb test(server): server get and apps 2021-01-21 11:32:46 +00:00
izzy lyseggen b3e9f425c0 feat(server): parse ServerInfo response 2021-01-21 11:32:37 +00:00
izzy lyseggen 739837bad2 test: reoder test execution 2021-01-21 11:07:27 +00:00
izzy lyseggen 0110b5b11a test(user): get and search 2021-01-21 11:07:08 +00:00
izzy lyseggen 0d31337350 test(stream): grant and revoke permissions 2021-01-21 11:06:01 +00:00
izzy lyseggen 12eb88819e test: add additional test user 2021-01-21 11:05:39 +00:00
izzy lyseggen 3d7d138841 feat(stream): grant and revoke permissions 2021-01-21 11:04:27 +00:00
izzy lyseggen 60bc4df16a test(commit): list query 2021-01-21 09:42:48 +00:00
izzy lyseggen d99c05761d test(branch): update and delete queries 2021-01-21 09:38:59 +00:00
izzy lyseggen dc326e6c6b feat(branch): update and delete queries 2021-01-21 09:38:47 +00:00
izzy lyseggen eb10e93300 fix(branch): adjust list query 2021-01-21 09:20:08 +00:00
izzy lyseggen 64434cd7f5 test(branch): get and list queries 2021-01-21 09:16:11 +00:00
izzy lyseggen 35a121abd9 feat(branch): get and list queries 2021-01-21 09:16:11 +00:00
izzy lyseggen fe5e7b19b0 Merge pull request #37 from specklesystems/izzy/accounts-fix
fix(credentials): adjust defs for py3.6 compat
2021-01-21 09:14:21 +00:00
izzy lyseggen adae615e29 fix(credentials): adjust defs for py3.6 compat 2021-01-21 09:12:26 +00:00
izzy lyseggen 203230fd8b Merge pull request #36 from specklesystems/izzy/ci-matrix-tests
🤖 ci: run tests for py 3.6 - 3.9
2021-01-20 17:03:24 +00:00
izzy lyseggen 64df2f241c ci: run tests for py 3.6 - 3.9 2021-01-20 16:57:49 +00:00
izzy lyseggen 110f10623e Merge pull request #35 from specklesystems/izzy/3.6-compat
fix: remove annotations for py 3.6 compatibility
2021-01-20 16:55:40 +00:00
izzy lyseggen 4dda9b8fe5 fix: remove annotations for py 3.6 compatibility 2021-01-20 16:53:48 +00:00
izzy lyseggen f494a663de Merge pull request #34 from specklesystems/izzy/ci
 setting up ci: take 2 
2021-01-19 17:22:26 +00:00
izzy lyseggen ff44f4ecb1 test: oops forgot to switch to testing.speckle.dev 2021-01-19 17:20:22 +00:00
izzy lyseggen 8da5086419 ci: upgrade pip and remove double pytest 2021-01-19 17:06:26 +00:00
izzy lyseggen f46dd7e0c6 ci: remove test args 2021-01-19 17:03:30 +00:00
izzy lyseggen dac3a86bcc ci: i'm dumb and spelled config wrong 2021-01-19 17:01:29 +00:00
izzy lyseggen c7a603ad45 Merge pull request #33 from specklesystems/revert-32-izzy/ci
Revert "setting up ci"
2021-01-19 17:00:24 +00:00
izzy lyseggen aaaf323b43 Revert "setting up ci" 2021-01-19 16:58:20 +00:00
izzy lyseggen 0173e76c76 Merge pull request #32 from specklesystems/izzy/ci
setting up ci
2021-01-19 16:57:35 +00:00
izzy lyseggen 3c2a437f98 ci: not sure what i'm doing tbh 2021-01-19 16:50:08 +00:00
izzy lyseggen 282623ce10 Merge pull request #31 from specklesystems/izzy/tests
🧫 Groundwork for testing: GQL queries and serialisation
2021-01-19 16:02:27 +00:00
izzy lyseggen 0409f2c0f3 test(object): create and get 2021-01-19 15:57:08 +00:00
izzy lyseggen 12528205f5 test(branch): test create 2021-01-19 15:56:55 +00:00
izzy lyseggen 063191e961 docs(object): update docstring 2021-01-19 15:56:38 +00:00
izzy lyseggen f9da5422c6 fix(branch): specify create return type 2021-01-19 15:55:52 +00:00
izzy lyseggen 41e66dda0d chore: requirements & formatting 2021-01-19 12:01:05 +00:00
izzy lyseggen 5eee3fee07 TEMPfix(serialisation): get around server bug
will revert when fix available for specklesystems/speckle-server#78
2021-01-19 11:05:42 +00:00
izzy lyseggen e14c42dfd1 test(serialisation): they work, yo 🥳 2021-01-19 11:04:34 +00:00
izzy lyseggen 062808c568 test: move fixtures to conf for sharing 2021-01-19 11:03:03 +00:00
izzy lyseggen 8b9f2db176 test: refactor into classes to use ordering 2021-01-19 11:02:11 +00:00
izzy lyseggen 5b458c630e test(commits): cleanup 2021-01-19 09:30:18 +00:00
izzy lyseggen 25c4b35eb3 test(serialisation): basic and chunking 2021-01-19 09:30:18 +00:00
izzy lyseggen 21b1b2c30a test(commits): scaffold tests 2021-01-19 09:30:18 +00:00
izzy lyseggen 1a063bbbf7 test(streams): small fixes 2021-01-19 09:30:18 +00:00
izzy lyseggen beb550624e test(streams): client stream operations 2021-01-19 09:30:18 +00:00
izzy lyseggen 50c6e2c840 test(config): set up tests with local server 2021-01-19 09:30:18 +00:00
izzy lyseggen 3f986fced3 Merge pull request #30 from specklesystems/izzy/dynamic-chunking
🧨 Dynamic prop chunking
2021-01-19 09:29:28 +00:00
izzy lyseggen ca234536bc feat(chunking): dynamic chunking with @() syntax
closes #24
2021-01-19 09:28:12 +00:00
izzy lyseggen f155129f7c feat(base): dynamic chunk default (1000) 2021-01-19 09:26:42 +00:00
izzy lyseggen 37d628d27b Merge pull request #29 from specklesystems/izzy/commit-queries
🧜‍♀️ Update Commit Models & Queries
2021-01-18 15:45:49 +00:00
izzy lyseggen 7b6e0aa6ed feat(commits): update queries and models 2021-01-18 15:43:58 +00:00
izzy lyseggen 2d449a328b Merge pull request #28 from specklesystems/izzy/base-props
🥠 Support for properties
2021-01-18 15:28:12 +00:00
izzy lyseggen ad47f649db feat(serializer): support deserialising of props 2021-01-18 15:27:43 +00:00
izzy lyseggen 750657db19 feat(base): bypass setattr for prop setters 2021-01-18 15:27:29 +00:00
izzy lyseggen 453a2e4211 Merge pull request #27 from specklesystems/izzy/smol-fixes
docs(objects): update docstring & add example obj
2021-01-18 11:44:22 +00:00
izzy lyseggen 2ca184538d docs(objects): update docstring & add example obj 2021-01-18 11:43:19 +00:00
Matteo Cominetti 79a832360b Merge pull request #22 from specklesystems/docs
feat: removes links to slack
2021-01-06 16:49:06 +00:00
Matteo Cominetti b2945ad3ff feat: removes links to slack 2021-01-06 16:48:45 +00:00
59 changed files with 3598 additions and 631 deletions
+69
View File
@@ -0,0 +1,69 @@
version: 2.1
orbs:
python: circleci/python@1.3.2
jobs:
test:
docker:
- image: "cimg/python:<<parameters.tag>>"
- image: "circleci/node:12"
- image: "circleci/redis:6"
- image: "circleci/postgres:12"
environment:
POSTGRES_DB: speckle2_test
POSTGRES_PASSWORD: speckle
POSTGRES_USER: speckle
- image: "speckle/speckle-server"
command: ["bash", "-c", "/wait && node bin/www"]
environment:
POSTGRES_URL: "localhost"
POSTGRES_USER: "speckle"
POSTGRES_PASSWORD: "speckle"
POSTGRES_DB: "speckle2_test"
REDIS_URL: "redis://localhost"
SESSION_SECRET: "keyboard cat"
STRATEGY_LOCAL: "true"
CANONICAL_URL: "http://localhost:3000"
WAIT_HOSTS: localhost:5432, localhost:6379
parameters:
tag:
default: "3.8"
type: string
steps:
- checkout
- run: python --version
- run:
command: python -m pip install --upgrade pip
name: upgrade pip
- python/install-packages:
pkg-manager: poetry
- run: poetry run pytest
deploy:
docker:
- image: "circleci/python:3.8"
steps:
- checkout
- run: python patch_version.py $CIRCLE_TAG
- run: poetry build
- run: poetry publish -u specklesystems -p $PYPI_PASSWORD
workflows:
main:
jobs:
- test:
matrix:
parameters:
tag: ["3.6", "3.7", "3.8", "3.9"]
filters:
tags:
only: /.*/
- deploy:
requires:
- test
filters:
tags:
only: /[0-9]+(\.[0-9]+)*/
branches:
ignore: /.*/ # For testing only! /ci\/.*/
+12 -11
View File
@@ -1,4 +1,5 @@
# Speckle Contribution Guidelines
## Introduction
Thank you for reading this! Speckle's a rather wide network of parts that depend on each other, either directly, indirectly or even just cosmetically.
@@ -9,41 +10,41 @@ This means that what might look like a simple quick change in one repo may have
## Bugs & Issues 🐞
### Found a new bug?
### Found a new bug?
- First step is to check whether this is a new bug! We encourage you to search through the issues of the project in question **and** associated repos!
- If you come up with nothing, **open a new issue with a clear title and description**, as much relevant information as possible: system configuration, code samples & steps to reproduce the problem.
- If you come up with nothing, **open a new issue with a clear title and description**, as much relevant information as possible: system configuration, code samples & steps to reproduce the problem.
- Can't mention this often enough: tells us how to reproduce the problem! We will ignore or flag as such issues without reproduction steps.
- Can't mention this often enough: tells us how to reproduce the problem! We will ignore or flag as such issues without reproduction steps.
- Try to reference & note all potentially affected projects.
### Sending a PR for Bug Fixes
You fixed something! Great! We hope you logged it first :) Make sure though that you've covered the lateral thinking needed for a bug report, as described above, also in your implementation! If there any tests, make sure they all pass. If there are none, it means they're missing - so add them!
You fixed something! Great! We hope you logged it first :) Make sure though that you've covered the lateral thinking needed for a bug report, as described above, also in your implementation! If there any tests, make sure they all pass. If there are none, it means they're missing - so add them!
## New Features 🎉
The golden rule is to Discuss First!
- Before embarking on adding a new feature, suggest it first as an issue with the `enhancement` label and/or title - this will allow relevant people to pitch in
- We'll now discuss your requirements and see how and if they fit within the Speckle ecosystem.
- The last step is to actually start writing code & submit a PR so we can follow along!
- All new features should, if and where possible, come with tests. We won't merge without!
- We'll now discuss your requirements and see how and if they fit within the Speckle ecosystem.
- The last step is to actually start writing code & submit a PR so we can follow along!
- All new features should, if and where possible, come with tests. We won't merge without!
> Many clients may potentially have overlapping scopes, some features might already be in dev somewhere else, or might have been postponed to the next major release due to api instability in that area. For example, adding a delete stream button in the accounts panel in rhino: this feature was planned for speckle admin, and the whole functionality of the accounts panel in rhino is to be greatly reduced!
## Cosmetic Patches ✨
Changes that are cosmetic in nature and do not add anything substantial to the stability or functionality of Speckle **will generally not be accepted**.
Changes that are cosmetic in nature and do not add anything substantial to the stability or functionality of Speckle **will generally not be accepted**.
Why? However trivial the changes might seem, there might be subtle reasons for the original code to be as it is. Furthermore, there are a lot of potential hidden costs (that even maintainers themselves are not aware of fully!) and they eat up review time unncessarily.
> **Examples**: modifying the colour of an UI element in one client may have a big hidden cost and need propagation in several other clients that implement a similar ui element. Changing the default port or specifiying `localhost` instead of `0.0.0.0` breaks cross-vm debugging and developing.
> **Examples**: modifying the colour of an UI element in one client may have a big hidden cost and need propagation in several other clients that implement a similar ui element. Changing the default port or specifiying `localhost` instead of `0.0.0.0` breaks cross-vm debugging and developing.
## Wrap up
Don't worry if you get things wrong. We all do, including project owners: this document should've been here a long time ago. There's plenty of room for discussion either on our community [forum](https://discourse.speckle.works) or [chat](https://speckle-works.slack.com/join/shared_invite/enQtNjY5Mzk2NTYxNTA4LTU4MWI5ZjdhMjFmMTIxZDIzOTAzMzRmMTZhY2QxMmM1ZjVmNzJmZGMzMDVlZmJjYWQxYWU0MWJkYmY3N2JjNGI).
Don't worry if you get things wrong. We all do, including project owners: this document should've been here a long time ago. There's plenty of room for discussion on our community [forum](https://discourse.speckle.works).
🙌❤️💙💚💜🙌
+6
View File
@@ -11,6 +11,12 @@
"program": "${file}",
"console": "integratedTerminal",
"justMyCode": false
},
{
"name": "Python: Test debug config",
"type": "python",
"request": "test",
"console": "integratedTerminal",
}
]
}
+36 -25
View File
@@ -1,45 +1,51 @@
# speckle-py 🥧
[![Twitter Follow](https://img.shields.io/twitter/follow/SpeckleSystems?style=social)](https://twitter.com/SpeckleSystems) [![Discourse users](https://img.shields.io/discourse/users?server=https%3A%2F%2Fdiscourse.speckle.works&style=flat-square)](https://discourse.speckle.works)
[![Slack Invite](https://img.shields.io/badge/-slack-grey?style=flat-square&logo=slack)](https://speckle-works.slack.com/join/shared_invite/enQtNjY5Mzk2NTYxNTA4LTU4MWI5ZjdhMjFmMTIxZDIzOTAzMzRmMTZhY2QxMmM1ZjVmNzJmZGMzMDVlZmJjYWQxYWU0MWJkYmY3N2JjNGI) [![website](https://img.shields.io/badge/www-speckle.systems-royalblue?style=flat-square)](https://speckle.systems)
[![Twitter Follow](https://img.shields.io/twitter/follow/SpeckleSystems?style=social)](https://twitter.com/SpeckleSystems) [![Community forum users](https://img.shields.io/discourse/users?server=https%3A%2F%2Fdiscourse.speckle.works&style=flat-square&logo=discourse&logoColor=white)](https://discourse.speckle.works) [![website](https://img.shields.io/badge/https://-speckle.systems-royalblue?style=flat-square)](https://speckle.systems) [![docs](https://img.shields.io/badge/docs-speckle.guide-orange?style=flat-square&logo=read-the-docs&logoColor=white)](https://speckle.guide/dev/)
## Introduction
> ⚠ This is the start of the Python client for Speckle 2.0. It is currently quite nebulous and may be trashed and rebuilt at any moment! It is compatible with Python 3.6+ ⚠
>
## Documentation
Comprehensive developer and user documentation can be found in our:
#### 📚 [Speckle Docs website](https://speckle.guide/dev/)
## Developing & Debugging
To get started, create a virtual environment and pip install the requirements.
### Installation
on windows:
```
python -m venv venv
venv\Scripts\activate
pip install -r requirements.txt
```
This project uses python-poetry for dependency management, make sure you follow the official [docs](https://python-poetry.org/docs/#installation) to get poetry.
on mac:
To bootstrap the project environment run `$ poetry install`. This will create a new virtual-env for the project and install both the package and dev dependencies.
```
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
If this is your first time using poetry and you're used to creating your venvs within the project directory, run `poetry config virtualenvs.in-project true` to configure poetry to do the same.
To execute any python script run `$ poetry run python my_script.py`
> Alternatively you may roll your own virtual-env with either venv, virtualenv, pyenv-virtualenv etc. Poetry will play along an recognize if it is invoked from inside a virtual environment.
### Local Data Paths
It may be helpful to know where the local accounts and object cache dbs are stored. Depending on on your OS, you can find the dbs at:
- Windows: `APPDATA` or `<USER>\AppData\Roaming\Speckle`
- Linux: `$XDG_DATA_HOME` or by default `~/.local/share/Speckle`
- Mac: `~/.config/Speckle`
## Overview of functionality
The `SpeckleClient` is the entry point for interacting with the GraphQL API. You'll need to have a running server to use this.
```py
from speckle.api.client import SpeckleClient
from speckle.api.credentials import get_default_account, get_local_accounts
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import get_default_account, get_local_accounts
all_accounts = get_local_accounts() # get back a list
account = get_default_account()
client = SpeckleClient(host="localhost:3000", use_ssl=False)
client = SpeckleClient(host="speckle.xyz")
# client = SpeckleClient(host="yourserver.com") or whatever your host is
client.authenticate(account.token)
@@ -80,6 +86,9 @@ deleted = client.commit.delete("stream id", "commit id")
The `BaseObjectSerializer` is used for decomposing and serializing `Base` objects so they can be sent / received to the server. You can use it directly to get the id (hash) and a serializable object representation of the decomposed `Base`. You can learn more about the Speckle `Base` object [here](https://discourse.speckle.works/t/core-2-0-the-base-object/782) and the decomposition API [here](https://discourse.speckle.works/t/core-2-0-decomposition-api/911).
```py
from specklepy.objects.base import Base
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
detached_base = Base()
detached_base.name = "this will get detached"
@@ -94,9 +103,12 @@ hash, obj_dict = serializer.traverse_base(base_obj)
If you use the `operations`, you will not need to interact with the serializer directly as this will be taken care of for you. You will just need to provide a transport to indicate where the objects should be sent / received from. At the moment, just the `MemoryTransport` and the `ServerTransport` are fully functional at the moment. If you'd like to learn more about Transports in Speckle 2.0, have a look [here](https://discourse.speckle.works/t/core-2-0-transports/919).
```py
from specklepy.transports.memory import MemoryTransport
from specklepy.api import operations
transport = MemoryTransport()
# this serialises the object and sends it to the transport
# this serialises the object and sends it to the transport
hash = operations.send(base=base_obj, transports=[transport])
# if the object had detached objects, you can see these as well
@@ -114,13 +126,13 @@ test_base = Base()
test_base.testing = "a test base obj"
# run it through the serialiser
s = BaseObjectSerializer()
s = BaseObjectSerializer()
hash, obj = s.traverse_base(test_base)
# send it to the server
objCreate = client.object.create(stream_id="stream id", objects=[obj])
received_base = client.object.get(hash)
received_base = client.object.get("stream id", hash)
```
This doc is not complete - there's more to see so have a dive into the code and play around! Please feel free to provide feedback, submit issues, or discuss new features ✨
@@ -131,12 +143,11 @@ Please make sure you read the [contribution guidelines](.github/CONTRIBUTING.md)
## Community
The Speckle Community hangs out in two main places, usually:
The Speckle Community hangs out on [the forum](https://discourse.speckle.works), do join and introduce yourself & feel free to ask us questions!
- on [the forum](https://discourse.speckle.works)
- on [the chat](https://speckle-works.slack.com/join/shared_invite/enQtNjY5Mzk2NTYxNTA4LTU4MWI5ZjdhMjFmMTIxZDIzOTAzMzRmMTZhY2QxMmM1ZjVmNzJmZGMzMDVlZmJjYWQxYWU0MWJkYmY3N2JjNGI)
## Security
Do join and introduce yourself!
For any security vulnerabilities or concerns, please contact us directly at security[at]speckle.systems.
## License
+59
View File
@@ -0,0 +1,59 @@
"""This is an example showcasing the usage of speckle `Base` class."""
# the speckle.objects module exposes all speckle provided classes
from specklepy.objects import Base
from specklepy.api import operations
from devtools import debug
class ExampleSub(Base):
"""
Inheriting from `Base` is done with in the standard way by default.
The syntax is similar to the stdlib dataclass syntax.
No __init__ method definition is required, that is done automatically by the base
type. Also the attributes defined this way are instance attributes despite they
might look like class attributes.
The speckle Base uses the pydantic BaseModel in the background, but ideally that
is not the consumers concern.
**Important note:** currently the way how serialization works, requires
each attribute to have a valid default value, just like `foo` has. This includes
default values for all primitives and complex datastructures.
Failing to provide a default, breaks the receiving end of the transport.
"""
foo: str = "bar"
class SpeckleSub(ExampleSub, speckle_type="custom_speckle_sub"):
"""
Example custom type name registration.
This is an optional feature.
The default value of the speckle_type is generated from the name of the class, but
optionally it may be overridden. This is useful, since the speckle_type has to be
unique for each subclass of speckle Base.
"""
magic: str = "trick"
if __name__ == "__main__":
# example usage
custom_sub = SpeckleSub(
foo=123,
magic="trick",
bar="baric",
extra=123,
)
# support for dynamic attributes
custom_sub.extra_extra = "what is this?"
debug(custom_sub.json())
serialized = operations.serialize(custom_sub)
deserialized = operations.deserialize(serialized)
# the only difference should be between the two data is that the deserialized
# instance id attribute is not None.
debug(deserialized.json())
+31
View File
@@ -0,0 +1,31 @@
import re
import sys
def patch(tag):
print(f"Patching version: {tag}")
with open("pyproject.toml", "r") as f:
lines = f.readlines()
if "version" not in lines[2]:
raise Exception(f"Invalid pyproject.toml. Could not patch version.")
lines[2] = f'version = "{tag}"\n'
with open("pyproject.toml", "w") as file:
file.writelines(lines)
def main():
if len(sys.argv) < 2:
return
tag = sys.argv[1]
if not re.match(r"[0-9]+(\.[0-9]+)*$", tag):
raise ValueError(f"Invalid tag provided: {tag}")
patch(tag)
if __name__ == "__main__":
main()
Generated
+795
View File
@@ -0,0 +1,795 @@
[[package]]
name = "aiohttp"
version = "3.7.4.post0"
description = "Async http client/server framework (asyncio)"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
async-timeout = ">=3.0,<4.0"
attrs = ">=17.3.0"
chardet = ">=2.0,<5.0"
idna-ssl = {version = ">=1.0", markers = "python_version < \"3.7\""}
multidict = ">=4.5,<7.0"
typing-extensions = ">=3.6.5"
yarl = ">=1.0,<2.0"
[package.extras]
speedups = ["aiodns", "brotlipy", "cchardet"]
[[package]]
name = "appdirs"
version = "1.4.4"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "async-timeout"
version = "3.0.1"
description = "Timeout context manager for asyncio programs"
category = "main"
optional = false
python-versions = ">=3.5.3"
[[package]]
name = "atomicwrites"
version = "1.4.0"
description = "Atomic file writes."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "attrs"
version = "20.3.0"
description = "Classes Without Boilerplate"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.extras]
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"]
docs = ["furo", "sphinx", "zope.interface"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"]
[[package]]
name = "black"
version = "20.8b1"
description = "The uncompromising code formatter."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
appdirs = "*"
click = ">=7.1.2"
dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""}
mypy-extensions = ">=0.4.3"
pathspec = ">=0.6,<1"
regex = ">=2020.1.8"
toml = ">=0.10.1"
typed-ast = ">=1.4.0"
typing-extensions = ">=3.7.4"
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
[[package]]
name = "certifi"
version = "2021.5.30"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "chardet"
version = "4.0.0"
description = "Universal encoding detector for Python 2 and 3"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "click"
version = "7.1.2"
description = "Composable command line interface toolkit"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "colorama"
version = "0.4.4"
description = "Cross-platform colored terminal text."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "dataclasses"
version = "0.8"
description = "A backport of the dataclasses module for Python 3.6"
category = "main"
optional = false
python-versions = ">=3.6, <3.7"
[[package]]
name = "gql"
version = "3.0.0a6"
description = "GraphQL client for Python"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
aiohttp = {version = ">=3.7.1,<3.8.0", optional = true, markers = "extra == \"all\""}
graphql-core = ">=3.1.5,<3.2"
requests = {version = ">=2.23,<3", optional = true, markers = "extra == \"all\""}
websockets = {version = ">=9,<10", optional = true, markers = "extra == \"all\""}
yarl = ">=1.6,<2.0"
[package.extras]
aiohttp = ["aiohttp (>=3.7.1,<3.8.0)"]
all = ["aiohttp (>=3.7.1,<3.8.0)", "requests (>=2.23,<3)", "websockets (>=9,<10)"]
dev = ["aiohttp (>=3.7.1,<3.8.0)", "requests (>=2.23,<3)", "websockets (>=9,<10)", "black (==19.10b0)", "check-manifest (>=0.42,<1)", "flake8 (==3.8.1)", "isort (==4.3.21)", "mypy (==0.770)", "sphinx (>=3.0.0,<4)", "sphinx_rtd_theme (>=0.4,<1)", "sphinx-argparse (==0.2.5)", "parse (==1.15.0)", "pytest (==5.4.2)", "pytest-asyncio (==0.11.0)", "pytest-cov (==2.8.1)", "mock (==4.0.2)", "vcrpy (==4.0.2)", "aiofiles"]
requests = ["requests (>=2.23,<3)"]
test = ["aiohttp (>=3.7.1,<3.8.0)", "requests (>=2.23,<3)", "websockets (>=9,<10)", "parse (==1.15.0)", "pytest (==5.4.2)", "pytest-asyncio (==0.11.0)", "pytest-cov (==2.8.1)", "mock (==4.0.2)", "vcrpy (==4.0.2)", "aiofiles"]
test_no_transport = ["parse (==1.15.0)", "pytest (==5.4.2)", "pytest-asyncio (==0.11.0)", "pytest-cov (==2.8.1)", "mock (==4.0.2)", "vcrpy (==4.0.2)", "aiofiles"]
websockets = ["websockets (>=9,<10)"]
[[package]]
name = "graphql-core"
version = "3.1.5"
description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL."
category = "main"
optional = false
python-versions = ">=3.6,<4"
[[package]]
name = "idna"
version = "2.10"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "idna-ssl"
version = "1.1.0"
description = "Patch ssl.match_hostname for Unicode(idna) domains support"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
idna = ">=2.0"
[[package]]
name = "importlib-metadata"
version = "3.4.0"
description = "Read metadata from Python packages"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
zipp = ">=0.5"
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
[[package]]
name = "iniconfig"
version = "1.1.1"
description = "iniconfig: brain-dead simple config-ini parsing"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "isort"
version = "5.7.0"
description = "A Python utility / library to sort Python imports."
category = "dev"
optional = false
python-versions = ">=3.6,<4.0"
[package.extras]
pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
requirements_deprecated_finder = ["pipreqs", "pip-api"]
colors = ["colorama (>=0.4.3,<0.5.0)"]
[[package]]
name = "multidict"
version = "5.1.0"
description = "multidict implementation"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "mypy-extensions"
version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "packaging"
version = "20.9"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies]
pyparsing = ">=2.0.2"
[[package]]
name = "pathspec"
version = "0.8.1"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "pluggy"
version = "0.13.1"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies]
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
[package.extras]
dev = ["pre-commit", "tox"]
[[package]]
name = "py"
version = "1.10.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pydantic"
version = "1.7.3"
description = "Data validation and settings management using python 3.6 type hinting"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""}
[package.extras]
dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"]
typing_extensions = ["typing-extensions (>=3.7.2)"]
[[package]]
name = "pyparsing"
version = "2.4.7"
description = "Python parsing module"
category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "pytest"
version = "6.2.2"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<1.0.0a1"
py = ">=1.8.2"
toml = "*"
[package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
[[package]]
name = "pytest-ordering"
version = "0.6"
description = "pytest plugin to run your tests in a specific order"
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
pytest = "*"
[[package]]
name = "regex"
version = "2020.11.13"
description = "Alternative regular expression module, to replace re."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "requests"
version = "2.25.1"
description = "Python HTTP for Humans."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.dependencies]
certifi = ">=2017.4.17"
chardet = ">=3.0.2,<5"
idna = ">=2.5,<3"
urllib3 = ">=1.21.1,<1.27"
[package.extras]
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
[[package]]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "typed-ast"
version = "1.4.2"
description = "a fork of Python 2 and 3 ast modules with type comment support"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "typing-extensions"
version = "3.7.4.3"
description = "Backported and Experimental Type Hints for Python 3.5+"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "urllib3"
version = "1.26.5"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
[package.extras]
brotli = ["brotlipy (>=0.6.0)"]
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
name = "websockets"
version = "9.1"
description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
category = "main"
optional = false
python-versions = ">=3.6.1"
[[package]]
name = "yarl"
version = "1.6.3"
description = "Yet another URL library"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
idna = ">=2.0"
multidict = ">=4.0"
typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
[[package]]
name = "zipp"
version = "3.4.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
[metadata]
lock-version = "1.1"
python-versions = "^3.6.5"
content-hash = "84e846c1bb02924ceada07406e95032e0632d229a36657ba2b85129e68f1526d"
[metadata.files]
aiohttp = [
{file = "aiohttp-3.7.4.post0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5"},
{file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8"},
{file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95"},
{file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290"},
{file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f"},
{file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809"},
{file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe"},
{file = "aiohttp-3.7.4.post0-cp36-cp36m-win32.whl", hash = "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287"},
{file = "aiohttp-3.7.4.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc"},
{file = "aiohttp-3.7.4.post0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87"},
{file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0"},
{file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970"},
{file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f"},
{file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde"},
{file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c"},
{file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8"},
{file = "aiohttp-3.7.4.post0-cp37-cp37m-win32.whl", hash = "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f"},
{file = "aiohttp-3.7.4.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5"},
{file = "aiohttp-3.7.4.post0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf"},
{file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df"},
{file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213"},
{file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4"},
{file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009"},
{file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5"},
{file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013"},
{file = "aiohttp-3.7.4.post0-cp38-cp38-win32.whl", hash = "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16"},
{file = "aiohttp-3.7.4.post0-cp38-cp38-win_amd64.whl", hash = "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5"},
{file = "aiohttp-3.7.4.post0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b"},
{file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd"},
{file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439"},
{file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22"},
{file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a"},
{file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb"},
{file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb"},
{file = "aiohttp-3.7.4.post0-cp39-cp39-win32.whl", hash = "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9"},
{file = "aiohttp-3.7.4.post0-cp39-cp39-win_amd64.whl", hash = "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe"},
{file = "aiohttp-3.7.4.post0.tar.gz", hash = "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf"},
]
appdirs = [
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
]
async-timeout = [
{file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"},
{file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"},
]
atomicwrites = [
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
]
attrs = [
{file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"},
{file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"},
]
black = [
{file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
]
certifi = [
{file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"},
{file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"},
]
chardet = [
{file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
{file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
]
click = [
{file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
{file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
]
colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
]
dataclasses = [
{file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"},
{file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"},
]
gql = [
{file = "gql-3.0.0a6.tar.gz", hash = "sha256:bdcbf60bc37b11d6d2f2ed271f69292c4e96d56df7000ba1dad52e487330bdce"},
]
graphql-core = [
{file = "graphql-core-3.1.5.tar.gz", hash = "sha256:a755635d1d364a17e8d270347000722351aaa03f1ab7d280878aae82fc68b1f3"},
{file = "graphql_core-3.1.5-py3-none-any.whl", hash = "sha256:91d96ef0e86665777bb7115d3bbb6b0326f43dc7dbcdd60da5486a27a50cfb11"},
]
idna = [
{file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
]
idna-ssl = [
{file = "idna-ssl-1.1.0.tar.gz", hash = "sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c"},
]
importlib-metadata = [
{file = "importlib_metadata-3.4.0-py3-none-any.whl", hash = "sha256:ace61d5fc652dc280e7b6b4ff732a9c2d40db2c0f92bc6cb74e07b73d53a1771"},
{file = "importlib_metadata-3.4.0.tar.gz", hash = "sha256:fa5daa4477a7414ae34e95942e4dd07f62adf589143c875c133c1e53c4eff38d"},
]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
isort = [
{file = "isort-5.7.0-py3-none-any.whl", hash = "sha256:fff4f0c04e1825522ce6949973e83110a6e907750cd92d128b0d14aaaadbffdc"},
{file = "isort-5.7.0.tar.gz", hash = "sha256:c729845434366216d320e936b8ad6f9d681aab72dc7cbc2d51bedc3582f3ad1e"},
]
multidict = [
{file = "multidict-5.1.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f"},
{file = "multidict-5.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf"},
{file = "multidict-5.1.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281"},
{file = "multidict-5.1.0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d"},
{file = "multidict-5.1.0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d"},
{file = "multidict-5.1.0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da"},
{file = "multidict-5.1.0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224"},
{file = "multidict-5.1.0-cp36-cp36m-win32.whl", hash = "sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26"},
{file = "multidict-5.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6"},
{file = "multidict-5.1.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76"},
{file = "multidict-5.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a"},
{file = "multidict-5.1.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f"},
{file = "multidict-5.1.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348"},
{file = "multidict-5.1.0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93"},
{file = "multidict-5.1.0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9"},
{file = "multidict-5.1.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37"},
{file = "multidict-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5"},
{file = "multidict-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632"},
{file = "multidict-5.1.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952"},
{file = "multidict-5.1.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79"},
{file = "multidict-5.1.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456"},
{file = "multidict-5.1.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7"},
{file = "multidict-5.1.0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635"},
{file = "multidict-5.1.0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a"},
{file = "multidict-5.1.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea"},
{file = "multidict-5.1.0-cp38-cp38-win32.whl", hash = "sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656"},
{file = "multidict-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3"},
{file = "multidict-5.1.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93"},
{file = "multidict-5.1.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647"},
{file = "multidict-5.1.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d"},
{file = "multidict-5.1.0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8"},
{file = "multidict-5.1.0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1"},
{file = "multidict-5.1.0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841"},
{file = "multidict-5.1.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda"},
{file = "multidict-5.1.0-cp39-cp39-win32.whl", hash = "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"},
{file = "multidict-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359"},
{file = "multidict-5.1.0.tar.gz", hash = "sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5"},
]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
packaging = [
{file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
{file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"},
]
pathspec = [
{file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"},
{file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"},
]
pluggy = [
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
]
py = [
{file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
{file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
]
pydantic = [
{file = "pydantic-1.7.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c59ea046aea25be14dc22d69c97bee629e6d48d2b2ecb724d7fe8806bf5f61cd"},
{file = "pydantic-1.7.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a4143c8d0c456a093387b96e0f5ee941a950992904d88bc816b4f0e72c9a0009"},
{file = "pydantic-1.7.3-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:d8df4b9090b595511906fa48deda47af04e7d092318bfb291f4d45dfb6bb2127"},
{file = "pydantic-1.7.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:514b473d264671a5c672dfb28bdfe1bf1afd390f6b206aa2ec9fed7fc592c48e"},
{file = "pydantic-1.7.3-cp36-cp36m-win_amd64.whl", hash = "sha256:dba5c1f0a3aeea5083e75db9660935da90216f8a81b6d68e67f54e135ed5eb23"},
{file = "pydantic-1.7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59e45f3b694b05a69032a0d603c32d453a23f0de80844fb14d55ab0c6c78ff2f"},
{file = "pydantic-1.7.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:5b24e8a572e4b4c18f614004dda8c9f2c07328cb5b6e314d6e1bbd536cb1a6c1"},
{file = "pydantic-1.7.3-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:b2b054d095b6431cdda2f852a6d2f0fdec77686b305c57961b4c5dd6d863bf3c"},
{file = "pydantic-1.7.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:025bf13ce27990acc059d0c5be46f416fc9b293f45363b3d19855165fee1874f"},
{file = "pydantic-1.7.3-cp37-cp37m-win_amd64.whl", hash = "sha256:6e3874aa7e8babd37b40c4504e3a94cc2023696ced5a0500949f3347664ff8e2"},
{file = "pydantic-1.7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e682f6442ebe4e50cb5e1cfde7dda6766fb586631c3e5569f6aa1951fd1a76ef"},
{file = "pydantic-1.7.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:185e18134bec5ef43351149fe34fda4758e53d05bb8ea4d5928f0720997b79ef"},
{file = "pydantic-1.7.3-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:f5b06f5099e163295b8ff5b1b71132ecf5866cc6e7f586d78d7d3fd6e8084608"},
{file = "pydantic-1.7.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:24ca47365be2a5a3cc3f4a26dcc755bcdc9f0036f55dcedbd55663662ba145ec"},
{file = "pydantic-1.7.3-cp38-cp38-win_amd64.whl", hash = "sha256:d1fe3f0df8ac0f3a9792666c69a7cd70530f329036426d06b4f899c025aca74e"},
{file = "pydantic-1.7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f6864844b039805add62ebe8a8c676286340ba0c6d043ae5dea24114b82a319e"},
{file = "pydantic-1.7.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ecb54491f98544c12c66ff3d15e701612fc388161fd455242447083350904730"},
{file = "pydantic-1.7.3-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:ffd180ebd5dd2a9ac0da4e8b995c9c99e7c74c31f985ba090ee01d681b1c4b95"},
{file = "pydantic-1.7.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8d72e814c7821125b16f1553124d12faba88e85405b0864328899aceaad7282b"},
{file = "pydantic-1.7.3-cp39-cp39-win_amd64.whl", hash = "sha256:475f2fa134cf272d6631072554f845d0630907fce053926ff634cc6bc45bf1af"},
{file = "pydantic-1.7.3-py3-none-any.whl", hash = "sha256:38be427ea01a78206bcaf9a56f835784afcba9e5b88fbdce33bbbfbcd7841229"},
{file = "pydantic-1.7.3.tar.gz", hash = "sha256:213125b7e9e64713d16d988d10997dabc6a1f73f3991e1ff8e35ebb1409c7dc9"},
]
pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
]
pytest = [
{file = "pytest-6.2.2-py3-none-any.whl", hash = "sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839"},
{file = "pytest-6.2.2.tar.gz", hash = "sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9"},
]
pytest-ordering = [
{file = "pytest-ordering-0.6.tar.gz", hash = "sha256:561ad653626bb171da78e682f6d39ac33bb13b3e272d406cd555adb6b006bda6"},
{file = "pytest_ordering-0.6-py2-none-any.whl", hash = "sha256:27fba3fc265f5d0f8597e7557885662c1bdc1969497cd58aff6ed21c3b617de2"},
{file = "pytest_ordering-0.6-py3-none-any.whl", hash = "sha256:3f314a178dbeb6777509548727dc69edf22d6d9a2867bf2d310ab85c403380b6"},
]
regex = [
{file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6"},
{file = "regex-2020.11.13-cp36-cp36m-win32.whl", hash = "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e"},
{file = "regex-2020.11.13-cp36-cp36m-win_amd64.whl", hash = "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884"},
{file = "regex-2020.11.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538"},
{file = "regex-2020.11.13-cp37-cp37m-win32.whl", hash = "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4"},
{file = "regex-2020.11.13-cp37-cp37m-win_amd64.whl", hash = "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444"},
{file = "regex-2020.11.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f"},
{file = "regex-2020.11.13-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d"},
{file = "regex-2020.11.13-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af"},
{file = "regex-2020.11.13-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f"},
{file = "regex-2020.11.13-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b"},
{file = "regex-2020.11.13-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8"},
{file = "regex-2020.11.13-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5"},
{file = "regex-2020.11.13-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b"},
{file = "regex-2020.11.13-cp38-cp38-win32.whl", hash = "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c"},
{file = "regex-2020.11.13-cp38-cp38-win_amd64.whl", hash = "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683"},
{file = "regex-2020.11.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc"},
{file = "regex-2020.11.13-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364"},
{file = "regex-2020.11.13-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e"},
{file = "regex-2020.11.13-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e"},
{file = "regex-2020.11.13-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917"},
{file = "regex-2020.11.13-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b"},
{file = "regex-2020.11.13-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9"},
{file = "regex-2020.11.13-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c"},
{file = "regex-2020.11.13-cp39-cp39-win32.whl", hash = "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f"},
{file = "regex-2020.11.13-cp39-cp39-win_amd64.whl", hash = "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d"},
{file = "regex-2020.11.13.tar.gz", hash = "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562"},
]
requests = [
{file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
{file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
]
toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
typed-ast = [
{file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"},
{file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"},
{file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"},
{file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"},
{file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"},
{file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"},
{file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"},
{file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"},
{file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"},
{file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"},
{file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"},
{file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"},
{file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"},
{file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"},
{file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"},
{file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"},
{file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"},
{file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"},
{file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"},
{file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"},
{file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"},
{file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"},
{file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"},
{file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"},
{file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"},
{file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"},
{file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"},
{file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"},
{file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"},
{file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"},
]
typing-extensions = [
{file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},
{file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"},
{file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"},
]
urllib3 = [
{file = "urllib3-1.26.5-py2.py3-none-any.whl", hash = "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c"},
{file = "urllib3-1.26.5.tar.gz", hash = "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"},
]
websockets = [
{file = "websockets-9.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d144b350045c53c8ff09aa1cfa955012dd32f00c7e0862c199edcabb1a8b32da"},
{file = "websockets-9.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:b4ad84b156cf50529b8ac5cc1638c2cf8680490e3fccb6121316c8c02620a2e4"},
{file = "websockets-9.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2cf04601633a4ec176b9cc3d3e73789c037641001dbfaf7c411f89cd3e04fcaf"},
{file = "websockets-9.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:5c8f0d82ea2468282e08b0cf5307f3ad022290ed50c45d5cb7767957ca782880"},
{file = "websockets-9.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:caa68c95bc1776d3521f81eeb4d5b9438be92514ec2a79fececda814099c8314"},
{file = "websockets-9.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d2c2d9b24d3c65b5a02cac12cbb4e4194e590314519ed49db2f67ef561c3cf58"},
{file = "websockets-9.1-cp36-cp36m-win32.whl", hash = "sha256:f31722f1c033c198aa4a39a01905951c00bd1c74f922e8afc1b1c62adbcdd56a"},
{file = "websockets-9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:3ddff38894c7857c476feb3538dd847514379d6dc844961dc99f04b0384b1b1b"},
{file = "websockets-9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:51d04df04ed9d08077d10ccbe21e6805791b78eac49d16d30a1f1fe2e44ba0af"},
{file = "websockets-9.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f68c352a68e5fdf1e97288d5cec9296664c590c25932a8476224124aaf90dbcd"},
{file = "websockets-9.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:b43b13e5622c5a53ab12f3272e6f42f1ce37cd5b6684b2676cb365403295cd40"},
{file = "websockets-9.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:9147868bb0cc01e6846606cd65cbf9c58598f187b96d14dd1ca17338b08793bb"},
{file = "websockets-9.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:836d14eb53b500fd92bd5db2fc5894f7c72b634f9c2a28f546f75967503d8e25"},
{file = "websockets-9.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:48c222feb3ced18f3dc61168ca18952a22fb88e5eb8902d2bf1b50faefdc34a2"},
{file = "websockets-9.1-cp37-cp37m-win32.whl", hash = "sha256:900589e19200be76dd7cbaa95e9771605b5ce3f62512d039fb3bc5da9014912a"},
{file = "websockets-9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ab5ee15d3462198c794c49ccd31773d8a2b8c17d622aa184f669d2b98c2f0857"},
{file = "websockets-9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:85e701a6c316b7067f1e8675c638036a796fe5116783a4c932e7eb8e305a3ffe"},
{file = "websockets-9.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b2e71c4670ebe1067fa8632f0d081e47254ee2d3d409de54168b43b0ba9147e0"},
{file = "websockets-9.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:230a3506df6b5f446fed2398e58dcaafdff12d67fe1397dff196411a9e820d02"},
{file = "websockets-9.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:7df3596838b2a0c07c6f6d67752c53859a54993d4f062689fdf547cb56d0f84f"},
{file = "websockets-9.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:826ccf85d4514609219725ba4a7abd569228c2c9f1968e8be05be366f68291ec"},
{file = "websockets-9.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:0dd4eb8e0bbf365d6f652711ce21b8fd2b596f873d32aabb0fbb53ec604418cc"},
{file = "websockets-9.1-cp38-cp38-win32.whl", hash = "sha256:1d0971cc7251aeff955aa742ec541ee8aaea4bb2ebf0245748fbec62f744a37e"},
{file = "websockets-9.1-cp38-cp38-win_amd64.whl", hash = "sha256:7189e51955f9268b2bdd6cc537e0faa06f8fffda7fb386e5922c6391de51b077"},
{file = "websockets-9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e9e5fd6dbdf95d99bc03732ded1fc8ef22ebbc05999ac7e0c7bf57fe6e4e5ae2"},
{file = "websockets-9.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9e7fdc775fe7403dbd8bc883ba59576a6232eac96dacb56512daacf7af5d618d"},
{file = "websockets-9.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:597c28f3aa7a09e8c070a86b03107094ee5cdafcc0d55f2f2eac92faac8dc67d"},
{file = "websockets-9.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:ad893d889bc700a5835e0a95a3e4f2c39e91577ab232a3dc03c262a0f8fc4b5c"},
{file = "websockets-9.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:1d6b4fddb12ab9adf87b843cd4316c4bd602db8d5efd2fb83147f0458fe85135"},
{file = "websockets-9.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:ebf459a1c069f9866d8569439c06193c586e72c9330db1390af7c6a0a32c4afd"},
{file = "websockets-9.1-cp39-cp39-win32.whl", hash = "sha256:be5fd35e99970518547edc906efab29afd392319f020c3c58b0e1a158e16ed20"},
{file = "websockets-9.1-cp39-cp39-win_amd64.whl", hash = "sha256:85db8090ba94e22d964498a47fdd933b8875a1add6ebc514c7ac8703eb97bbf0"},
{file = "websockets-9.1.tar.gz", hash = "sha256:276d2339ebf0df4f45df453923ebd2270b87900eda5dfd4a6b0cfa15f82111c3"},
]
yarl = [
{file = "yarl-1.6.3-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434"},
{file = "yarl-1.6.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478"},
{file = "yarl-1.6.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6"},
{file = "yarl-1.6.3-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e"},
{file = "yarl-1.6.3-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406"},
{file = "yarl-1.6.3-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76"},
{file = "yarl-1.6.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366"},
{file = "yarl-1.6.3-cp36-cp36m-win32.whl", hash = "sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721"},
{file = "yarl-1.6.3-cp36-cp36m-win_amd64.whl", hash = "sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643"},
{file = "yarl-1.6.3-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e"},
{file = "yarl-1.6.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3"},
{file = "yarl-1.6.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8"},
{file = "yarl-1.6.3-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a"},
{file = "yarl-1.6.3-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c"},
{file = "yarl-1.6.3-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f"},
{file = "yarl-1.6.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970"},
{file = "yarl-1.6.3-cp37-cp37m-win32.whl", hash = "sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e"},
{file = "yarl-1.6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50"},
{file = "yarl-1.6.3-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2"},
{file = "yarl-1.6.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec"},
{file = "yarl-1.6.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"},
{file = "yarl-1.6.3-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc"},
{file = "yarl-1.6.3-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959"},
{file = "yarl-1.6.3-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2"},
{file = "yarl-1.6.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2"},
{file = "yarl-1.6.3-cp38-cp38-win32.whl", hash = "sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896"},
{file = "yarl-1.6.3-cp38-cp38-win_amd64.whl", hash = "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a"},
{file = "yarl-1.6.3-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e"},
{file = "yarl-1.6.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724"},
{file = "yarl-1.6.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c"},
{file = "yarl-1.6.3-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25"},
{file = "yarl-1.6.3-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96"},
{file = "yarl-1.6.3-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0"},
{file = "yarl-1.6.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4"},
{file = "yarl-1.6.3-cp39-cp39-win32.whl", hash = "sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424"},
{file = "yarl-1.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6"},
{file = "yarl-1.6.3.tar.gz", hash = "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10"},
]
zipp = [
{file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"},
{file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"},
]
+48
View File
@@ -0,0 +1,48 @@
[tool.poetry]
name = "specklepy"
version = "2.1.0"
description = "The Python SDK for Speckle 2.0"
readme = "README.md"
authors = ["Speckle Systems <devops@speckle.systems>"]
license = "Apache-2.0"
repository = "https://github.com/specklesystems/speckle-py"
documentation = "https://speckle.guide/dev/py-examples.html"
homepage = "https://speckle.systems/"
[tool.poetry.dependencies]
python = "^3.6.5"
pydantic = "^1.7.3"
appdirs = "^1.4.4"
gql = {version = ">=3.0.0a6", extras = ["all"], allow-prereleases = true}
[tool.poetry.dev-dependencies]
black = "^20.8b1"
isort = "^5.7.0"
pytest = "^6.2.2"
pytest-ordering = "^0.6"
[tool.black]
exclude = '''
/(
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
)/
'''
include = '\.pyi?$'
line-length = 88
target-version = ["py36", "py37", "py38"]
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
-31
View File
@@ -1,31 +0,0 @@
aiohttp==3.7.1
appdirs==1.4.4
astroid==2.4.2
async-timeout==3.0.1
attrs==20.3.0
black==20.8b1
certifi==2020.11.8
chardet==3.0.4
click==7.1.2
colorama==0.4.4
gql==3.0.0a4
graphql-core==3.1.2
idna==2.10
isort==5.6.4
lazy-object-proxy==1.4.3
mccabe==0.6.1
multidict==5.0.0
mypy-extensions==0.4.3
pathspec==0.8.1
pydantic==1.7.2
pylint==2.6.0
regex==2020.11.11
requests==2.24.0
six==1.15.0
toml==0.10.2
typed-ast==1.4.1
typing-extensions==3.7.4.3
urllib3==1.25.11
websockets==8.1
wrapt==1.12.1
yarl==1.5.1
-63
View File
@@ -1,63 +0,0 @@
from __future__ import annotations
from typing import List, Optional
from pydantic import BaseModel
from speckle.transports.sqlite import SQLiteTransport
account_storage = SQLiteTransport(scope="Accounts")
def get_local_accounts() -> List[Account]:
"""Gets all the accounts present in this environment
Returns:
List[Account] -- list of all local accounts or an empty list if no accounts were found
"""
res = account_storage.get_all_objects()
return [Account.parse_raw(r[1]) for r in res] if res else []
def get_default_account() -> Account:
"""Gets this environment's default account if any. If there is no default, the first found will be returned and set as default.
Returns:
Account -- the default account or None if no local accounts were found
"""
accounts = get_local_accounts()
if not accounts:
return None
default = next((acc for acc in accounts if acc.isDefault), None)
if not default:
default = accounts[0]
default.isDefault = True
return default
class ServerInfo(BaseModel):
name: str
company: Optional[str]
url: str
class UserInfo(BaseModel):
name: str
email: str
company: Optional[str]
id: str
class Account(BaseModel):
isDefault: bool
token: str
refreshToken: str
serverInfo: ServerInfo
userInfo: UserInfo
id: str
def __repr__(self) -> str:
return f"Account(email: {self.userInfo.email}, server: {self.serverInfo.url}, isDefault: {self.isDefault})"
def __str__(self) -> str:
return f"Account(email: {self.userInfo.email}, server: {self.serverInfo.url}, isDefault: {self.isDefault})"
-48
View File
@@ -1,48 +0,0 @@
from typing import List, Optional
from gql import gql
from pydantic.main import BaseModel
from speckle.api.resource import ResourceBase
from speckle.api.models import Branch
NAME = "branch"
METHODS = ["create"]
class Resource(ResourceBase):
"""API Access class for branches"""
def __init__(self, me, basepath, client) -> None:
super().__init__(
me=me, basepath=basepath, client=client, name=NAME, methods=METHODS
)
self.schema = Branch
def create(
self, streamId: str, name: str, description: str = "No description provided"
) -> str:
"""Create a new branch on this stream
Arguments:
name {str} -- the name of the new branch
description {str} -- a short description of the branch
Returns:
id {str} -- the newly created branch's id
"""
query = gql(
"""
mutation BranchCreate($branch: BranchCreateInput!){
branchCreate(branch: $branch)
}
"""
)
params = {
"branch": {
"streamId": streamId,
"name": name,
"description": description,
}
}
return self.make_request(query=query, params=params, parse_response=False)
-79
View File
@@ -1,79 +0,0 @@
from typing import Dict
from gql import gql
from gql.client import Client
from speckle.api.resource import ResourceBase
NAME = "server"
METHODS = ["get", "apps"]
class Resource(ResourceBase):
"""API Access class for the server"""
def __init__(self, me, basepath, client) -> None:
super().__init__(
me=me, basepath=basepath, client=client, name=NAME, methods=METHODS
)
def get(self) -> Dict:
"""Get the server info
Returns:
dict -- the server info in dictionary form
"""
query = gql(
"""
query Server {
serverInfo {
name
company
description
adminContact
canonicalUrl
roles {
name
description
resourceTarget
}
scopes {
name
description
}
authStrategies{
id
name
icon
}
}
}
"""
)
return self.make_request(query=query)
def apps(self) -> Dict:
"""Get the apps registered on the server
Returns:
dict -- a dictionary of apps registered on the server
"""
query = gql(
"""
query Apps {
apps {
id
name
description
termsAndConditionsLink
logo
author {
id
name
}
}
}
"""
)
return self.make_request(query=query)
-14
View File
@@ -1,14 +0,0 @@
from pathlib import Path
import sys
import inspect
import pkgutil
from importlib import import_module
from .base import Base
for (_, name, _) in pkgutil.iter_modules([Path(__file__).parent]):
imported_module = import_module("." + name, package=__name__)
classes = inspect.getmembers(imported_module, inspect.isclass)
for c in classes:
if issubclass(c[1], Base):
setattr(sys.modules[__name__], c[0], c[1])
-140
View File
@@ -1,140 +0,0 @@
from __future__ import annotations
from pydantic import BaseModel
from pydantic.main import Extra
from typing import Dict, List, Optional, Any
from speckle.transports.memory import MemoryTransport
from speckle.logging.exceptions import SpeckleException
PRIMITIVES = (int, float, str, bool)
class Base(BaseModel):
id: Optional[str] = None
totalChildrenCount: Optional[int] = None
applicationId: Optional[str] = None
speckle_type: Optional[str] = "Base"
_chunkable: Dict[str, int] = {} # dict of chunkable props and their max chunk size
def __init__(self, **kwargs) -> None:
super().__init__()
self.speckle_type = self.__class__.__name__
self.__dict__.update(kwargs)
def __repr__(self) -> str:
return f"{self.__class__.__name__}(id: {self.id}, speckle_type: {self.speckle_type}, totalChildrenCount: {self.totalChildrenCount})"
def __str__(self) -> str:
return self.__repr__()
def __setitem__(self, name: str, value: Any) -> None:
self.__dict__[name] = value
def __getitem__(self, name: str) -> Any:
return self.__dict__[name]
def to_dict(self) -> Dict:
"""Convenience method to view the whole base object as a dict"""
base_dict = self.__dict__
for key, value in base_dict.items():
if not value or isinstance(value, PRIMITIVES):
continue
else:
base_dict[key] = self.__dict_helper(value)
return base_dict
def __dict_helper(self, obj: Any) -> Any:
if isinstance(obj, PRIMITIVES):
return obj
if isinstance(obj, Base):
return self.__dict_helper(obj.__dict__)
if isinstance(obj, (list, set)):
return [self.__dict_helper(v) for v in obj]
if isinstance(obj, dict):
for k, v in obj.items():
if not v or isinstance(obj, PRIMITIVES):
pass
else:
obj[k] = self.__dict_helper(v)
return obj
else:
raise SpeckleException(
message=f"Could not convert to dict due to unrecognised type: {type(obj)}"
)
def get_member_names(self) -> List[str]:
"""Get all of the property names on this object, dynamic or not"""
return list(self.__dict__.keys())
def get_typed_member_names(self) -> List[str]:
"""Get all of the names of the defined (typed) properties of this object"""
return list(self.__fields__.keys())
def get_dynamic_member_names(self) -> List[str]:
"""Get all of the names of the dynamic properties of this object"""
return list(set(self.__dict__.keys()) - set(self.__fields__.keys()))
def get_children_count(self) -> int:
"""Get the total count of children Base objects"""
parsed = []
return 1 + self._count_descendants(self, parsed)
def get_id(self, decompose: bool = False) -> str:
if self.id and not decompose:
return self.id
else:
from speckle.serialization.base_object_serializer import (
BaseObjectSerializer,
)
serializer = BaseObjectSerializer()
if decompose:
serializer.write_transports = [MemoryTransport()]
return serializer.traverse_base(self)[0]
def _count_descendants(self, base: Base, parsed: List) -> int:
if base in parsed:
return 0
parsed.append(base)
count = 0
for name, value in base.__dict__.items():
if name.startswith("@"):
continue
else:
count += self._handle_object_count(value, parsed)
return count
def _handle_object_count(self, obj: Any, parsed: List) -> int:
count = 0
if obj == None:
return count
if isinstance(obj, Base):
count += 1
count += self._count_descendants(obj, parsed)
return count
elif isinstance(obj, list):
for item in obj:
if isinstance(item, Base):
count += 1
count += self._count_descendants(item, parsed)
else:
count += self._handle_object_count(item, parsed)
elif isinstance(obj, dict):
for _, value in obj.items():
if isinstance(value, Base):
count += 1
count += self._count_descendants(value, parsed)
else:
count += self._handle_object_count(value, parsed)
return count
class Config:
extra = Extra.allow
class DataChunk(Base):
data: List[Any] = []
-24
View File
@@ -1,24 +0,0 @@
from __future__ import annotations
from typing import List, Optional
from pydantic import BaseModel
from .base import Base
CHUNKABLE_PROPS = {
"vertices": 2000,
"faces": 2000,
"colors": 2000,
"textureCoordinates": 2000,
}
class Mesh(Base):
vertices: List[float] = None
faces: List[int] = None
colors: List[int] = None
textureCoordinates: List[float] = None
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self._chunkable.update(CHUNKABLE_PROPS)
@@ -1,10 +1,19 @@
import re
from gql.client import SyncClientSession
from speckle.logging.exceptions import SpeckleException
from specklepy.logging.exceptions import SpeckleException
from typing import Dict
from speckle.api import resources
from speckle.api.resources import commit, stream, object, server, user, subscriptions
from specklepy.api import resources
from specklepy.api.resources import (
branch,
commit,
stream,
object,
server,
user,
subscriptions,
)
from specklepy.api.models import ServerInfo
from gql import Client, gql
from gql.transport.requests import RequestsHTTPTransport
from gql.transport.aiohttp import AIOHTTPTransport
@@ -12,7 +21,34 @@ from gql.transport.websockets import WebsocketsTransport
class SpeckleClient:
DEFAULT_HOST = "staging.speckle.dev"
"""
The `SpeckleClient` is your entry point for interacting with your Speckle Server's GraphQL API.
You'll need to have access to a server to use it, or you can use our public server `speckle.xyz`.
To authenticate the client, you'll need to have downloaded the [Speckle Manager](https://speckle.guide/#speckle-manager)
and added your account.
```py
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import get_default_account
# initialise the client
client = SpeckleClient(host="speckle.xyz") # or whatever your host is
# client = SpeckleClient(host="localhost:3000", use_ssl=False) or use local server
# authenticate the client with a token (account has been added in Speckle Manager)
account = get_default_account()
client.authenticate(token=account.token)
# create a new stream. this returns the stream id
new_stream_id = client.stream.create(name="a shiny new stream")
# use that stream id to get the stream from the server
new_stream = client.stream.get(id=new_stream_id)
```
"""
DEFAULT_HOST = "speckle.xyz"
USE_SSL = True
def __init__(self, host: str = DEFAULT_HOST, use_ssl: bool = USE_SSL) -> None:
@@ -38,6 +74,19 @@ class SpeckleClient:
self._init_resources()
# Check compatibility with the server
try:
serverInfo = self.server.get()
if not isinstance(serverInfo, ServerInfo):
raise Exception("Couldn't get ServerInfo")
except Exception as ex:
raise SpeckleException(f"{self.url} is not a compatible Speckle Server", ex)
def __repr__(self):
return (
f"SpeckleClient( server: {self.url}, authenticated: {self.me is not None} )"
)
def authenticate(self, token: str) -> None:
"""Authenticate the client using a personal access token
The token is saved in the client object and a synchronous GraphQL entrypoint is created
@@ -72,6 +121,9 @@ class SpeckleClient:
self.commit = commit.Resource(
me=self.me, basepath=self.url, client=self.httpclient
)
self.branch = branch.Resource(
me=self.me, basepath=self.url, client=self.httpclient
)
self.object = object.Resource(
me=self.me, basepath=self.url, client=self.httpclient
)
@@ -92,4 +144,4 @@ class SpeckleClient:
except:
raise SpeckleException(
f"Method {name} is not supported by the SpeckleClient class"
)
)
+182
View File
@@ -0,0 +1,182 @@
import os
from specklepy.transports.server.server import ServerTransport
from warnings import warn
from pydantic import BaseModel
from typing import List, Optional
from urllib.parse import urlparse, unquote
from specklepy.api.models import ServerInfo
from specklepy.api.client import SpeckleClient
from specklepy.transports.sqlite import SQLiteTransport
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
class UserInfo(BaseModel):
name: str
email: str
company: Optional[str]
id: str
class Account(BaseModel):
isDefault: bool = None
token: str
refreshToken: str = None
serverInfo: ServerInfo
userInfo: UserInfo
id: str = None
def __repr__(self) -> str:
return f"Account(email: {self.userInfo.email}, server: {self.serverInfo.url}, isDefault: {self.isDefault})"
def __str__(self) -> str:
return self.__repr__()
def get_local_accounts(base_path: str = None) -> List[Account]:
"""Gets all the accounts present in this environment
Arguments:
base_path {str} -- custom base path if you are not using the system default
Returns:
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)
json_path = os.path.join(account_storage._base_path, "Accounts")
os.makedirs(json_path, exist_ok=True)
json_acct_files = [file for file in os.listdir(json_path) if file.endswith(".json")]
accounts = []
res = account_storage.get_all_objects()
if res:
accounts.extend(Account.parse_raw(r[1]) for r in res)
if json_acct_files:
try:
accounts.extend(
Account.parse_file(os.path.join(json_path, json_file))
for json_file in json_acct_files
)
except Exception as ex:
raise SpeckleException(
"Invalid json accounts could not be read. Please fix or remove them.",
ex,
)
return accounts
def get_default_account(base_path: str = None) -> Account:
"""Gets this environment's default account if any. If there is no default, the first found will be returned and set as default.
Arguments:
base_path {str} -- custom base path if you are not using the system default
Returns:
Account -- the default account or None if no local accounts were found
"""
accounts = get_local_accounts(base_path=base_path)
if not accounts:
return None
default = next((acc for acc in accounts if acc.isDefault), None)
if not default:
default = accounts[0]
default.isDefault = True
return default
class StreamWrapper:
stream_url: str = None
use_ssl: bool = True
host: str = None
stream_id: str = None
commit_id: str = None
object_id: str = None
branch_name: str = None
client: SpeckleClient = None
account: Account = None
def __repr__(self):
return f"StreamWrapper( server: {self.host}, stream_id: {self.stream_id}, type: {self.type} )"
def __str__(self) -> str:
return self.__repr__()
@property
def type(self) -> str:
if self.object_id:
return "object"
elif self.commit_id:
return "commit"
elif self.branch_name:
return "branch"
else:
return "stream" if self.stream_id else "invalid"
def __init__(self, url: str) -> None:
self.stream_url = url
parsed = urlparse(url)
self.host = parsed.netloc
self.use_ssl = parsed.scheme == "https"
segments = parsed.path.strip("/").split("/")
if not segments or len(segments) > 4 or len(segments) < 2:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - invalid URL provided."
)
while segments:
segment = segments.pop(0)
if segments and segment.lower() == "streams":
self.stream_id = segments.pop(0)
elif segments and segment.lower() == "commits":
self.commit_id = segments.pop(0)
elif segments and segment.lower() == "branches":
self.branch_name = unquote(segments.pop(0))
elif segments and segment.lower() == "objects":
self.object_id = segments.pop(0)
elif segment.lower() == "globals":
self.branch_name = "globals"
if segments:
self.commit_id = segments.pop(0)
else:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - invalid URL provided."
)
if not self.stream_id:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - no stream id found."
)
def get_account(self) -> Account:
if self.account:
return self.account
self.account = next(
(a for a in get_local_accounts() if self.host in a.serverInfo.url),
None,
)
return self.account
def get_client(self) -> SpeckleClient:
if self.client:
return self.client
if not self.account:
self.get_account()
self.client = SpeckleClient(host=self.host, use_ssl=self.use_ssl)
if self.account is None:
warn(f"No local account found for server {self.host}", SpeckleWarning)
return self.client
self.client.authenticate(acct.token)
return self.client
def get_transport(self) -> ServerTransport:
if not self.client:
self.get_client()
return ServerTransport(self.client, self.stream_id)
@@ -2,8 +2,6 @@
# filename: stream_schema.json
# timestamp: 2020-11-17T14:33:13+00:00
from __future__ import annotations
from datetime import datetime
from typing import Any, Dict, List, Optional
@@ -24,8 +22,12 @@ class Commit(BaseModel):
authorName: Optional[str]
authorId: Optional[str]
authorAvatar: Optional[str]
branchName: Optional[str]
createdAt: Optional[str]
sourceApplication: Optional[str]
referencedObject: Optional[str]
totalChildrenCount: Optional[int]
parents: Optional[List[str]]
def __repr__(self) -> str:
return f"Commit( id: {self.id}, message: {self.message}, referencedObject: {self.referencedObject}, authorName: {self.authorName}, createdAt: {self.createdAt} )"
@@ -61,12 +63,6 @@ class Branches(BaseModel):
items: List[Branch] = []
class Streams(BaseModel):
totalCount: Optional[int]
cursor: Optional[datetime]
items: List[Stream] = []
class Stream(BaseModel):
id: Optional[str]
name: Optional[str]
@@ -86,6 +82,12 @@ class Stream(BaseModel):
return self.__repr__()
class Streams(BaseModel):
totalCount: Optional[int]
cursor: Optional[datetime]
items: List[Stream] = []
class User(BaseModel):
id: Optional[str]
email: Optional[str]
@@ -102,3 +104,16 @@ class User(BaseModel):
def __str__(self) -> str:
return self.__repr__()
class ServerInfo(BaseModel):
name: Optional[str]
company: Optional[str]
url: Optional[str]
description: Optional[str]
adminContact: Optional[str]
canonicalUrl: Optional[str]
roles: Optional[List[dict]]
scopes: Optional[List[dict]]
authStrategies: Optional[List[dict]]
version: Optional[str]
@@ -1,9 +1,11 @@
import json
from typing import List
from speckle.objects.base import Base
from speckle.transports.memory import MemoryTransport
from speckle.logging.exceptions import SpeckleException
from speckle.transports.abstract_transport import AbstractTransport
from speckle.serialization.base_object_serializer import BaseObjectSerializer
from specklepy.objects.base import Base
from specklepy.transports.sqlite import SQLiteTransport
from specklepy.transports.server import ServerTransport
from specklepy.logging.exceptions import SpeckleException
from specklepy.transports.abstract_transport import AbstractTransport
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
def send(
@@ -26,8 +28,7 @@ def send(
message="You need to provide at least one transport: cannot send with an empty transport list and no default cache"
)
if use_default_cache:
# TODO: finish sqlite transport and chuck it in here
pass
transports.insert(0, SQLiteTransport())
serializer = BaseObjectSerializer(write_transports=transports)
@@ -51,23 +52,22 @@ def receive(
Arguments:
obj_id {str} -- the id of the object to receive
remote_transport {Transport} -- the transport to receive from
local_transport {Transport} -- the transport to send from
local_transport {Transport} -- the local cache to check for existing objects
(defaults to `SQLiteTransport`)
Returns:
Base -- the base object
"""
# TODO: replace with sqlite transport
if not local_transport:
local_transport = MemoryTransport()
local_transport = SQLiteTransport()
serializer = BaseObjectSerializer(read_transport=local_transport)
# try local transport first. if the parent is there, we assume all the children are there and continue wth deserialisation using the local transport
obj_string = local_transport.get_object(obj_id)
if obj_string:
base = serializer.read_json(obj_string=obj_string)
return base
return serializer.read_json(obj_string=obj_string)
if not remote_transport:
raise SpeckleException(
@@ -81,13 +81,38 @@ def receive(
return serializer.read_json(obj_string=obj_string)
def serialize(base: Base) -> str:
serializer = BaseObjectSerializer()
def serialize(base: Base, write_transports: List[AbstractTransport] = []) -> str:
"""
Serialize a base object. If no write transports are provided, the object will be serialized
without detaching or chunking any of the attributes.
Arguments:
base {Base} -- the object to serialize
write_transports {List[AbstractTransport]} -- optional: the transports to write to
Returns:
str -- the serialized object
"""
serializer = BaseObjectSerializer(write_transports=write_transports)
return serializer.write_json(base)[1]
def deserialize(obj_string: str) -> Base:
serializer = BaseObjectSerializer()
def deserialize(obj_string: str, read_transport: AbstractTransport = None) -> Base:
"""
Deserialize a string object into a Base object. If the object contains referenced child objects that are not stored in the local db, a read transport needs to be provided in order to recompose the base with the children objects.
Arguments:
obj_string {str} -- the string object to deserialize
read_transport {AbstractTransport} -- the transport to fetch children objects from
(defaults to SQLiteTransport)
Returns:
Base -- the deserialized object
"""
if not read_transport:
read_transport = SQLiteTransport()
serializer = BaseObjectSerializer(read_transport=read_transport)
return serializer.read_json(obj_string=obj_string)
@@ -1,5 +1,5 @@
from logging import error
from speckle.logging.exceptions import GraphQLException, SpeckleException
from specklepy.logging.exceptions import GraphQLException, SpeckleException
from typing import Dict, List
from gql.client import Client
from gql.gql import gql
@@ -5,9 +5,9 @@ import pkgutil
from importlib import import_module
for (_, name, _) in pkgutil.iter_modules([Path(__file__).parent]):
for (_, name, _) in pkgutil.iter_modules(__path__):
imported_module = import_module("." + name, package=__name__)
if hasattr(imported_module, "Resource"):
setattr(sys.modules[__name__], name, imported_module)
setattr(sys.modules[__name__], name, imported_module)
+212
View File
@@ -0,0 +1,212 @@
from specklepy.api.resources import stream
from typing import List, Optional
from gql import gql
from pydantic.main import BaseModel
from specklepy.api.resource import ResourceBase
from specklepy.api.models import Branch
NAME = "branch"
METHODS = ["create"]
class Resource(ResourceBase):
"""API Access class for branches"""
def __init__(self, me, basepath, client) -> None:
super().__init__(
me=me, basepath=basepath, client=client, name=NAME, methods=METHODS
)
self.schema = Branch
def create(
self, stream_id: str, name: str, description: str = "No description provided"
) -> str:
"""Create a new branch on this stream
Arguments:
name {str} -- the name of the new branch
description {str} -- a short description of the branch
Returns:
id {str} -- the newly created branch's id
"""
query = gql(
"""
mutation BranchCreate($branch: BranchCreateInput!) {
branchCreate(branch: $branch)
}
"""
)
params = {
"branch": {
"streamId": stream_id,
"name": name,
"description": description,
}
}
return self.make_request(
query=query, params=params, return_type="branchCreate", parse_response=False
)
def get(self, stream_id: str, name: str, commits_limit: int = 10):
"""Get a branch by name from a stream
Arguments:
stream_id {str} -- the id of the stream to get the branch from
name {str} -- the name of the branch to get
commits_limit {int} -- maximum number of commits to get
Returns:
Branch -- the fetched branch with its latest commits
"""
query = gql(
"""
query BranchGet($stream_id: String!, $name: String!, $commits_limit: Int!) {
stream(id: $stream_id) {
branch(name: $name) {
id,
name,
description,
commits (limit: $commits_limit) {
totalCount,
cursor,
items {
id,
referencedObject,
sourceApplication,
totalChildrenCount,
message,
authorName,
authorId,
branchName,
parents,
createdAt
}
}
}
}
}
"""
)
params = {"stream_id": stream_id, "name": name, "commits_limit": commits_limit}
return self.make_request(
query=query, params=params, return_type=["stream", "branch"]
)
def list(self, stream_id: str, branches_limit: int = 10, commits_limit: int = 10):
"""Get a list of branches from a given stream
Arguments:
stream_id {str} -- the id of the stream to get the branches from
branches_limit {int} -- maximum number of branches to get
commits_limit {int} -- maximum number of commits to get
Returns:
List[Branch] -- the branches on the stream
"""
query = gql(
"""
query BranchesGet($stream_id: String!, $branches_limit: Int!, $commits_limit: Int!) {
stream(id: $stream_id) {
branches(limit: $branches_limit) {
items {
id
name
description
commits(limit: $commits_limit) {
totalCount
items{
id
message
referencedObject
sourceApplication
parents
authorId
authorName
branchName
createdAt
}
}
}
}
}
}
"""
)
params = {
"stream_id": stream_id,
"branches_limit": branches_limit,
"commits_limit": commits_limit,
}
return self.make_request(
query=query, params=params, return_type=["stream", "branches", "items"]
)
def update(
self, stream_id: str, branch_id: str, name: str = None, description: str = None
):
"""Update a branch
Arguments:
stream_id {str} -- the id of the stream containing the branch to update
branch_id {str} -- the id of the branch to update
name {str} -- optional: the updated branch name
description {str} -- optional: the updated branch description
Returns:
bool -- True if update is successfull
"""
query = gql(
"""
mutation BranchUpdate($branch: BranchUpdateInput!) {
branchUpdate(branch: $branch)
}
"""
)
params = {
"branch": {
"streamId": stream_id,
"id": branch_id,
}
}
if name:
params["branch"]["name"] = name
if description:
params["branch"]["description"] = description
return self.make_request(
query=query, params=params, return_type="branchUpdate", parse_response=False
)
def delete(self, stream_id: str, branch_id: str):
"""Delete a branch
Arguments:
stream_id {str} -- the id of the stream containing the branch to delete
branch_id {str} -- the branch to delete
Returns:
bool -- True if deletion is successful
"""
query = gql(
"""
mutation BranchDelete($branch: BranchDeleteInput!) {
branchDelete(branch: $branch)
}
"""
)
params = {"branch": {"streamId": stream_id, "id": branch_id}}
return self.make_request(
query=query, params=params, return_type="branchDelete", parse_response=False
)
@@ -1,8 +1,8 @@
from typing import Optional, List
from gql import gql
from pydantic.main import BaseModel
from speckle.api.resource import ResourceBase
from speckle.api.models import Commit
from specklepy.api.resource import ResourceBase
from specklepy.api.models import Commit
NAME = "commit"
@@ -37,9 +37,14 @@ class Resource(ResourceBase):
id
referencedObject
message
authorName
authorId
authorName
authorAvatar
branchName
createdAt
sourceApplication
totalChildrenCount
parents
}
}
}
@@ -70,10 +75,15 @@ class Resource(ResourceBase):
items {
id
message
referencedObject
authorName
authorId
authorName
authorAvatar
createdAt
referencedObject
sourceApplication
totalChildrenCount
parents
}
}
}
@@ -92,6 +102,8 @@ class Resource(ResourceBase):
object_id: str,
branch_name: str = "main",
message: str = "",
source_application: str = "python",
parents: List[str] = None,
) -> str:
"""
Creates a commit on a branch
@@ -101,6 +113,8 @@ class Resource(ResourceBase):
object_id {str} -- the hash of your commit object
branch_name {str} -- the name of the branch to commit to (defaults to "main")
message {str} -- optional: a message to give more information about the commit
source_application{str} -- optional: the application from which the commit was created (defaults to "python")
parents {List[str]} -- optional: the id of the parent commits
Returns:
str -- the id of the created commit
@@ -116,8 +130,11 @@ class Resource(ResourceBase):
"branchName": branch_name,
"objectId": object_id,
"message": message,
"sourceApplication": source_application,
}
}
if parents:
params["commit"]["parents"] = parents
return self.make_request(
query=query, params=params, return_type="commitCreate", parse_response=False
@@ -1,8 +1,8 @@
from typing import Dict, List
from gql import gql
from graphql.language import parser
from speckle.api.resource import ResourceBase
from speckle.objects.base import Base
from specklepy.api.resource import ResourceBase
from specklepy.objects.base import Base
NAME = "object"
METHODS = []
@@ -49,14 +49,17 @@ class Resource(ResourceBase):
params = {"stream_id": stream_id, "object_id": object_id}
return self.make_request(
query=query, params=params, return_type=["stream", "object"]
query=query, params=params, return_type=["stream", "object", "data"]
)
def create(self, stream_id: str, objects: List[Dict]) -> str:
"""
Create a new object on a stream. To send a base object, you can prepare it by running it through the `BaseObjectSerializer.travers_base` function to get a valid (serialisable) object to send.
Not advised - generally, you want to use `operations.send()`.
NOTE: this does not create a commit - you can create one with `SpeckleClient.commit.create`.
Create a new object on a stream. To send a base object, you can prepare it by running it through the
`BaseObjectSerializer.traverse_base()` function to get a valid (serialisable) object to send.
NOTE: this does not create a commit - you can create one with `SpeckleClient.commit.create`. Dynamic fields will be located in the 'data' dict of the received `Base` object
Arguments:
stream_id {str} -- the id of the stream you want to send the object to
+137
View File
@@ -0,0 +1,137 @@
from typing import Dict, List
from gql import gql
from gql.client import Client
from specklepy.api.models import ServerInfo
from specklepy.api.resource import ResourceBase
NAME = "server"
METHODS = ["get", "apps"]
class Resource(ResourceBase):
"""API Access class for the server"""
def __init__(self, me, basepath, client) -> None:
super().__init__(
me=me, basepath=basepath, client=client, name=NAME, methods=METHODS
)
def get(self) -> ServerInfo:
"""Get the server info
Returns:
dict -- the server info in dictionary form
"""
query = gql(
"""
query Server {
serverInfo {
name
company
description
adminContact
canonicalUrl
version
roles {
name
description
resourceTarget
}
scopes {
name
description
}
authStrategies{
id
name
icon
}
}
}
"""
)
return self.make_request(
query=query, return_type="serverInfo", schema=ServerInfo
)
def apps(self) -> Dict:
"""Get the apps registered on the server
Returns:
dict -- a dictionary of apps registered on the server
"""
query = gql(
"""
query Apps {
apps{
id
name
description
termsAndConditionsLink
trustByDefault
logo
author {
id
name
avatar
}
}
}
"""
)
return self.make_request(query=query, return_type="apps", parse_response=False)
def create_token(self, name: str, scopes: List[str], lifespan: int) -> str:
"""Create a personal API token
Arguments:
scopes {List[str]} -- the scopes to grant with this token
name {str} -- a name for your new token
lifespan {int} -- duration before the token expires
Returns:
str -- the new API token. note: this is the only time you'll see the token!
"""
query = gql(
"""
mutation TokenCreate($token: ApiTokenCreateInput!) {
apiTokenCreate(token: $token)
}
"""
)
params = {"token": {"scopes": scopes, "name": name, "lifespan": lifespan}}
return self.make_request(
query=query,
params=params,
return_type="apiTokenCreate",
parse_response=False,
)
def revoke_token(self, token: str) -> bool:
"""Revokes (deletes) a personal API token
Arguments:
token {str} -- the token to revoke (delete)
Returns:
bool -- True if the token was successfully deleted
"""
query = gql(
"""
mutation TokenRevoke($token: String!) {
apiTokenRevoke(token: $token)
}
"""
)
params = {"token": token}
return self.make_request(
query=query,
params=params,
return_type="apiTokenRevoke",
parse_response=False,
)
@@ -1,10 +1,7 @@
from re import search
from typing import Dict, List, Optional
from pydantic import BaseModel
from gql import gql
from speckle.api.resource import ResourceBase
from speckle.api.models import Stream
from speckle.logging.exceptions import GraphQLException
from specklepy.api.resource import ResourceBase
from specklepy.api.models import Stream
NAME = "stream"
METHODS = [
@@ -296,3 +293,64 @@ class Resource(ResourceBase):
return self.make_request(
query=query, params=params, return_type=["streams", "items"]
)
def grant_permission(self, stream_id: str, user_id: str, role: str):
"""Grant permissions to a user on a given stream
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
"""
query = gql(
"""
mutation StreamGrantPermission($permission_params: StreamGrantPermissionInput !) {
streamGrantPermission(permissionParams: $permission_params)
}
"""
)
params = {
"permission_params": {
"streamId": stream_id,
"userId": user_id,
"role": role,
}
}
return self.make_request(
query=query,
params=params,
return_type="streamGrantPermission",
parse_response=False,
)
def revoke_permission(self, stream_id: str, user_id: str):
"""Revoke permissions from a user on a given stream
Arguments:
stream_id {str} -- the id of the stream to revoke permissions from
user_id {str} -- the id of the user to revoke permissions from
Returns:
bool -- True if the operation was successful
"""
query = gql(
"""
mutation StreamRevokePermission($permission_params: StreamRevokePermissionInput !) {
streamRevokePermission(permissionParams: $permission_params)
}
"""
)
params = {"permission_params": {"streamId": stream_id, "userId": user_id}}
return self.make_request(
query=query,
params=params,
return_type="streamRevokePermission",
parse_response=False,
)
@@ -1,9 +1,9 @@
from typing import Callable, Dict, List, Optional, Any
from functools import wraps
from gql import gql
from speckle.api.resource import ResourceBase
from speckle.api.resources.stream import Stream
from speckle.logging.exceptions import GraphQLException, SpeckleException
from specklepy.api.resource import ResourceBase
from specklepy.api.resources.stream import Stream
from specklepy.logging.exceptions import GraphQLException, SpeckleException
NAME = "subscribe"
METHODS = [
@@ -122,4 +122,4 @@ class Resource(ResourceBase):
if callback is not None:
callback(res)
else:
return res
return res
@@ -1,9 +1,9 @@
from speckle.logging.exceptions import SpeckleException
from specklepy.logging.exceptions import SpeckleException
from typing import List, Optional
from gql import gql
from pydantic.main import BaseModel
from speckle.api.resource import ResourceBase
from speckle.api.models import User
from specklepy.api.resource import ResourceBase
from specklepy.api.models import User
NAME = "user"
METHODS = ["get"]
@@ -30,17 +30,17 @@ class Resource(ResourceBase):
query = gql(
"""
query User($id: String) {
user(id: $id) {
id
email
name
bio
company
avatar
verified
profiles
role
}
user(id: $id) {
id
email
name
bio
company
avatar
verified
profiles
role
}
}
"""
)
@@ -66,16 +66,16 @@ class Resource(ResourceBase):
query = gql(
"""
query UserSearch($search_query: String!, $limit: Int!) {
userSearch(query: $search_query, limit: $limit) {
items {
id
name
bio
company
avatar
verified
userSearch(query: $search_query, limit: $limit) {
items {
id
name
bio
company
avatar
verified
}
}
}
}
"""
)
@@ -84,3 +84,37 @@ class Resource(ResourceBase):
return self.make_request(
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
"""
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
)
@@ -28,3 +28,8 @@ class GraphQLException(SpeckleException):
def __str__(self) -> str:
return f"GraphQLException: {self.message}"
class SpeckleWarning(Warning):
def __init__(self, *args: object) -> None:
super().__init__(*args)
+5
View File
@@ -0,0 +1,5 @@
"""Builtin Speckle object kit."""
from specklepy.objects.base import Base
__all__ = ["Base"]
+253
View File
@@ -0,0 +1,253 @@
from inspect import getattr_static
from pydantic import BaseModel, validator
from pydantic.main import Extra
from typing import ClassVar, Dict, List, Optional, Any, Set, Type
from specklepy.transports.memory import MemoryTransport
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.units import get_units_from_string
PRIMITIVES = (int, float, str, bool)
class _RegisteringBase(BaseModel):
"""
Private Base model for Speckle types.
This is an implementation detail, please do not use this outside this module.
This class provides automatic registration of `speckle_type` into a global,
(class level) registry for each subclassing type.
The type registry is a base for accurate type based (de)serialization.
"""
speckle_type: ClassVar[str]
_type_registry: ClassVar[Dict[str, Type["Base"]]] = {}
class Config:
validate_assignment = True
@classmethod
def get_registered_type(cls, speckle_type: str) -> Optional[Type["Base"]]:
"""Get the registered type from the protected mapping via the `speckle_type`"""
return cls._type_registry.get(speckle_type, None)
def __init_subclass__(
cls,
speckle_type: Optional[str] = None,
**kwargs: Dict[str, Any],
):
"""
Hook into subclass type creation.
This is provides a mechanism to hook into the event of the subclass type object
initialization. This is reused to register each subclassing type into a class
level dictionary.
"""
if speckle_type in cls._type_registry:
raise ValueError(
f"The speckle_type: {speckle_type} is already registered for type: "
f"{cls._type_registry[speckle_type].__name__}. "
f"Please choose a different type name."
)
cls.speckle_type = speckle_type or cls.__name__
cls._type_registry[cls.speckle_type] = cls # type: ignore
super().__init_subclass__(**kwargs)
class Base(_RegisteringBase):
id: Optional[str] = None
totalChildrenCount: Optional[int] = None
applicationId: Optional[str] = None
_units: str = "m"
_chunkable: Dict[str, int] = {} # dict of chunkable props and their max chunk size
_chunk_size_default: int = 1000
_detachable: Set[str] = set() # list of defined detachable props
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}(id: {self.id}, "
f"speckle_type: {self.speckle_type}, "
f"totalChildrenCount: {self.totalChildrenCount})"
)
def __str__(self) -> str:
return self.__repr__()
def __setitem__(self, name: str, value: Any) -> None:
self.validate_prop_name(name)
self.__dict__[name] = value
def __getitem__(self, name: str) -> Any:
return self.__dict__[name]
def __setattr__(self, name: str, value: Any) -> None:
"""
Guard attribute and property set mechanism.
The `speckle_type` is a protected class attribute it must not be overridden.
"""
if name != "speckle_type":
attr = getattr(self.__class__, name, None)
if isinstance(attr, property):
try:
attr.__set__(self, value)
except AttributeError:
pass # the prop probably doesn't have a setter
super().__setattr__(name, value)
@classmethod
def validate_prop_name(cls, name: str) -> None:
"""Validator for dynamic attribute names."""
if name in {"", "@"}:
raise ValueError("Invalid Name: Base member names cannot be empty strings")
if name.startswith("@@"):
raise ValueError(
"Invalid Name: Base member names cannot start with more than one '@'",
)
if "." in name or "/" in name:
raise ValueError(
"Invalid Name: Base member names cannot contain characters '.' or '/'",
)
def add_chunkable_attrs(self, **kwargs: int) -> None:
"""
Mark defined attributes as chunkable for serialisation
Arguments:
kwargs {int} -- the name of the attribute as the keyword and the chunk size as the arg
"""
chunkable = {k: v for k, v in kwargs.items() if isinstance(v, int)}
self._chunkable = dict(self._chunkable, **chunkable)
def add_detachable_attrs(self, names: Set[str]) -> None:
"""
Mark defined attributes as detachable for serialisation
Arguments:
names {Set[str]} -- the names of the attributes to detach as a set of strings
"""
self._detachable = self._detachable.union(names)
@property
def units(self):
return self._units
@units.setter
def units(self, value: str):
self._units = get_units_from_string(value)
def to_dict(self) -> Dict[str, Any]:
"""Convenience method to view the whole base object as a dict"""
base_dict = self.__dict__
for key, value in base_dict.items():
if not value or isinstance(value, PRIMITIVES):
continue
else:
base_dict[key] = self.__dict_helper(value)
return base_dict
def __dict_helper(self, obj: Any) -> Any:
if not obj or isinstance(obj, PRIMITIVES):
return obj
if isinstance(obj, Base):
return self.__dict_helper(obj.__dict__)
if isinstance(obj, (list, set)):
return [self.__dict_helper(v) for v in obj]
if not isinstance(obj, dict):
raise SpeckleException(
message=f"Could not convert to dict due to unrecognized type: {type(obj)}"
)
for k, v in obj.items():
if v and not isinstance(obj, PRIMITIVES):
obj[k] = self.__dict_helper(v)
return obj
def get_member_names(self) -> List[str]:
"""Get all of the property names on this object, dynamic or not"""
attrs = list(self.__dict__.keys())
properties = [
name
for name in dir(self)
if not name.startswith("_")
and name
!= "fields" # soon to be removed as this pydantic prop is depreciated
and isinstance(getattr(type(self), name, None), property)
]
return attrs + properties
def get_typed_member_names(self) -> List[str]:
"""Get all of the names of the defined (typed) properties of this object"""
return list(self.__fields__.keys())
def get_dynamic_member_names(self) -> List[str]:
"""Get all of the names of the dynamic properties of this object"""
return list(set(self.__dict__.keys()) - set(self.__fields__.keys()))
def get_children_count(self) -> int:
"""Get the total count of children Base objects"""
parsed = []
return 1 + self._count_descendants(self, parsed)
def get_id(self, decompose: bool = False) -> str:
"""
Gets the id (a unique hash) of this object. ⚠️ This method fully serializes the object, which in the case of large objects (with many sub-objects), has a tangible cost. Avoid using it!
Note: the hash of a decomposed object differs from that of a non-decomposed object
Arguments:
decompose {bool} -- if True, will decompose the object in the process of hashing it
Returns:
str -- the hash (id) of the fully serialized object
"""
from specklepy.serialization.base_object_serializer import (
BaseObjectSerializer,
)
serializer = BaseObjectSerializer()
if decompose:
serializer.write_transports = [MemoryTransport()]
return serializer.traverse_base(self)[0]
def _count_descendants(self, base: "Base", parsed: List) -> int:
if base in parsed:
return 0
parsed.append(base)
return sum(
self._handle_object_count(value, parsed)
for name, value in base.__dict__.items()
if not name.startswith("@")
)
def _handle_object_count(self, obj: Any, parsed: List) -> int:
count = 0
if obj is None:
return count
if isinstance(obj, "Base"):
count += 1
count += self._count_descendants(obj, parsed)
return count
elif isinstance(obj, list):
for item in obj:
if isinstance(item, "Base"):
count += 1
count += self._count_descendants(item, parsed)
else:
count += self._handle_object_count(item, parsed)
elif isinstance(obj, dict):
for _, value in obj.items():
if isinstance(value, "Base"):
count += 1
count += self._count_descendants(value, parsed)
else:
count += self._handle_object_count(value, parsed)
return count
class Config:
extra = Extra.allow
class DataChunk(Base, speckle_type="Speckle.Core.Models.DataChunk"):
data: List[Any] = []
+38
View File
@@ -0,0 +1,38 @@
from specklepy.objects.geometry import Point
from typing import List
from .base import Base
CHUNKABLE_PROPS = {
"vertices": 100,
"faces": 100,
"colors": 100,
"textureCoordinates": 100,
"test_bases": 10,
}
DETACHABLE = {"detach_this", "origin", "detached_list"}
class FakeMesh(Base):
vertices: List[float] = None
faces: List[int] = None
colors: List[int] = None
textureCoordinates: List[float] = None
test_bases: List[Base] = None
detach_this: Base = None
detached_list: List[Base] = None
_origin: Point = None
def __init__(self, **kwargs) -> None:
super(FakeMesh, self).__init__(**kwargs)
self.add_chunkable_attrs(**CHUNKABLE_PROPS)
self.add_detachable_attrs(DETACHABLE)
@property
def origin(self):
return self._origin
@origin.setter
def origin(self, value: Point):
self._origin = value
+351
View File
@@ -0,0 +1,351 @@
from .base import Base
from typing import Any, List
GEOMETRY = "Objects.Geometry."
class Interval(Base, speckle_type="Objects.Primitive.Interval"):
start: float = 0
end: float = 0
def length(self):
return abs(self.start - self.end)
class Point(Base, speckle_type=GEOMETRY + "Point"):
x: float = 0
y: float = 0
z: float = 0
def __init__(self, x: float = 0, y: float = 0, z: float = 0, **data: Any) -> None:
super().__init__(**data)
self.x, self.y, self.z = x, y, z
def __repr__(self) -> str:
return f"{self.__class__.__name__}(x: {self.x}, y: {self.y}, z: {self.z}, id: {self.id}, speckle_type: {self.speckle_type})"
class Vector(Point, speckle_type=GEOMETRY + "Vector"):
pass
class ControlPoint(Point, speckle_type=GEOMETRY + "ControlPoint"):
weight: float = None
class Plane(Base, speckle_type=GEOMETRY + "Plane"):
origin: Point = Point()
normal: Vector = Vector()
xdir: Vector = Vector()
ydir: Vector = Vector()
class Box(Base, speckle_type=GEOMETRY + "Box"):
basePlane: Plane = Plane()
ySize: Interval = Interval()
zSize: Interval = Interval()
xSize: Interval = Interval()
area: float = None
volume: float = None
class Line(Base, speckle_type=GEOMETRY + "Line"):
start: Point = Point()
end: Point = None
domain: Interval = None
bbox: Box = None
length: float = None
class Arc(Base, speckle_type=GEOMETRY + "Arc"):
radius: float = None
startAngle: float = None
endAngle: float = None
angleRadians: float = None
plane: Plane = None
domain: Interval = None
startPoint: Point = None
midPoint: Point = None
endPoint: Point = None
bbox: Box = None
area: float = None
length: float = None
class Circle(Base, speckle_type=GEOMETRY + "Circle"):
radius: float = None
plane: Plane = None
domain: Interval = None
bbox: Box = None
area: float = None
length: float = None
class Ellipse(Base, speckle_type=GEOMETRY + "Ellipse"):
firstRadius: float = None
secondRadius: float = None
plane: Plane = None
domain: Interval = None
trimDomain: Interval = None
bbox: Box = None
area: float = None
length: float = None
class Polyline(Base, speckle_type=GEOMETRY + "Polyline"):
value: List[float] = None
closed: bool = None
domain: Interval = None
bbox: Box = None
area: float = None
length: float = None
def __init__(self, **data: Any) -> None:
super().__init__(**data)
self.add_chunkable_attrs(value=20000)
@classmethod
def from_points(cls, points: List[Point]):
polyline = cls()
polyline.units = points[0].units
for point in points:
polyline.value.extend([point.x, point.y, point.z])
return polyline
# @property
# def value(self) -> List[float]:
# return self._value
# @value.setter
# def value(self, coords) -> None:
# if len(coords) % 3:
# coords.extend([0] * (3 - len(coords) % 3))
# self._value = coords
def as_points(self) -> List[Point]:
"""Converts the `value` attribute to a list of Points"""
if not self.value:
return
if len(self.value) % 3:
raise ValueError("Points array malformed: length%3 != 0.")
values = iter(self.value)
return [Point(v, next(values), next(values), units=self.units) for v in values]
class Curve(Base, speckle_type=GEOMETRY + "Curve"):
degree: int = None
periodic: bool = None
rational: bool = None
points: List[float] = None
weights: List[float] = None
knots: List[float] = None
domain: Interval = None
displayValue: Polyline = None
closed: bool = None
bbox: Box = None
area: float = None
length: float = None
def __init__(self, **data: Any) -> None:
super().__init__(**data)
self.add_chunkable_attrs(points=20000, weights=20000, knots=20000)
def as_points(self) -> List[Point]:
"""Converts the `value` attribute to a list of Points"""
if not self.points:
return
if len(self.points) % 3:
raise ValueError("Points array malformed: length%3 != 0.")
values = iter(self.points)
return [Point(v, next(values), next(values), units=self.units) for v in values]
class Polycurve(Base, speckle_type=GEOMETRY + "Polycurve"):
segments: List[Base] = []
domain: Interval = None
closed: bool = None
bbox: Box = None
area: float = None
length: float = None
class Extrusion(Base, speckle_type=GEOMETRY + "Extrusion"):
capped: bool = None
profile: Base = None
pathStart: Point = None
pathEnd: Point = None
pathCurve: Base = None
pathTangent: Base = None
profiles: List[Base] = None
length: float = None
area: float = None
volume: float = None
bbox: Box = None
class Mesh(Base, speckle_type=GEOMETRY + "Mesh"):
vertices: List[float] = None
faces: List[int] = None
colors: List[int] = None
textureCoordinates: List[float] = None
bbox: Box = None
area: float = None
volume: float = None
def __init__(self, **data) -> None:
super().__init__(**data)
self.add_chunkable_attrs(
vertices=2000, faces=2000, colors=2000, textureCoordinates=2000
)
class Surface(Base, speckle_type=GEOMETRY + "Surface"):
degreeU: int = None
degreeV: int = None
rational: bool = None
area: float = None
pointData: List[float] = None
countU: int = None
countV: int = None
bbox: Box = None
class BrepFace(Base, speckle_type=GEOMETRY + "BrepFace"):
_Brep: "Brep" = None
SurfaceIndex: int = None
LoopIndices: List[int] = None
OuterLoopIndex: int = None
OrientationReversed: bool = None
@property
def _outer_loop(self):
return self._Brep.Loops[self.OuterLoopIndex]
@property
def _surface(self):
return self._Brep.Surfaces[self.SurfaceIndex]
@property
def _loops(self):
return [self._Brep.Loops[index] for index in self.LoopIndices]
class BrepEdge(Base, speckle_type=GEOMETRY + "BrepEdge"):
_Brep: "Brep" = None
Curve3dIndex: int = None
TrimIndices: List[int] = None
StartIndex: int = None
EndIndex: int = None
ProxyCurveIsReversed: bool = None
Domain: Interval = None
@property
def _start_vertex(self):
return self._Brep.Vertices[self.StartIndex]
@property
def _end_vertex(self):
return self._Brep.Vertices[self.EndIndex]
@property
def _trims(self):
return [self._Brep.Trims[i] for i in self.TrimIndices]
@property
def _curve(self):
return self._Brep.Curve3D[self.Curve3dIndex]
class BrepLoop(Base, speckle_type=GEOMETRY + "BrepLoop"):
_Brep: "Brep" = None
FaceIndex: int = None
TrimIndices: List[int] = None
Type: str = None
@property
def _face(self):
return self._Brep.Faces[self.FaceIndex]
@property
def _trims(self):
return [self._Brep.Trims[i] for i in self.TrimIndices]
class BrepTrim(Base, speckle_type=GEOMETRY + "BrepTrim"):
_Brep: "Brep" = None
EdgeIndex: int = None
StartIndex: int = None
EndIndex: int = None
FaceIndex: int = None
LoopIndex: int = None
CurveIndex: int = None
IsoStatus: int = None
TrimType: str = None
IsReversed: bool = None
Domain: Interval = None
@property
def _face(self):
return self._Brep.Faces[self.FaceIndex]
@property
def _loop(self):
return self._Brep.Loops[self.LoopIndex]
@property
def _edge(self):
return self._Brep.Edges[self.EdgeIndex] if self.EdgeIndex != -1 else None
@property
def _curve_2d(self):
return self._Brep.Curve2D[self.CurveIndex]
class Brep(Base, speckle_type=GEOMETRY + "Brep"):
provenance: str = None
bbox: Box = None
area: float = None
volume: float = None
displayValue: Mesh = None
Surfaces: List[Surface] = []
Curve3D: List[Base] = []
Curve2D: List[Base] = []
Vertices: List[Point] = []
Edges: List[BrepEdge] = []
Loops: List[BrepLoop] = []
Trims: List[BrepTrim] = []
Faces: List[BrepFace] = []
IsClosed: bool = None
Orientation: int = 0
def __init__(self, **data: Any) -> None:
super().__init__(**data)
self.add_detachable_attrs({"displayValue"})
self.add_chunkable_attrs(
Surfaces=200,
Curve3D=200,
Curve2D=200,
Vertices=5000,
Edges=5000,
Loops=5000,
Trims=5000,
Faces=5000,
)
def __setattr__(self, name: str, value: Any) -> None:
if not value:
return
if name in ["Edges", "Loops", "Trims", "Faces"]:
for val in value:
val._Brep = self
super().__setattr__(name, value)
BrepEdge.update_forward_refs()
BrepLoop.update_forward_refs()
BrepTrim.update_forward_refs()
BrepFace.update_forward_refs()
+12
View File
@@ -0,0 +1,12 @@
from .base import Base
OTHER = "Objects.Other."
class RenderMaterial(Base, speckle_type=OTHER + "RenderMaterial"):
name: str = None
opacity: float = 1
metalness: float = 0
roughness: float = 1
diffuse: int = -2894893 # light gray arbg
emissive: int = -16777216 # black arbg
+26
View File
@@ -0,0 +1,26 @@
from specklepy.logging.exceptions import SpeckleException
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"],
}
def get_units_from_string(unit: str):
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})."
)
@@ -1,12 +1,14 @@
import json
import hashlib
import re
from speckle import objects
from uuid import uuid4
from typing import Any, Dict, List, Tuple
from speckle.objects.base import Base, DataChunk
from speckle.logging.exceptions import SerializationException, SpeckleException
from speckle.transports.abstract_transport import AbstractTransport
from specklepy.objects.base import Base, DataChunk
from specklepy.logging.exceptions import SerializationException, SpeckleException
from specklepy.transports.abstract_transport import AbstractTransport
import specklepy.objects.geometry
import specklepy.objects.other
PRIMITIVES = (int, float, str, bool)
@@ -48,23 +50,37 @@ class BaseObjectSerializer:
self.detach_lineage = [True]
self.lineage.append(uuid4().hex)
object_builder = {"id": ""}
object_builder = {"id": "", "speckle_type": "Base", "totalChildrenCount": 0}
object_builder.update(speckle_type=base.speckle_type)
obj, props = base, base.get_member_names()
while props:
prop = props.pop(0)
value = obj[prop]
value = getattr(obj, prop, None)
chunkable = False
detach = False
# skip nulls or props marked to be ignored with "__" or "_"
if not value or prop.startswith(("__", "_")):
if value is None or prop.startswith(("__", "_")):
continue
# don't prepopulate id as this will mess up hashing
if prop == "id":
continue
chunkable = True if prop in base._chunkable else False
detach = True if prop.startswith("@") or chunkable else False
# only bother with chunking and detaching if there is a write transport
if self.write_transports:
dynamic_chunk_match = re.match(r"^@\((\d*)\)", prop)
if dynamic_chunk_match:
chunk_size = dynamic_chunk_match.groups()[0]
base._chunkable[prop] = (
int(chunk_size) if chunk_size else base._chunk_size_default
)
chunkable = prop in base._chunkable
detach = bool(
prop.startswith("@") or prop in base._detachable or chunkable
)
# 1. handle primitives (ints, floats, strings, and bools)
if isinstance(value, PRIMITIVES):
@@ -102,20 +118,24 @@ class BaseObjectSerializer:
# 4. handle all other cases
else:
child_obj = self.traverse_value(value)
child_obj = self.traverse_value(value, detach)
object_builder[prop] = child_obj
hash = hash_obj(object_builder)
object_builder["id"] = hash
closure = {}
# add closures & children count to the object
detached = self.detach_lineage.pop()
# add closures to the object
if self.lineage[-1] in self.family_tree:
object_builder["__closure"] = self.closure_table[hash] = {
closure = {
ref: depth - len(self.detach_lineage)
for ref, depth in self.family_tree[self.lineage[-1]].items()
}
object_builder["totalChildrenCount"] = len(closure)
hash = hash_obj(object_builder)
object_builder["id"] = hash
if closure:
object_builder["__closure"] = self.closure_table[hash] = closure
# write detached or root objects to transports
if detached and self.write_transports:
@@ -139,7 +159,18 @@ class BaseObjectSerializer:
return obj
elif isinstance(obj, (list, tuple, set)):
return [self.traverse_value(o) for o in obj]
if not detach:
return [self.traverse_value(o) for o in obj]
detached_list = []
for o in obj:
if isinstance(o, Base):
self.detach_lineage.append(detach)
hash, _ = self.traverse_base(o)
detached_list.append(self.detach_helper(ref_hash=hash))
else:
detached_list.append(self.traverse_value(o, detach))
return detached_list
elif isinstance(obj, dict):
for k, v in obj.items():
@@ -206,9 +237,7 @@ class BaseObjectSerializer:
if not obj_string:
return None
obj = json.loads(obj_string)
base = self.recompose_base(obj=obj)
return base
return self.recompose_base(obj=obj)
def recompose_base(self, obj: dict) -> Base:
"""Steps through a base object dictionary and recomposes the base object
@@ -224,12 +253,20 @@ class BaseObjectSerializer:
return
if isinstance(obj, str):
obj = json.loads(obj)
if obj["speckle_type"] == "reference":
if "speckle_type" in obj and obj["speckle_type"] == "reference":
obj = self.get_child(obj=obj)
# initialise the base object using `speckle_type`
base = getattr(objects, obj["speckle_type"], Base)()
speckle_type = obj.get("speckle_type")
# if speckle type is not in the object definition, it is treated as a dict
if not speckle_type:
return obj
# get the registered type from base register.
object_type = Base.get_registered_type(speckle_type)
# initialise the base object using `speckle_type` fall back to base if needed
base = object_type() if object_type else Base(speckle_type=speckle_type)
# get total children count
if "__closure" in obj:
if not self.read_transport:
@@ -240,9 +277,9 @@ class BaseObjectSerializer:
base.totalChildrenCount = len(closure)
for prop, value in obj.items():
# 1. handle primitives (ints, floats, strings, and bools)
if isinstance(value, PRIMITIVES):
base[prop] = value
# 1. handle primitives (ints, floats, strings, and bools) or None
if isinstance(value, PRIMITIVES) or value is None:
base.__setattr__(prop, value)
continue
# 2. handle referenced child objects
@@ -254,11 +291,11 @@ class BaseObjectSerializer:
f"Could not find the referenced child object of id `{ref_hash}` in the given read transport: {self.read_transport.name}"
)
ref_obj = json.loads(ref_obj_str)
base[prop] = self.recompose_base(obj=ref_obj)
base.__setattr__(prop, self.recompose_base(obj=ref_obj))
# 3. handle all other cases (base objects, lists, and dicts)
else:
base[prop] = self.handle_value(value)
base.__setattr__(prop, self.handle_value(value))
return base
@@ -271,20 +308,25 @@ class BaseObjectSerializer:
Returns:
Any -- the handled value (primitive, list, dictionary, or Base)
"""
if not obj:
return obj
if isinstance(obj, PRIMITIVES):
return obj
# lists (regular and chunked)
if isinstance(obj, list):
obj_list = [self.handle_value(o) for o in obj]
# handle chunked lists
if isinstance(obj_list[0], DataChunk):
if (
hasattr(obj_list[0], "speckle_type")
and "DataChunk" in obj_list[0].speckle_type
):
# handle chunked lists
data = []
for o in obj_list:
data.extend(o["data"])
return data
else:
return obj_list
return obj_list
# bases
if isinstance(obj, dict) and "speckle_type" in obj:
@@ -1,5 +1,5 @@
from abc import ABC, abstractmethod
from typing import Any, Optional
from typing import Any, Optional, List, Dict
from pydantic import BaseModel
from pydantic.main import Extra
@@ -64,6 +64,18 @@ class AbstractTransport(ABC, BaseModel):
"""
pass
@abstractmethod
def has_objects(self, id_list: List[str]) -> Dict[str, bool]:
"""Checks the presence of multiple objects.
Arguments:
id_list -- List of object id to be checked
Returns:
Dict[str, bool] -- keys: input ids, values: whether the transport has that object
"""
pass
@abstractmethod
def copy_object_and_children(
self, id: str, target_transport: "AbstractTransport"
@@ -1,7 +1,7 @@
import json
from typing import Any
from speckle.logging.exceptions import SpeckleException
from speckle.transports.abstract_transport import AbstractTransport
from typing import Any, List, Dict
from specklepy.logging.exceptions import SpeckleException
from specklepy.transports.abstract_transport import AbstractTransport
class MemoryTransport(AbstractTransport):
@@ -33,6 +33,9 @@ class MemoryTransport(AbstractTransport):
else:
return None
def has_objects(self, id_list: List[str]) -> Dict[str, bool]:
return {id: (id in self.objects) for id in id_list}
def begin_write(self) -> None:
self.saved_object_count = 0
+1
View File
@@ -0,0 +1 @@
from .server import ServerTransport
+141
View File
@@ -0,0 +1,141 @@
import json
import logging
import threading
import queue
import time
import gzip
import requests
from specklepy.logging.exceptions import SpeckleException
LOG = logging.getLogger(__name__)
class BatchSender(object):
def __init__(
self,
server_url,
stream_id,
token,
max_batch_size_mb=1,
batch_buffer_length=10,
thread_count=4,
):
self.server_url = server_url
self.stream_id = stream_id
self._token = token
self.max_size = int(max_batch_size_mb * 1000 * 1000)
self._batches = queue.Queue(batch_buffer_length)
self._crt_batch = []
self._crt_batch_size = 0
self.thread_count = thread_count
self._send_threads = []
self._exception = None
def send_object(self, id: str, obj: str):
if not self._send_threads:
self._create_threads()
crt_obj_size = len(obj)
if not self._crt_batch or self._crt_batch_size + crt_obj_size < self.max_size:
self._crt_batch.append((id, obj))
self._crt_batch_size += crt_obj_size
return
self._batches.put(self._crt_batch)
self._crt_batch = [(id, obj)]
self._crt_batch_size = crt_obj_size
def flush(self):
# Add current non-complete batch
if self._crt_batch:
self._batches.put(self._crt_batch)
self._crt_batch = []
self._crt_batch_size = 0
# Wait for queued batches to be sent
self._batches.join()
# End the sending threads
self._delete_threads()
# If there was any error, throw the first exception that occurred during upload
if self._exception is not None:
ex = self._exception
self._exception = None
raise ex
def _sending_thread_main(self):
try:
session = requests.Session()
session.headers.update(
{"Authorization": f"Bearer {self._token}", "Accept": "text/plain"}
)
while True:
batch = self._batches.get()
# None is a sentinel value, meaning the thread should exit gracefully
if batch is None:
self._batches.task_done()
break
try:
self._bg_send_batch(session, batch)
except Exception as ex:
self._exception = self._exception or ex
LOG.error("Error sending batch of objects to server: " + str(ex))
self._batches.task_done()
except Exception as ex:
self._exception = self._exception or ex
LOG.error("ServerTransport sending thread error: " + str(ex))
def _bg_send_batch(self, session, batch):
object_ids = [obj[0] for obj in batch]
server_has_object = session.post(
url=f"{self.server_url}/api/diff/{self.stream_id}",
data={"objects": json.dumps(object_ids)},
).json()
new_object_ids = [x for x in object_ids if not server_has_object[x]]
new_object_ids = set(new_object_ids)
new_objects = [obj[1] for obj in batch if obj[0] in new_object_ids]
if not new_objects:
LOG.info(f"Uploading batch of {len(batch)} objects: all objects are already in the server")
return
upload_data = "[" + ",".join(new_objects) + "]"
upload_data_gzip = gzip.compress(upload_data.encode())
LOG.info(
"Uploading batch of %s objects (%s new): (size: %s, compressed size: %s)"
% (len(batch), len(new_objects), len(upload_data), len(upload_data_gzip))
)
r = session.post(
url=f"{self.server_url}/objects/{self.stream_id}",
files={"batch-1": ("batch-1", upload_data_gzip, "application/gzip")},
)
if r.status_code != 201:
LOG.warning("Upload server response: %s", r.text)
raise SpeckleException(
message=f"Could not save the object to the server - status code {r.status_code}"
)
def _create_threads(self):
for i in range(self.thread_count):
t = threading.Thread(target=self._sending_thread_main, daemon=True)
t.start()
self._send_threads.append(t)
def _delete_threads(self):
for i in range(len(self._send_threads)):
self._batches.put(None)
for thread in self._send_threads:
thread.join()
self._send_threads = []
def __del__(self):
self._delete_threads()
@@ -1,10 +1,15 @@
import json
import time
import requests
from asyncio import Queue, Task
from typing import Any, Dict, List, Type
from speckle.api.client import SpeckleClient
from speckle.logging.exceptions import SpeckleException
from speckle.transports.abstract_transport import AbstractTransport
from specklepy.api.client import SpeckleClient
from specklepy.logging.exceptions import SpeckleException
from specklepy.transports.abstract_transport import AbstractTransport
from .batch_sender import BatchSender
class ServerTransport(AbstractTransport):
@@ -13,36 +18,33 @@ class ServerTransport(AbstractTransport):
stream_id: str = None
saved_obj_count: int = 0
session: requests.Session = None
__queue: Queue = None
__workers: List[Task] = []
def __init__(self, client: SpeckleClient, stream_id: str, **data: Any) -> None:
super().__init__(**data)
# TODO: replace client with account or some other auth avenue
if not client.me:
raise SpeckleException("The provided SpeckleClient was not authenticated.")
self.url = client.url
self.stream_id = stream_id
token = client.me["token"]
self._batch_sender = BatchSender(
self.url, self.stream_id, token, max_batch_size_mb=1
)
self.session = requests.Session()
self.session.headers.update(
{"Authorization": f"Bearer {client.me['token']}", "Accept": "text/plain"}
{"Authorization": f"Bearer {token}", "Accept": "text/plain"}
)
def begin_write(self) -> None:
self.saved_obj_count = 0
def end_write(self) -> None:
pass
self._batch_sender.flush()
# TODO: add save task to queue and process as the root is being deserialised
def save_object(self, id: str, serialized_object: str) -> None:
endpoint = f"{self.url}/objects/{self.stream_id}"
r = self.session.post(
url=endpoint,
files={"batch-1": ("batch-1", f"[{serialized_object}]")},
)
if r.status_code != 201:
raise SpeckleException(
message=f"Could not save the object to the server - status code {r.status_code}"
)
self._batch_sender.send_object(id, serialized_object)
def save_object_from_transport(
self, id: str, source_transport: AbstractTransport
@@ -63,26 +65,50 @@ class ServerTransport(AbstractTransport):
NotImplementedError,
)
def has_objects(self, id_list: List[str]) -> Dict[str, bool]:
return {id: False for id in id_list}
def copy_object_and_children(
self, id: str, target_transport: AbstractTransport
) -> str:
endpoint = f"{self.url}/objects/{self.stream_id}/{id}"
r = self.session.get(endpoint, stream=True)
endpoint = f"{self.url}/objects/{self.stream_id}/{id}/single"
r = self.session.get(endpoint)
if r.encoding is None:
r.encoding = "utf-8"
if r.status_code != 200:
raise SpeckleException(
f"Can't get object {self.stream_id}/{id}: HTTP error {r.status_code} ({r.text[:1000]})"
)
root_obj_serialized = r.text
root_obj = json.loads(root_obj_serialized)
closures = root_obj.get("__closure", {})
# Check which children are not already in the target transport
children_ids = list(closures.keys())
children_found_map = target_transport.has_objects(children_ids)
new_children_ids = [
id for id in children_found_map if not children_found_map[id]
]
# Get the new children
endpoint = f"{self.url}/api/getobjects/{self.stream_id}"
r = self.session.post(
endpoint, data={"objects": json.dumps(new_children_ids)}, stream=True
)
if r.encoding is None:
r.encoding = "utf-8"
lines = r.iter_lines(decode_unicode=True)
# save first (root) obj for return
root_hash, root_obj = next(lines).split("\t")
target_transport.save_object(root_hash, root_obj)
# iter through returned objects saving them as we go
for line in lines:
if line:
hash, obj = line.split("\t")
target_transport.save_object(hash, obj)
return root_obj
target_transport.save_object(id, root_obj_serialized)
return root_obj_serialized
# async def stream_res(self, endpoint: str) -> str:
# data = b""
@@ -100,4 +126,4 @@ class ServerTransport(AbstractTransport):
# break
# data += chunk
# return data.decode("utf-8")
# return data.decode("utf-8")
@@ -3,22 +3,21 @@ import sys
import time
import sched
import sqlite3
from typing import Any
from typing import Any, List, Dict
from appdirs import user_data_dir
from contextlib import closing
from multiprocessing import Process, Queue
from speckle.transports.abstract_transport import AbstractTransport
from speckle.logging.exceptions import SpeckleException
from specklepy.transports.abstract_transport import AbstractTransport
from specklepy.logging.exceptions import SpeckleException
class SQLiteTransport(AbstractTransport):
_name = "SQLite"
_base_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
__queue: Queue = Queue()
app_name: str = ""
scope: str = ""
saved_obj_count: int = 0
@@ -33,21 +32,29 @@ class SQLiteTransport(AbstractTransport):
super().__init__(**data)
self.app_name = app_name or "Speckle"
self.scope = scope or "Objects"
base_path = base_path or self.__get_base_path()
self._base_path = base_path or self.__get_base_path()
os.makedirs(base_path, exist_ok=True)
try:
os.makedirs(self._base_path, exist_ok=True)
self._root_path = os.path.join(os.path.join(base_path, f"{self.scope}.db"))
self.__initialise()
self._root_path = os.path.join(
os.path.join(self._base_path, f"{self.scope}.db")
)
self.__initialise()
except Exception as ex:
raise SpeckleException(
f"SQLiteTransport could not initialise {self.scope}.db at {self._base_path}. Either provide a different `base_path` or use an alternative transport.",
ex,
)
def __repr__(self) -> str:
return f"SQLiteTransport(app: '{self.app_name}', scope: '{self.scope}')"
def __write_timer_elapsed(self):
print("WRITE TIMER ELAPSED")
proc = Process(target=_run_queue, args=(self.__queue, self._root_path))
proc.start()
proc.join()
# def __write_timer_elapsed(self):
# print("WRITE TIMER ELAPSED")
# proc = Process(target=_run_queue, args=(self.__queue, self._root_path))
# proc.start()
# proc.join()
def __get_base_path(self):
# from appdirs https://github.com/ActiveState/appdirs/blob/master/appdirs.py
@@ -60,56 +67,55 @@ class SQLiteTransport(AbstractTransport):
if os_name.startswith("Mac"):
system = "darwin"
if system == "darwin":
path = os.path.expanduser("~/.config/")
return os.path.join(path, self.app_name)
else:
if system != "darwin":
return user_data_dir(appname=self.app_name, appauthor=False, roaming=True)
def __consume_queue(self):
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_sync(data[0], data[1])
self._is_writing = False
path = os.path.expanduser("~/.config/")
return os.path.join(path, self.app_name)
self._scheduler.enter(
delay=self._polling_interval, priority=1, action=self.__consume_queue
)
self._scheduler.run(blocking=True)
# def __consume_queue(self):
# 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
def save_object(self, id: str, serialized_object: str) -> None:
"""Adds an object to the queue and schedules it to be saved.
# self._scheduler.enter(
# delay=self._polling_interval, priority=1, action=self.__consume_queue
# )
# self._scheduler.run(blocking=True)
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))
# def save_object(self, id: str, serialized_object: str) -> None:
# """Adds an object to the queue and schedules it to be saved.
self._scheduler.enter(
delay=self._polling_interval, priority=1, action=self.__consume_queue
)
self._scheduler.run(blocking=True)
# 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(
self, id: str, source_transport: AbstractTransport
) -> None:
"""Adds an object from the given transport to the queue and schedules it to be saved.
"""Adds an object from the given transport to the the local db
Arguments:
id {str} -- the object id
source_transport {AbstractTransport) -- the transport through which the object can be found
"""
serialized_object = source_transport.get_object(id)
self.__queue.put((id, serialized_object))
raise NotImplementedError
self.save_object(id, serialized_object)
def save_object_sync(self, id: str, serialized_object: str) -> None:
def save_object(self, id: str, serialized_object: str) -> None:
"""Directly saves an object into the database.
Arguments:
@@ -137,6 +143,17 @@ class SQLiteTransport(AbstractTransport):
).fetchone()
return row[1] if row else None
def has_objects(self, id_list: List[str]) -> Dict[str, bool]:
ret = {}
self.__check_connection()
with closing(self.__connection.cursor()) as c:
for id in id_list:
row = c.execute(
"SELECT 1 FROM objects WHERE hash = ? LIMIT 1", (id,)
).fetchone()
ret[id] = bool(row)
return ret
def begin_write(self):
self.saved_obj_count = 0
@@ -183,17 +200,17 @@ class SQLiteTransport(AbstractTransport):
self.__connection.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()
# 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()
View File
+105
View File
@@ -0,0 +1,105 @@
import uuid
import random
import pytest
import requests
from specklepy.api.models import Stream
from specklepy.api.client import SpeckleClient
from specklepy.objects.base import Base
from specklepy.objects.geometry import Point
from specklepy.objects.fakemesh import FakeMesh
@pytest.fixture(scope="session")
def host():
return "localhost:3000"
def seed_user(host):
seed = uuid.uuid4().hex
user_dict = {
"email": f"{seed[0:7]}@spockle.com",
"password": "$uper$3cr3tP@ss",
"name": f"{seed[0:7]} Name",
"company": "test spockle",
}
r = requests.post(
url=f"http://{host}/auth/local/register?challenge=pyspeckletests",
data=user_dict,
)
print(r.url)
access_code = r.url.split("access_code=")[1]
r_tokens = requests.post(
url=f"http://{host}/auth/token",
json={
"appSecret": "spklwebapp",
"appId": "spklwebapp",
"accessCode": access_code,
"challenge": "pyspeckletests",
},
)
user_dict.update(**r_tokens.json())
return user_dict
@pytest.fixture(scope="session")
def user_dict(host):
return seed_user(host)
@pytest.fixture(scope="session")
def second_user_dict(host):
return seed_user(host)
@pytest.fixture(scope="session")
def client(host, user_dict):
client = SpeckleClient(host=host, use_ssl=False)
client.authenticate(user_dict["token"])
return client
@pytest.fixture(scope="session")
def sample_stream(client):
stream = Stream(
name="a sample stream for testing",
description="a stream created for testing",
isPublic=True,
)
stream.id = client.stream.create(stream.name, stream.description, stream.isPublic)
return stream
@pytest.fixture(scope="session")
def mesh():
mesh = FakeMesh()
mesh.name = "my_mesh"
mesh.vertices = [random.uniform(0, 10) for _ in range(1, 210)]
mesh.faces = [i for i in range(1, 210)]
mesh["@(100)colours"] = [random.uniform(0, 10) for _ in range(1, 210)]
mesh["@()default_chunk"] = [random.uniform(0, 10) for _ in range(1, 210)]
mesh.test_bases = [Base(name=f"test {i}") for i in range(1, 22)]
mesh.detach_this = Base(name="predefined detached base")
mesh["@detach"] = Base(name="detached base")
mesh["@detached_list"] = [
42,
"some text",
[1, 2, 3],
Base(name="detached within a list"),
]
mesh.origin = Point(value=[4, 2, 0])
return mesh
@pytest.fixture(scope="session")
def base():
base = Base()
base.name = "my_base"
base.units = "millimetres"
base.vertices = [random.uniform(0, 10) for _ in range(1, 120)]
base.test_bases = [Base(name=i) for i in range(1, 22)]
base["@detach"] = Base(name="detached base")
return base
+74
View File
@@ -0,0 +1,74 @@
import pytest
from specklepy.objects import Base
from specklepy.api import operations
from contextlib import ExitStack as does_not_raise
@pytest.mark.parametrize(
"invalid_prop_name",
[
(""),
("@"),
("@@wow"),
("this.is.bad"),
("super/bad"),
],
)
def test_empty_prop_names(invalid_prop_name: str) -> None:
base = Base()
with pytest.raises(ValueError):
base[invalid_prop_name] = "🐛️"
class FakeModel(Base):
"""Just a test class type."""
foo: str = ""
def test_new_type_registration() -> None:
"""Test if a new subclass is registered into the type register."""
assert Base.get_registered_type("FakeModel") == FakeModel
assert Base.get_registered_type("🐺️") is None
def test_fake_base_serialization() -> None:
fake_model = FakeModel(foo="bar")
serialized = operations.serialize(fake_model)
deserialized = operations.deserialize(serialized)
assert fake_model.get_id() == deserialized.get_id()
def test_duplicate_speckle_type_raises_error():
with pytest.raises(ValueError):
class NaughtyClass(Base, speckle_type="Base"):
"""This class has a speckle_type that is already taken."""
@pytest.mark.parametrize(
"forbidden_attribute_name, expectation",
[
("", pytest.raises(ValueError)),
("@", pytest.raises(ValueError)),
("@@", pytest.raises(ValueError)),
("im.cheeky", pytest.raises(ValueError)),
("im.cheeky", pytest.raises(ValueError)),
("imgood", does_not_raise()),
],
)
def test_attribute_name_validation(
forbidden_attribute_name: str,
expectation,
base: Base,
):
with expectation:
base[forbidden_attribute_name] = None
def test_speckle_type_cannot_be_set(base: Base) -> None:
assert base.speckle_type == "Base"
base.speckle_type = "unset"
assert base.speckle_type == "Base"
+82
View File
@@ -0,0 +1,82 @@
import pytest
from specklepy.api import operations
from specklepy.transports.server import ServerTransport
from specklepy.api.models import Branch, Commit, Stream
class TestBranch:
@pytest.fixture(scope="module")
def branch(self):
return Branch(name="olive branch 🌿", description="a test branch")
@pytest.fixture(scope="module")
def updated_branch(self):
return Branch(name="eucalyptus branch 🌿", description="an updated test branch")
@pytest.fixture(scope="module")
def stream(self, client):
stream = Stream(
name="a sample stream for testing",
description="a stream created for testing",
isPublic=True,
)
stream.id = client.stream.create(
stream.name, stream.description, stream.isPublic
)
return stream
def test_branch_create(self, client, stream, branch):
branch.id = client.branch.create(
stream_id=stream.id, name=branch.name, description=branch.description
)
assert isinstance(branch.id, str)
def test_branch_get(self, client, mesh, stream, branch):
transport = ServerTransport(client=client, stream_id=stream.id)
mesh.id = operations.send(mesh, transports=[transport])
client.commit.create(
stream_id=stream.id,
branch_name=branch.name,
object_id=mesh.id,
message="a commit for testing branch get",
)
fetched_branch = client.branch.get(stream_id=stream.id, name=branch.name)
assert isinstance(fetched_branch, Branch)
assert fetched_branch.name == branch.name
assert fetched_branch.description == branch.description
assert isinstance(fetched_branch.commits.items, list)
assert isinstance(fetched_branch.commits.items[0], Commit)
def test_branch_list(self, client, stream, branch):
branches = client.branch.list(stream_id=stream.id)
print(branches)
assert isinstance(branches, list)
assert len(branches) == 2
assert isinstance(branches[0], Branch)
assert branches[0].name == branch.name
def test_branch_update(self, client, stream, branch, updated_branch):
updated = client.branch.update(
stream_id=stream.id,
branch_id=branch.id,
name=updated_branch.name,
description=updated_branch.description,
)
fetched_branch = client.branch.get(
stream_id=stream.id, name=updated_branch.name
)
assert updated is True
assert fetched_branch.name == updated_branch.name
assert fetched_branch.description == updated_branch.description
def test_branch_delete(self, client, stream, branch):
deleted = client.branch.delete(stream_id=stream.id, branch_id=branch.id)
assert deleted is True
+70
View File
@@ -0,0 +1,70 @@
import pytest
from specklepy.api import operations
from specklepy.api.models import Commit, Stream
from specklepy.transports.server.server import ServerTransport
@pytest.mark.run(order=4)
class TestCommit:
@pytest.fixture(scope="module")
def commit(self):
return Commit(message="a fun little test commit")
@pytest.fixture(scope="module")
def updated_commit(
self,
):
return Commit(message="a fun little updated commit")
@pytest.fixture(scope="module")
def stream(self, client):
stream = Stream(
name="a sample stream for testing",
description="a stream created for testing",
isPublic=True,
)
stream.id = client.stream.create(
stream.name, stream.description, stream.isPublic
)
return stream
def test_commit_create(self, client, stream, mesh, commit):
transport = ServerTransport(client=client, stream_id=stream.id)
mesh.id = operations.send(mesh, transports=[transport])
commit.id = client.commit.create(
stream_id=stream.id, object_id=mesh.id, message=commit.message
)
assert isinstance(commit.id, str)
def test_commit_get(self, client, stream, mesh, commit):
fetched_commit = client.commit.get(stream_id=stream.id, commit_id=commit.id)
assert fetched_commit.message == commit.message
assert fetched_commit.referencedObject == mesh.id
def test_commit_list(self, client, stream):
commits = client.commit.list(stream_id=stream.id)
assert isinstance(commits, list)
assert isinstance(commits[0], Commit)
def test_commit_update(self, client, stream, commit, updated_commit):
updated = client.commit.update(
stream_id=stream.id, commit_id=commit.id, message=updated_commit.message
)
fetched_commit = client.commit.get(stream_id=stream.id, commit_id=commit.id)
assert updated is True
assert fetched_commit.message == updated_commit.message
def test_commit_delete(self, client, stream, mesh):
commit_id = client.commit.create(
stream_id=stream.id, object_id=mesh.id, message="a great commit to delete"
)
deleted = client.commit.delete(stream_id=stream.id, commit_id=commit_id)
assert deleted is True
+38
View File
@@ -0,0 +1,38 @@
from specklepy.objects import Base
from specklepy.transports.memory import MemoryTransport
from specklepy.api.models import Stream
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
import pytest
class TestObject:
@pytest.fixture(scope="module")
def stream(self, client):
stream = Stream(
name="a sample stream for testing",
description="a stream created for testing",
isPublic=True,
)
stream.id = client.stream.create(
stream.name, stream.description, stream.isPublic
)
return stream
def test_object_create(self, client, stream, base):
transport = MemoryTransport()
s = BaseObjectSerializer(write_transports=[transport], read_transport=transport)
_, base_dict = s.traverse_base(base)
obj_id = client.object.create(stream_id=stream.id, objects=[base_dict])[0]
assert isinstance(obj_id, str)
assert obj_id == base.get_id(True)
def test_object_get(self, client, stream, base):
fetched_base = client.object.get(
stream_id=stream.id, object_id=base.get_id(True)
)
assert isinstance(fetched_base, Base)
assert fetched_base.name == base.name
assert isinstance(fetched_base.vertices, list)
assert fetched_base["@detach"]["speckle_type"] == "reference"
+86
View File
@@ -0,0 +1,86 @@
import json
from attr import has
import pytest
from specklepy.api import operations
from specklepy.transports.server import ServerTransport
from specklepy.transports.memory import MemoryTransport
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
from specklepy.objects import Base
from specklepy.objects.geometry import Point
from specklepy.objects.fakemesh import FakeMesh
@pytest.mark.run(order=3)
class TestSerialization:
def test_serialize(self, base):
serialized = operations.serialize(base)
deserialized = operations.deserialize(serialized)
assert base.get_id() == deserialized.get_id()
assert base.units == "mm"
assert isinstance(base.test_bases[0], Base)
assert base["@detach"].name == deserialized["@detach"].name
def test_detaching(self, mesh):
transport = MemoryTransport()
serialized = operations.serialize(mesh, [transport])
deserialized = operations.deserialize(serialized, transport)
serialized_dict = json.loads(serialized)
assert serialized_dict["detach_this"]["speckle_type"] == "reference"
assert serialized_dict["@detach"]["speckle_type"] == "reference"
assert serialized_dict["origin"]["speckle_type"] == "reference"
assert serialized_dict["@detached_list"][-1]["speckle_type"] == "reference"
assert mesh.get_id() == deserialized.get_id()
def test_chunking(self, mesh):
transport = MemoryTransport()
serialized = operations.serialize(mesh, [transport])
deserialized = operations.deserialize(serialized, transport)
serialized_dict = json.loads(serialized)
assert len(serialized_dict["vertices"]) == 3
assert len(serialized_dict["@(100)colours"]) == 3
assert len(serialized_dict["@()default_chunk"]) == 1
assert serialized_dict["vertices"][0]["speckle_type"] == "reference"
assert serialized_dict["@(100)colours"][0]["speckle_type"] == "reference"
assert serialized_dict["@()default_chunk"][0]["speckle_type"] == "reference"
assert mesh.get_id() == deserialized.get_id()
def test_send_and_receive(self, client, sample_stream, mesh):
transport = ServerTransport(client=client, stream_id=sample_stream.id)
hash = operations.send(mesh, transports=[transport])
# use a fresh memory transport to force receiving from remote
received = operations.receive(
hash, remote_transport=transport, local_transport=MemoryTransport()
)
assert isinstance(received, FakeMesh)
assert received.vertices == mesh.vertices
assert isinstance(received.origin, Point)
assert received.origin.value == mesh.origin.value
# not comparing hashes as order is not guaranteed back from server
mesh.id = hash # populate with decomposed id for use in proceeding tests
def test_receive_local(self, client, mesh):
hash = operations.send(mesh) # defaults to SQLiteTransport
received = operations.receive(hash)
assert isinstance(received, Base)
assert mesh.get_id() == received.get_id()
def test_unknown_type(self):
unknown = '{"speckle_type": "mysterious.type"}'
deserialised = operations.deserialize(unknown)
assert isinstance(deserialised, Base)
assert deserialised.speckle_type == "mysterious.type"
def test_no_speckle_type(self):
untyped = '{"foo": "bar"}'
deserialised = operations.deserialize(untyped)
assert deserialised == {"foo": "bar"}
+39
View File
@@ -0,0 +1,39 @@
import pytest
from specklepy.api.models import ServerInfo
class TestServer:
@pytest.fixture(scope="module")
def token_info(self):
return {
"token": None,
"name": "super secret token",
"scopes": ["streams:read", "streams:write"],
"lifespan": 9001,
}
def test_server_get(self, client):
server = client.server.get()
assert isinstance(server, ServerInfo)
def test_server_apps(self, client):
apps = client.server.apps()
assert isinstance(apps, list)
assert len(apps) >= 1
assert any(app["name"] == "Speckle Web Manager" for app in apps)
def test_server_create_token(self, client, token_info):
token_info["token"] = client.server.create_token(
name=token_info["name"],
scopes=token_info["scopes"],
lifespan=token_info["lifespan"],
)
assert isinstance(token_info["token"], str)
def test_server_revoke_token(self, client, token_info):
revoked = client.server.revoke_token(token=token_info["token"])
assert revoked is True
+99
View File
@@ -0,0 +1,99 @@
import pytest
from specklepy.api.models import Stream
from specklepy.logging.exceptions import GraphQLException
@pytest.mark.run(order=2)
class TestStream:
@pytest.fixture(scope="session")
def stream(self):
return Stream(
name="a wonderful stream",
description="a stream created for testing",
isPublic=True,
)
@pytest.fixture(scope="module")
def updated_stream(
self,
):
return Stream(
name="a wonderful updated stream",
description="an updated stream description for testing",
isPublic=False,
)
def test_stream_create(self, client, stream, updated_stream):
stream.id = updated_stream.id = client.stream.create(
name=stream.name,
description=stream.description,
is_public=stream.isPublic,
)
assert isinstance(stream.id, str)
def test_stream_get(self, client, stream):
fetched_stream = client.stream.get(stream.id)
assert fetched_stream.name == stream.name
assert fetched_stream.description == stream.description
assert fetched_stream.isPublic == stream.isPublic
def test_stream_update(self, client, updated_stream):
updated = client.stream.update(
id=updated_stream.id,
name=updated_stream.name,
description=updated_stream.description,
is_public=updated_stream.isPublic,
)
fetched_stream = client.stream.get(updated_stream.id)
assert updated is True
assert fetched_stream.name == updated_stream.name
assert fetched_stream.description == updated_stream.description
assert fetched_stream.isPublic == updated_stream.isPublic
def test_stream_list(self, client):
client.stream.create(name="a second wonderful stream")
client.stream.create(name="a third fantastic stream")
streams = client.stream.list()
assert len(streams) >= 3
def test_stream_search(self, client, updated_stream):
search_results = client.stream.search(updated_stream.name)
assert len(search_results) == 1
assert search_results[0].name == updated_stream.name
def test_stream_grant_permission(self, client, stream, second_user_dict):
granted = client.stream.grant_permission(
stream_id=stream.id,
user_id=second_user_dict["id"],
role="stream:contributor",
)
fetched_stream = client.stream.get(stream.id)
assert granted is True
assert len(fetched_stream.collaborators) == 2
assert fetched_stream.collaborators[0].name == second_user_dict["name"]
def test_stream_revoke_permission(self, client, stream, second_user_dict):
revoked = client.stream.revoke_permission(
stream_id=stream.id, user_id=second_user_dict["id"]
)
fetched_stream = client.stream.get(stream.id)
assert revoked == True
assert len(fetched_stream.collaborators) == 1
def test_stream_delete(self, client, stream):
deleted = client.stream.delete(stream.id)
stream_get = client.stream.get(stream.id)
assert deleted is True
assert isinstance(stream_get, GraphQLException)
+45
View File
@@ -0,0 +1,45 @@
from specklepy.logging.exceptions import SpeckleException
from specklepy.api.models import User
import pytest
@pytest.mark.run(order=1)
class TestUser:
def test_user_get_self(self, client, user_dict):
fetched_user = client.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_search(self, client, second_user_dict):
search_results = client.user.search(search_query=second_user_dict["name"][:5])
assert isinstance(search_results, list)
assert isinstance(search_results[0], User)
assert search_results[0].name == second_user_dict["name"]
second_user_dict["id"] = search_results[0].id
def test_user_get(self, client, second_user_dict):
fetched_user = client.user.get(id=second_user_dict["id"])
assert isinstance(fetched_user, User)
assert fetched_user.name == second_user_dict["name"]
assert fetched_user.email == second_user_dict["email"]
second_user_dict["id"] = fetched_user.id
def test_user_update(self, client):
bio = "i am a ghost in the machine"
failed_update = client.user.update()
updated = client.user.update(bio=bio)
me = client.user.get()
assert isinstance(failed_update, SpeckleException)
assert updated is True
assert me.bio == bio
+41
View File
@@ -0,0 +1,41 @@
import pytest
from specklepy.api.credentials import StreamWrapper
class TestWrapper:
def test_parse_stream(self):
wrap = StreamWrapper("https://testing.speckle.dev/streams/a75ab4f10f")
assert wrap.type == "stream"
def test_parse_branch(self):
wacky_wrap = StreamWrapper(
"https://testing.speckle.dev/streams/4c3ce1459c/branches/%F0%9F%8D%95%E2%AC%85%F0%9F%8C%9F%20you%20wat%3F"
)
wrap = StreamWrapper(
"https://testing.speckle.dev/streams/4c3ce1459c/branches/next%20level"
)
assert wacky_wrap.type == "branch"
assert wacky_wrap.branch_name == "🍕⬅🌟 you wat?"
assert wrap.type == "branch"
def test_parse_commit(self):
wrap = StreamWrapper(
"https://testing.speckle.dev/streams/4c3ce1459c/commits/8b9b831792"
)
assert wrap.type == "commit"
def test_parse_object(self):
wrap = StreamWrapper(
"https://testing.speckle.dev/streams/a75ab4f10f/objects/5530363e6d51c904903dafc3ea1d2ec6"
)
assert wrap.type == "object"
def test_parse_globals_as_branch(self):
wrap = StreamWrapper("https://testing.speckle.dev/streams/0c6ad366c4/globals/")
assert wrap.type == "branch"
def test_parse_globals_as_commit(self):
wrap = StreamWrapper(
"https://testing.speckle.dev/streams/0c6ad366c4/globals/abd3787893"
)
assert wrap.type == "commit"