Compare commits

..

27 Commits

Author SHA1 Message Date
Gergő Jedlicska b50e658333 Merge pull request #228 from specklesystems/gergo/noDiskAccess
make sure specklepy send works completely without disk write access
2022-11-01 20:50:14 +01:00
Gergő Jedlicska 88248353ab make sure specklepy send works completely without disk write access 2022-11-01 16:43:17 +01:00
Alan Rynne aec94f8f7f Merge pull request #225 from specklesystems/gergo/limited_user_query
deprecate user resoure, add new active_user and other_user resources
2022-10-28 10:06:12 +02:00
Gergő Jedlicska e6b1604bc3 fix seed user redirects 2022-10-26 16:45:24 +02:00
Gergő Jedlicska de29b93b8b fix test ordering 2022-10-26 14:49:12 +02:00
Gergő Jedlicska 10aa8b59b6 update lock file 2022-10-26 14:43:22 +02:00
Gergő Jedlicska b86faa6a14 remove pytest sugar, its broken on circleci 2022-10-26 14:34:23 +02:00
Gergő Jedlicska 7430611c52 remove old dev dependency syntax 2022-10-26 14:31:04 +02:00
Gergő Jedlicska ddd52f4af9 deprecate user resoure, add new active_user and other_user resources 2022-10-26 11:30:04 +02:00
Gergő Jedlicska 35bc6b0350 Merge pull request #223 from specklesystems/gergo/src_folder
move code to /src/ folder pattern
2022-10-25 13:11:29 +03:00
Gergő Jedlicska 9585d46c4e move code to /src/ folder pattern 2022-10-25 11:55:27 +02:00
Alan Rynne fd09e97a53 Merge pull request #218 from specklesystems/gergo/receiveMetricsUpdate
gergo/receiveMetricsUpdate
2022-10-06 17:24:32 +02:00
Gergő Jedlicska 459bd0901f add untracked receive operations for connector side tracking 2022-10-06 16:40:03 +02:00
Gergő Jedlicska ae7c4bc14d add host aplication implementation from sharp 2022-10-06 16:39:40 +02:00
Gergő Jedlicska 41f1823aae Merge pull request #215 from specklesystems/gergo/urlCompare
gergo/urlCompare
2022-09-27 15:19:09 +02:00
Gergő Jedlicska 625bd5cd84 fixing path.unlink for py37 2022-09-27 15:13:29 +02:00
Gergő Jedlicska 8812985c67 Merge branch 'main' of github.com:specklesystems/specklepy into gergo/urlCompare 2022-09-27 14:57:48 +02:00
Gergő Jedlicska c838835a65 Merge pull request #213 from specklesystems/gergo/noneUnits
gergo/noneUnits
2022-09-27 14:56:54 +02:00
Gergő Jedlicska 361ba6bfcd fix stream wrapper matching on accounts with subdomain url-s 2022-09-26 20:15:45 +02:00
Gergő Jedlicska 8078a4b596 remove literal import 2022-09-26 09:59:42 +02:00
Gergő Jedlicska 08b106464f remove unused literal 2022-09-24 16:24:48 +02:00
Gergő Jedlicska 0cfe5db674 reformat stuff, fix backwards compatible typing 2022-09-24 16:22:04 +02:00
Gergő Jedlicska d9dbca2c68 py311 is not yet supported by circleci base images 2022-09-24 16:14:43 +02:00
Gergő Jedlicska ab5b55871b test for python311 2022-09-24 16:12:01 +02:00
Gergő Jedlicska 2f87956154 remove checking for the fetched users email 2022-09-24 16:09:47 +02:00
Gergő Jedlicska 6fc4ab1539 update tests 2022-09-23 17:09:25 +02:00
Gergő Jedlicska 7f432e768d Fix warnings about None type units being set on Base objects
Add proper units enum implementation

Co-authored-by: Alan Rynne <alan@speckle.systems>
Co-authored-by: Morten Engen <morten.engen@multiconsult.no>
2022-09-23 17:08:53 +02:00
66 changed files with 1642 additions and 676 deletions
+39
View File
@@ -0,0 +1,39 @@
from typing import List
from specklepy.api.wrapper import StreamWrapper
from specklepy.objects import Base
from specklepy.api import operations
import string
import random
class Sub(Base):
bar: List[str]
def random_string():
letters = string.ascii_lowercase
return "".join(random.choice(letters) for _ in range(10))
def create_object(child_count: int) -> Base:
foo = Base()
for i in range(child_count):
stuff = random_string()
foo[f"@child_{i}"] = Sub(bar=["asdf", "bar", i, stuff])
return foo
if __name__ == "__main__":
stream_url = "http://hyperion:3000/streams/2372b54c35"
child_count = 10
foo = create_object(child_count)
wrapper = StreamWrapper(stream_url)
transport = wrapper.get_transport()
hash = operations.send(base=foo, transports=[transport], use_default_cache=False)
rec = operations.receive(hash, transport)
print(rec)
+35
View File
@@ -0,0 +1,35 @@
from specklepy.api import operations
from specklepy.objects.geometry import Base
from specklepy.objects.units import Units
dct = {
"id": "1234abcd",
"units": None,
"speckle_type": "Base",
"applicationId": None,
"totalChildrenCount": 0,
}
base = Base()
for prop, value in dct.items():
base.__setattr__(prop, value)
from devtools import debug
debug(base)
debug(base.units)
base.units = "m"
debug(base.units)
base.units = None
debug(base.units)
foo = operations.serialize(base)
base.units = 10
debug(base.units)
debug(foo)
base.units = Units.mm
debug(base.units)
Generated
+504 -448
View File
File diff suppressed because it is too large Load Diff
+8 -5
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "specklepy"
version = "2.4.0"
version = "2.9.1"
description = "The Python SDK for Speckle 2.0"
readme = "README.md"
authors = ["Speckle Systems <devops@speckle.systems>"]
@@ -8,6 +8,9 @@ license = "Apache-2.0"
repository = "https://github.com/specklesystems/speckle-py"
documentation = "https://speckle.guide/dev/py-examples.html"
homepage = "https://speckle.systems/"
packages = [
{ include = "specklepy", from = "src" },
]
[tool.poetry.dependencies]
@@ -18,15 +21,15 @@ gql = {extras = ["requests", "websockets"], version = "^3.3.0"}
ujson = "^5.3.0"
Deprecated = "^1.2.13"
[tool.poetry.dev-dependencies]
black = "^20.8b1"
[tool.poetry.group.dev.dependencies]
black = "^22.8.0"
isort = "^5.7.0"
pytest = "^6.2.2"
pytest = "^7.1.3"
pytest-ordering = "^0.6"
pytest-cov = "^3.0.0"
devtools = "^0.8.0"
pylint = "^2.14.4"
mypy = "^0.982"
[tool.black]
exclude = '''
-63
View File
@@ -1,63 +0,0 @@
from warnings import warn
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
UNITS = ["mm", "cm", "m", "in", "ft", "yd", "mi"]
UNITS_STRINGS = {
"mm": ["mm", "mil", "millimeters", "millimetres"],
"cm": ["cm", "centimetre", "centimeter", "centimetres", "centimeters"],
"m": ["m", "meter", "meters", "metre", "metres"],
"km": ["km", "kilometer", "kilometre", "kilometers", "kilometres"],
"in": ["in", "inch", "inches"],
"ft": ["ft", "foot", "feet"],
"yd": ["yd", "yard", "yards"],
"mi": ["mi", "mile", "miles"],
"none": ["none", "null"],
}
UNITS_ENCODINGS = {
"none": 0,
None: 0,
"mm": 1,
"cm": 2,
"m": 3,
"km": 4,
"in": 5,
"ft": 6,
"yd": 7,
"mi": 8,
}
def get_units_from_string(unit: str):
if not isinstance(unit, str):
warn(
f"Invalid units: expected type str but received {type(unit)} ({unit}). Skipping - no units will be set.",
SpeckleWarning,
)
return
unit = str.lower(unit)
for name, alternates in UNITS_STRINGS.items():
if unit in alternates:
return name
raise SpeckleException(
message=f"Could not understand what unit {unit} is referring to. Please enter a valid unit (eg {UNITS})."
)
def get_units_from_encoding(unit: int):
for name, encoding in UNITS_ENCODINGS.items():
if unit == encoding:
return name
raise SpeckleException(
message=f"Could not understand what unit {unit} is referring to. Please enter a valid unit encoding (eg {UNITS_ENCODINGS})."
)
def get_encoding_from_units(unit: str):
try:
return UNITS_ENCODINGS[unit]
except KeyError as e:
raise SpeckleException(message=f"No encoding exists for unit {unit}. Please enter a valid unit to encode (eg {UNITS_ENCODINGS}).") from e
@@ -18,8 +18,9 @@ from specklepy.api.resources import (
server,
user,
subscriptions,
other_user,
active_user
)
from specklepy.api.models import ServerInfo
from gql import Client
from gql.transport.requests import RequestsHTTPTransport
from gql.transport.websockets import WebsocketsTransport
@@ -137,7 +138,7 @@ class SpeckleClient:
"Authorization": f"Bearer {self.account.token}",
"Content-Type": "application/json",
"apollographql-client-name": metrics.HOST_APP,
"apollographql-client-version": metrics.HOST_APP_VERSION
"apollographql-client-version": metrics.HOST_APP_VERSION,
}
httptransport = RequestsHTTPTransport(
url=self.graphql, headers=headers, verify=True, retries=3
@@ -176,6 +177,18 @@ class SpeckleClient:
client=self.httpclient,
server_version=server_version,
)
self.other_user = other_user.Resource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.active_user = active_user.Resource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.stream = stream.Resource(
account=self.account,
basepath=self.url,
@@ -5,13 +5,14 @@ from specklepy.logging import metrics
from specklepy.api.models import ServerInfo
from specklepy.transports.sqlite import SQLiteTransport
from specklepy.logging.exceptions import SpeckleException
from specklepy import paths
class UserInfo(BaseModel):
name: Optional[str]
email: Optional[str]
company: Optional[str]
id: Optional[str]
name: Optional[str] = None
email: Optional[str] = None
company: Optional[str] = None
id: Optional[str] = None
class Account(BaseModel):
@@ -35,7 +36,7 @@ class Account(BaseModel):
return acct
def get_local_accounts(base_path: str = None) -> List[Account]:
def get_local_accounts(base_path: Optional[str] = None) -> List[Account]:
"""Gets all the accounts present in this environment
Arguments:
@@ -44,18 +45,30 @@ def get_local_accounts(base_path: str = None) -> List[Account]:
Returns:
List[Account] -- list of all local accounts or an empty list if no accounts were found
"""
account_storage = SQLiteTransport(scope="Accounts", base_path=base_path)
# pylint: disable=protected-access
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: List[Account] = []
res = account_storage.get_all_objects()
account_storage.close()
try:
account_storage = SQLiteTransport(scope="Accounts", base_path=base_path)
res = account_storage.get_all_objects()
account_storage.close()
if res:
accounts.extend(Account.parse_raw(r[1]) for r in res)
except SpeckleException:
# cannot open SQLiteTransport, probably because of the lack
# of disk write permissions
pass
json_acct_files = []
json_path = paths.accounts_path()
try:
os.makedirs(json_path, exist_ok=True)
json_acct_files.extend(
file for file in os.listdir(json_path) if file.endswith(".json")
)
except Exception:
# cannot find or get the json account paths
pass
if res:
accounts.extend(Account.parse_raw(r[1]) for r in res)
if json_acct_files:
try:
accounts.extend(
@@ -79,7 +92,7 @@ def get_local_accounts(base_path: str = None) -> List[Account]:
return accounts
def get_default_account(base_path: str = None) -> Account:
def get_default_account(base_path: Optional[str] = None) -> Optional[Account]:
"""Gets this environment's default account if any. If there is no default, the first found will be returned and set as default.
Arguments:
base_path {str} -- custom base path if you are not using the system default
+116
View File
@@ -0,0 +1,116 @@
from enum import Enum
from dataclasses import dataclass
from unicodedata import name
class HostAppVersion(Enum):
v = "v"
v6 = "v6"
v7 = "v7"
v2019 = "v2019"
v2020 = "v2020"
v2021 = "v2021"
v2022 = "v2022"
v2023 = "v2023"
v2024 = "v2024"
v2025 = "v2025"
vSandbox = "vSandbox"
vRevit = "vRevit"
vRevit2021 = "vRevit2021"
vRevit2022 = "vRevit2022"
vRevit2023 = "vRevit2023"
vRevit2024 = "vRevit2024"
vRevit2025 = "vRevit2025"
v25 = "v25"
v26 = "v26"
def __repr__(self) -> str:
return self.value
def __str__(self) -> str:
return self.value
@dataclass
class HostApplication:
name: str
slug: str
def get_version(self, version: HostAppVersion) -> str:
return f"{name.replace(' ', '')}{str(version).strip('v')}"
RHINO = HostApplication("Rhino", "rhino")
GRASSHOPPER = HostApplication("Grasshopper", "grasshopper")
REVIT = HostApplication("Revit", "revit")
DYNAMO = HostApplication("Dynamo", "dynamo")
UNITY = HostApplication("Unity", "unity")
GSA = HostApplication("GSA", "gsa")
CIVIL = HostApplication("Civil 3D", "civil3d")
AUTOCAD = HostApplication("AutoCAD", "autocad")
MICROSTATION = HostApplication("MicroStation", "microstation")
OPENROADS = HostApplication("OpenRoads", "openroads")
OPENRAIL = HostApplication("OpenRail", "openrail")
OPENBUILDINGS = HostApplication("OpenBuildings", "openbuildings")
ETABS = HostApplication("ETABS", "etabs")
SAP2000 = HostApplication("SAP2000", "sap2000")
CSIBRIDGE = HostApplication("CSIBridge", "csibridge")
SAFE = HostApplication("SAFE", "safe")
TEKLASTRUCTURES = HostApplication("Tekla Structures", "teklastructures")
DXF = HostApplication("DXF Converter", "dxf")
EXCEL = HostApplication("Excel", "excel")
UNREAL = HostApplication("Unreal", "unreal")
POWERBI = HostApplication("Power BI", "powerbi")
BLENDER = HostApplication("Blender", "blender")
QGIS = HostApplication("QGIS", "qgis")
ARCGIS = HostApplication("ArcGIS", "arcgis")
SKETCHUP = HostApplication("SketchUp", "sketchup")
ARCHICAD = HostApplication("Archicad", "archicad")
TOPSOLID = HostApplication("TopSolid", "topsolid")
PYTHON = HostApplication("Python", "python")
NET = HostApplication(".NET", "net")
OTHER = HostApplication("Other", "other")
_app_name_host_app_mapping = {
"dynamo": DYNAMO,
"revit": REVIT,
"autocad": AUTOCAD,
"civil": CIVIL,
"rhino": RHINO,
"grasshopper": GRASSHOPPER,
"unity": UNITY,
"gsa": GSA,
"microstation": MICROSTATION,
"openroads": OPENROADS,
"openrail": OPENRAIL,
"openbuildings": OPENBUILDINGS,
"etabs": ETABS,
"sap": SAP2000,
"csibridge": CSIBRIDGE,
"safe": SAFE,
"teklastructures": TEKLASTRUCTURES,
"dxf": DXF,
"excel": EXCEL,
"unreal": UNREAL,
"powerbi": POWERBI,
"blender": BLENDER,
"qgis": QGIS,
"arcgis": ARCGIS,
"sketchup": SKETCHUP,
"archicad": ARCHICAD,
"topsolid": TOPSOLID,
"python": PYTHON,
"net": NET,
}
def get_host_app_from_string(app_name: str) -> HostApplication:
app_name = app_name.lower().replace(" ", "")
for partial_app_name, host_app in _app_name_host_app_mapping.items():
if partial_app_name in app_name:
return host_app
return HostApplication(app_name, app_name)
if __name__ == "__main__":
print(HostAppVersion.v)
@@ -1,12 +1,8 @@
# generated by datamodel-codegen:
# filename: stream_schema.json
# timestamp: 2020-11-17T14:33:13+00:00
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel # pylint: disable=no-name-in-module
from pydantic import BaseModel, Field
class Collaborator(BaseModel):
@@ -64,20 +60,20 @@ class Branches(BaseModel):
class Stream(BaseModel):
id: Optional[str]
id: Optional[str] = None
name: Optional[str]
role: Optional[str]
isPublic: Optional[bool]
description: Optional[str]
createdAt: Optional[datetime]
updatedAt: Optional[datetime]
collaborators: List[Collaborator] = []
branches: Optional[Branches]
commit: Optional[Commit]
object: Optional[Object]
commentCount: Optional[int]
favoritedDate: Optional[datetime]
favoritesCount: Optional[int]
role: Optional[str] = None
isPublic: Optional[bool] = None
description: Optional[str] = None
createdAt: Optional[datetime] = None
updatedAt: Optional[datetime] = None
collaborators: List[Collaborator] = Field(default_factory=list)
branches: Optional[Branches] = None
commit: Optional[Commit] = None
object: Optional[Object] = None
commentCount: Optional[int] = None
favoritedDate: Optional[datetime] = None
favoritesCount: Optional[int] = None
def __repr__(self):
return f"Stream( id: {self.id}, name: {self.name}, description: {self.description}, isPublic: {self.isPublic})"
@@ -110,6 +106,18 @@ class User(BaseModel):
return self.__repr__()
class LimitedUser(BaseModel):
"""Limited user type, for showing public info about a user to another user."""
id: str
name: Optional[str]
bio: Optional[str]
company: Optional[str]
avatar: Optional[str]
verified: Optional[bool]
role: Optional[str]
class PendingStreamCollaborator(BaseModel):
id: Optional[str]
inviteId: Optional[str]
@@ -158,13 +166,13 @@ class ActivityCollection(BaseModel):
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]
name: Optional[str] = None
company: Optional[str] = None
url: Optional[str] = None
description: Optional[str] = None
adminContact: Optional[str] = None
canonicalUrl: Optional[str] = None
roles: Optional[List[dict]] = None
scopes: Optional[List[dict]] = None
authStrategies: Optional[List[dict]] = None
version: Optional[str] = None
@@ -2,7 +2,6 @@ from typing import List
from specklepy.logging import metrics
from specklepy.objects.base import Base
from specklepy.transports.sqlite import SQLiteTransport
from specklepy.transports.server import ServerTransport
from specklepy.logging.exceptions import SpeckleException
from specklepy.transports.abstract_transport import AbstractTransport
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
@@ -53,6 +52,16 @@ def receive(
remote_transport: AbstractTransport = None,
local_transport: AbstractTransport = None,
) -> Base:
metrics.track(metrics.RECEIVE, getattr(remote_transport, "account", None))
return _untracked_receive(obj_id, remote_transport, local_transport)
def _untracked_receive(
obj_id: str,
remote_transport: AbstractTransport = None,
local_transport: AbstractTransport = None,
) -> Base:
"""Receives an object from a transport.
Arguments:
@@ -64,13 +73,12 @@ def receive(
Returns:
Base -- the base object
"""
metrics.track(metrics.RECEIVE, getattr(remote_transport, "account", None))
if not local_transport:
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 with deserialisation using the local transport
# try local transport first. if the parent is there, we assume all the children are there and continue with deserialization using the local transport
obj_string = local_transport.get_object(obj_id)
if obj_string:
return serializer.read_json(obj_string=obj_string)
@@ -124,3 +132,6 @@ def deserialize(obj_string: str, read_transport: AbstractTransport = None) -> Ba
serializer = BaseObjectSerializer(read_transport=read_transport)
return serializer.read_json(obj_string=obj_string)
__all__ = [receive.__name__, send.__name__, serialize.__name__, deserialize.__name__]
@@ -98,6 +98,9 @@ class ResourceBase(object):
"""
if not unsupported_message:
unsupported_message = f"The client method used is not supported on Speckle Server versions prior to v{'.'.join(target_version)}"
# if version is dev, it should be supported... (or not)
if self.server_version == ("dev",):
return
if self.server_version and self.server_version < target_version:
raise UnsupportedException(unsupported_message)
+244
View File
@@ -0,0 +1,244 @@
from typing import List, Optional
from datetime import datetime, timezone
from gql import gql
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.api.resource import ResourceBase
from specklepy.api.models import ActivityCollection, PendingStreamCollaborator, User
NAME = "active_user"
class Resource(ResourceBase):
"""API Access class for users"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=NAME,
server_version=server_version,
)
self.schema = User
def get(self) -> User:
"""Gets the profile of a user. If no id argument is provided, will return the current authenticated user's profile (as extracted from the authorization header).
Arguments:
id {str} -- the user id
Returns:
User -- the retrieved user
"""
metrics.track(metrics.USER, self.account, {"name": "get"})
query = gql(
"""
query User {
activeUser {
id
email
name
bio
company
avatar
verified
profiles
role
}
}
"""
)
params = {}
return self.make_request(query=query, params=params, return_type="activeUser")
def update(
self,
name: Optional[str] = None,
company: Optional[str] = None,
bio: Optional[str] = None,
avatar: Optional[str] = None,
):
"""Updates your user profile. All arguments are optional.
Arguments:
name {str} -- your name
company {str} -- the company you may or may not work for
bio {str} -- tell us about yourself
avatar {str} -- a nice photo of yourself
Returns @deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT):
bool -- True if your profile was updated successfully
"""
metrics.track(metrics.USER, self.account, {"name": "update"})
query = gql(
"""
mutation UserUpdate($user: UserUpdateInput!) {
userUpdate(user: $user)
}
"""
)
params = {"name": name, "company": company, "bio": bio, "avatar": avatar}
params = {"user": {k: v for k, v in params.items() if v is not None}}
if not params["user"]:
return SpeckleException(
message="You must provide at least one field to update your user profile"
)
return self.make_request(
query=query, params=params, return_type="userUpdate", parse_response=False
)
def activity(
self,
limit: int = 20,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
):
"""
Get the activity from a given stream in an Activity collection. Step into the activity `items` for the list of activity.
If no id argument is provided, will return the current authenticated user's activity (as extracted from the authorization header).
Note: all timestamps arguments should be `datetime` of any tz as they will be converted to UTC ISO format strings
user_id {str} -- the id of the user to get the activity from
action_type {str} -- filter results to a single action type (eg: `commit_create` or `commit_receive`)
limit {int} -- max number of Activity items to return
before {datetime} -- latest cutoff for activity (ie: return all activity _before_ this time)
after {datetime} -- oldest cutoff for activity (ie: return all activity _after_ this time)
cursor {datetime} -- timestamp cursor for pagination
"""
query = gql(
"""
query UserActivity($action_type: String, $before:DateTime, $after: DateTime, $cursor: DateTime, $limit: Int){
activeUser {
activity(actionType: $action_type, before: $before, after: $after, cursor: $cursor, limit: $limit) {
totalCount
cursor
items {
actionType
info
userId
streamId
resourceId
resourceType
message
time
}
}
}
}
"""
)
params = {
"limit": limit,
"action_type": action_type,
"before": before.astimezone(timezone.utc).isoformat() if before else before,
"after": after.astimezone(timezone.utc).isoformat() if after else after,
"cursor": cursor.astimezone(timezone.utc).isoformat() if cursor else cursor,
}
return self.make_request(
query=query,
params=params,
return_type=["activeUser", "activity"],
schema=ActivityCollection,
)
def get_all_pending_invites(self) -> List[PendingStreamCollaborator]:
"""Get all of the active user's pending stream invites
Requires Speckle Server version >= 2.6.4
Returns:
List[PendingStreamCollaborator] -- a list of pending invites for the current user
"""
metrics.track(metrics.INVITE, self.account, {"name": "get"})
self._check_invites_supported()
query = gql(
"""
query StreamInvites {
streamInvites{
id
token
inviteId
streamId
streamName
title
role
invitedBy {
id
name
company
avatar
}
}
}
"""
)
return self.make_request(
query=query,
return_type="streamInvites",
schema=PendingStreamCollaborator,
)
def get_pending_invite(
self, stream_id: str, token: Optional[str] = None
) -> Optional[PendingStreamCollaborator]:
"""Get a particular pending invite for the active user on a given stream.
If no invite_id is provided, any valid invite will be returned.
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the id of the stream to look for invites on
token {str} -- the token of the invite to look for (optional)
Returns:
PendingStreamCollaborator -- the invite for the given stream (or None if it isn't found)
"""
metrics.track(metrics.INVITE, self.account, {"name": "get"})
self._check_invites_supported()
query = gql(
"""
query StreamInvite($streamId: String!, $token: String) {
streamInvite(streamId: $streamId, token: $token) {
id
token
streamId
streamName
title
role
invitedBy {
id
name
company
avatar
}
}
}
"""
)
params = {"streamId": stream_id}
if token:
params["token"] = token
return self.make_request(
query=query,
params=params,
return_type="streamInvite",
schema=PendingStreamCollaborator,
)
+157
View File
@@ -0,0 +1,157 @@
from typing import List, Optional, Union
from datetime import datetime, timezone
from gql import gql
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.api.resource import ResourceBase
from specklepy.api.models import (
ActivityCollection,
LimitedUser,
)
NAME = "other_user"
class Resource(ResourceBase):
"""API Access class for other users, that are not the currently active user."""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=NAME,
server_version=server_version,
)
self.schema = LimitedUser
def get(self, id: str) -> LimitedUser:
"""
Gets the profile of another user.
Arguments:
id {str} -- the user id
Returns:
LimitedUser -- the retrieved profile of another user
"""
metrics.track(metrics.OTHER_USER, self.account, {"name": "get"})
query = gql(
"""
query OtherUser($id: String!) {
otherUser(id: $id) {
id
name
bio
company
avatar
verified
role
}
}
"""
)
params = {"id": id}
return self.make_request(query=query, params=params, return_type="otherUser")
def search(
self, search_query: str, limit: int = 25
) -> Union[List[LimitedUser], SpeckleException]:
"""Searches for user by name or email. The search query must be at least 3 characters long
Arguments:
search_query {str} -- a string to search for
limit {int} -- the maximum number of results to return
Returns:
List[LimitedUser] -- a list of User objects that match the search query
"""
if len(search_query) < 3:
return SpeckleException(
message="User search query must be at least 3 characters"
)
metrics.track(metrics.OTHER_USER, self.account, {"name": "search"})
query = gql(
"""
query UserSearch($search_query: String!, $limit: Int!) {
userSearch(query: $search_query, limit: $limit) {
items {
id
name
bio
company
avatar
verified
}
}
}
"""
)
params = {"search_query": search_query, "limit": limit}
return self.make_request(
query=query, params=params, return_type=["userSearch", "items"]
)
def activity(
self,
user_id: str,
limit: int = 20,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
) -> ActivityCollection:
"""
Get the activity from a given stream in an Activity collection. Step into the activity `items` for the list of activity.
Note: all timestamps arguments should be `datetime` of any tz as they will be converted to UTC ISO format strings
user_id {str} -- the id of the user to get the activity from
action_type {str} -- filter results to a single action type (eg: `commit_create` or `commit_receive`)
limit {int} -- max number of Activity items to return
before {datetime} -- latest cutoff for activity (ie: return all activity _before_ this time)
after {datetime} -- oldest cutoff for activity (ie: return all activity _after_ this time)
cursor {datetime} -- timestamp cursor for pagination
"""
query = gql(
"""
query UserActivity($user_id: String!, $action_type: String, $before:DateTime, $after: DateTime, $cursor: DateTime, $limit: Int){
otherUser(id: $user_id) {
activity(actionType: $action_type, before: $before, after: $after, cursor: $cursor, limit: $limit) {
totalCount
cursor
items {
actionType
info
userId
streamId
resourceId
resourceType
message
time
}
}
}
}
"""
)
params = {
"user_id": user_id,
"limit": limit,
"action_type": action_type,
"before": before.astimezone(timezone.utc).isoformat() if before else before,
"after": after.astimezone(timezone.utc).isoformat() if after else after,
"cursor": cursor.astimezone(timezone.utc).isoformat() if cursor else cursor,
}
return self.make_request(
query=query,
params=params,
return_type=["otherUser", "activity"],
schema=ActivityCollection,
)
@@ -363,7 +363,11 @@ class Resource(ResourceBase):
bool -- True if the operation was successful
"""
metrics.track(metrics.PERMISSION, self.account, {"name": "add", "role": role})
if self.server_version and self.server_version >= (2, 6, 4):
# we're checking for the actual version info, and if the version is 'dev' we treat it
# as an up to date instance
if self.server_version and (
self.server_version == ("dev",) or self.server_version >= (2, 6, 4)
):
raise UnsupportedException(
(
"Server mutation `grant_permission` is no longer supported as of Speckle Server v2.6.4. "
@@ -641,7 +645,9 @@ class Resource(ResourceBase):
metrics.track(
metrics.PERMISSION, self.account, {"name": "update", "role": role}
)
if self.server_version and self.server_version < (2, 6, 4):
if self.server_version and (
self.server_version != ("dev",) and self.server_version < (2, 6, 4)
):
raise UnsupportedException(
(
"Server mutation `update_permission` is only supported as of Speckle Server v2.6.4. "
@@ -5,9 +5,14 @@ from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.api.resource import ResourceBase
from specklepy.api.models import ActivityCollection, PendingStreamCollaborator, User
from deprecated import deprecated
NAME = "user"
DEPRECATION_VERSION = "2.9.0"
DEPRECATION_TEXT = "The user resource is deprecated, please use the active_user or other_user resources"
class Resource(ResourceBase):
"""API Access class for users"""
@@ -22,8 +27,12 @@ class Resource(ResourceBase):
)
self.schema = User
def get(self, id: str = None) -> User:
"""Gets the profile of a user. If no id argument is provided, will return the current authenticated user's profile (as extracted from the authorization header).
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def get(self, id: Optional[str] = None) -> User:
"""
Gets the profile of a user.
If no id argument is provided, will return the current authenticated
user's profile (as extracted from the authorization header).
Arguments:
id {str} -- the user id
@@ -54,6 +63,7 @@ class Resource(ResourceBase):
return self.make_request(query=query, params=params, return_type="user")
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def search(
self, search_query: str, limit: int = 25
) -> Union[List[User], SpeckleException]:
@@ -93,8 +103,13 @@ class Resource(ResourceBase):
query=query, params=params, return_type=["userSearch", "items"]
)
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def update(
self, name: str = None, company: str = None, bio: str = None, avatar: str = None
self,
name: Optional[str] = None,
company: Optional[str] = None,
bio: Optional[str] = None,
avatar: Optional[str] = None,
):
"""Updates your user profile. All arguments are optional.
@@ -128,14 +143,15 @@ class Resource(ResourceBase):
query=query, params=params, return_type="userUpdate", parse_response=False
)
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def activity(
self,
user_id: str = None,
user_id: Optional[str] = None,
limit: int = 20,
action_type: str = None,
before: datetime = None,
after: datetime = None,
cursor: datetime = None,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
):
"""
Get the activity from a given stream in an Activity collection. Step into the activity `items` for the list of activity.
@@ -190,6 +206,7 @@ class Resource(ResourceBase):
schema=ActivityCollection,
)
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def get_all_pending_invites(self) -> List[PendingStreamCollaborator]:
"""Get all of the active user's pending stream invites
@@ -229,8 +246,9 @@ class Resource(ResourceBase):
schema=PendingStreamCollaborator,
)
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def get_pending_invite(
self, stream_id: str, token: str = None
self, stream_id: str, token: Optional[str] = None
) -> Optional[PendingStreamCollaborator]:
"""Get a particular pending invite for the active user on a given stream.
If no invite_id is provided, any valid invite will be returned.
@@ -110,7 +110,11 @@ class StreamWrapper:
return self._account
self._account = next(
(a for a in get_local_accounts() if self.host in a.serverInfo.url),
(
a
for a in get_local_accounts()
if self.host == urlparse(a.serverInfo.url).netloc
),
None,
)
@@ -11,6 +11,15 @@ class SpeckleException(Exception):
return f"SpeckleException: {self.message}"
class SpeckleInvalidUnitException(SpeckleException):
def __init__(self, invalid_unit: Any) -> None:
super().__init__(
message=f"Invalid units: expected type str but received {type(invalid_unit)} ({invalid_unit}).",
exception=None,
)
class SerializationException(SpeckleException):
def __init__(self, message: str, obj: Any, exception: Exception = None) -> None:
super().__init__(message=message, exception=exception)
@@ -3,6 +3,7 @@ import queue
import hashlib
import getpass
import logging
from typing import Optional
import requests
import threading
import platform
@@ -30,6 +31,7 @@ INVITE = "Invite Action"
COMMIT = "Commit Action"
BRANCH = "Branch Action"
USER = "User Action"
OTHER_USER = "Other User Action"
SERVER = "Server Action"
CLIENT = "Speckle Client"
STREAM_WRAPPER = "Stream Wrapper"
@@ -50,13 +52,13 @@ def enable():
TRACK = True
def set_host_app(host_app: str, host_app_version: str = None):
def set_host_app(host_app: str, host_app_version: Optional[str] = None):
global HOST_APP, HOST_APP_VERSION
HOST_APP = host_app
HOST_APP_VERSION = host_app_version or HOST_APP_VERSION
def track(action: str, account: "Account" = None, custom_props: dict = None):
def track(action: str, account: "Account" = None, custom_props: Optional[dict] = None):
if not TRACK:
return
try:
@@ -14,8 +14,8 @@ import contextlib
from enum import EnumMeta
from warnings import warn
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.units import get_units_from_string
from specklepy.logging.exceptions import SpeckleException, SpeckleInvalidUnitException
from specklepy.objects.units import get_units_from_string, Units
from specklepy.transports.memory import MemoryTransport
PRIMITIVES = (int, float, str, bool)
@@ -146,10 +146,10 @@ class _RegisteringBase:
class Base(_RegisteringBase):
id: Optional[str] = None
totalChildrenCount: Optional[int] = None
applicationId: Optional[str] = None
_units: Union[str, None] = None
id: Union[str, None] = None
totalChildrenCount: Union[int, None] = None
applicationId: Union[str, None] = None
_units: Union[Units, None] = None
def __init__(self, **kwargs) -> None:
super().__init__()
@@ -313,14 +313,23 @@ class Base(_RegisteringBase):
self._detachable = self._detachable.union(names)
@property
def units(self):
return self._units
def units(self) -> Union[str, None]:
if self._units:
return self._units.value
return None
@units.setter
def units(self, value: str):
units = get_units_from_string(value)
if units:
self._units = units
def units(self, value: Union[str, Units, None]):
if value == None:
units = value
elif isinstance(value, Units):
units: Units = value
else:
units = get_units_from_string(value)
self._units = units
# except SpeckleInvalidUnitException as ex:
# warn(f"Units are reset to None. Reason {ex.message}")
# self._units = None
def get_member_names(self) -> List[str]:
"""Get all of the property names on this object, dynamic or not"""
@@ -77,7 +77,7 @@ class Plane(Base, speckle_type=GEOMETRY + "Plane"):
*self.normal.to_list(),
*self.xdir.to_list(),
*self.ydir.to_list(),
get_encoding_from_units(self.units),
get_encoding_from_units(self._units),
]
@@ -113,7 +113,7 @@ class Line(Base, speckle_type=GEOMETRY + "Line"):
*self.start.to_list(),
*self.end.to_list(),
*domain,
get_encoding_from_units(self.units),
get_encoding_from_units(self._units),
]
@@ -158,7 +158,7 @@ class Arc(Base, speckle_type=GEOMETRY + "Arc"):
*self.startPoint.to_list(),
*self.midPoint.to_list(),
*self.endPoint.to_list(),
get_encoding_from_units(self.units),
get_encoding_from_units(self._units),
]
@@ -185,7 +185,7 @@ class Circle(Base, speckle_type=GEOMETRY + "Circle"):
self.radius,
*self.domain.to_list(),
*self.plane.to_list(),
get_encoding_from_units(self.units),
get_encoding_from_units(self._units),
]
@@ -216,7 +216,7 @@ class Ellipse(Base, speckle_type=GEOMETRY + "Ellipse"):
self.secondRadius,
*self.domain.to_list(),
*self.plane.to_list(),
get_encoding_from_units(self.units),
get_encoding_from_units(self._units),
]
@@ -255,7 +255,7 @@ class Polyline(Base, speckle_type=GEOMETRY + "Polyline", chunkable={"value": 200
*self.domain.to_list(),
len(self.value),
*self.value,
get_encoding_from_units(self.units),
get_encoding_from_units(self._units),
]
def as_points(self) -> List[Point]:
@@ -340,7 +340,7 @@ class Curve(
*self.points,
*self.weights,
*self.knots,
get_encoding_from_units(self.units),
get_encoding_from_units(self._units),
]
@@ -370,7 +370,7 @@ class Polycurve(Base, speckle_type=GEOMETRY + "Polycurve"):
*self.domain.to_list(),
len(curve_array),
*curve_array,
get_encoding_from_units(self.units),
get_encoding_from_units(self._units),
]
@@ -488,7 +488,7 @@ class Surface(Base, speckle_type=GEOMETRY + "Surface"):
*self.pointData,
*self.knotsU,
*self.knotsV,
get_encoding_from_units(self.units),
get_encoding_from_units(self._units),
]
@@ -590,7 +590,6 @@ class BrepEdge(Base, speckle_type=GEOMETRY + "BrepEdge"):
]
class BrepLoopType(int, Enum):
Unknown = 0
Outer = 1
@@ -842,7 +841,7 @@ class Brep(
def VerticesValue(self) -> List[Point]:
if self.Vertices is None:
return None
encoded_unit = get_encoding_from_units(self.Vertices[0].units)
encoded_unit = get_encoding_from_units(self.Vertices[0]._units)
values = [encoded_unit]
for vertex in self.Vertices:
values.extend(vertex.to_list())
@@ -5,4 +5,4 @@ from ..geometry import Plane
class Axis(Base, speckle_type="Objects.Structural.Geometry.Axis"):
name: str = None
axisType: str = None
plane: Plane = None
plane: Plane = None
+70
View File
@@ -0,0 +1,70 @@
from typing import Union
from specklepy.logging.exceptions import SpeckleException, SpeckleInvalidUnitException
from enum import Enum
class Units(Enum):
mm = "mm"
cm = "cm"
m = "m"
km = "km"
inches = "in"
feet = "ft"
yards = "yd"
miles = "mi"
none = "none"
UNITS_STRINGS = {
Units.mm: ["mm", "mil", "millimeters", "millimetres"],
Units.cm: ["cm", "centimetre", "centimeter", "centimetres", "centimeters"],
Units.m: ["m", "meter", "meters", "metre", "metres"],
Units.km: ["km", "kilometer", "kilometre", "kilometers", "kilometres"],
Units.inches: ["in", "inch", "inches"],
Units.feet: ["ft", "foot", "feet"],
Units.yards: ["yd", "yard", "yards"],
Units.miles: ["mi", "mile", "miles"],
Units.none: ["none", "null"],
}
UNITS_ENCODINGS = {
Units.none: 0,
None: 0,
Units.mm: 1,
Units.cm: 2,
Units.m: 3,
Units.km: 4,
Units.inches: 5,
Units.feet: 6,
Units.yards: 7,
Units.miles: 8,
}
def get_units_from_string(unit: str) -> Units:
if not isinstance(unit, str):
raise SpeckleInvalidUnitException(unit)
unit = str.lower(unit)
for name, alternates in UNITS_STRINGS.items():
if unit in alternates:
return name
raise SpeckleInvalidUnitException(unit)
def get_units_from_encoding(unit: int):
for name, encoding in UNITS_ENCODINGS.items():
if unit == encoding:
return name
raise SpeckleException(
message=f"Could not understand what unit {unit} is referring to. Please enter a valid unit encoding (eg {UNITS_ENCODINGS})."
)
def get_encoding_from_units(unit: Union[Units, None]):
try:
return UNITS_ENCODINGS[unit]
except KeyError as e:
raise SpeckleException(
message=f"No encoding exists for unit {unit}. Please enter a valid unit to encode (eg {UNITS_ENCODINGS})."
) from e
+27
View File
@@ -0,0 +1,27 @@
import sys
from pathlib import Path
from appdirs import user_data_dir
def base_path(app_name) -> Path:
# from appdirs https://github.com/ActiveState/appdirs/blob/master/appdirs.py
# default mac path is not the one we use (we use unix path), so using special case for this
system = sys.platform
if system.startswith("java"):
import platform
os_name = platform.java_ver()[3][0]
if os_name.startswith("Mac"):
system = "darwin"
if system == "darwin":
return Path(Path.home(), ".config", app_name)
return Path(user_data_dir(appname=app_name, appauthor=False, roaming=True))
def accounts_path(app_name: str = "Speckle") -> Path:
"""
Gets the path where the Speckle applications are looking for accounts.
"""
return base_path(app_name).joinpath("Accounts")
@@ -44,9 +44,13 @@ class BaseObjectSerializer:
lineage: List[str] # keeps track of hash chain through the object tree
family_tree: Dict[str, Dict[str, int]]
closure_table: Dict[str, Dict[str, int]]
deserialized: Dict[str, Base] # holds deserialized objects so objects with same id return the same instance
deserialized: Dict[
str, Base
] # holds deserialized objects so objects with same id return the same instance
def __init__(self, write_transports: List[AbstractTransport] = None, read_transport=None) -> None:
def __init__(
self, write_transports: List[AbstractTransport] = None, read_transport=None
) -> None:
self.write_transports = write_transports or []
self.read_transport = read_transport
self.detach_lineage = []
@@ -294,7 +298,7 @@ class BaseObjectSerializer:
"""
if not obj_string:
return None
self.deserialized = {}
obj = safe_json_loads(obj_string)
return self.recompose_base(obj=obj)
@@ -1,7 +1,7 @@
from abc import ABC, abstractmethod
from typing import Optional, List, Dict
from pydantic import BaseModel
from pydantic.main import Extra
from pydantic.config import Extra
# __________________
# | |
@@ -1,13 +1,10 @@
import os
import sys
import time
import sched
import sqlite3
from typing import Any, List, Dict, Tuple
from appdirs import user_data_dir
from typing import Any, List, Dict, Optional, Tuple
from contextlib import closing
from specklepy.transports.abstract_transport import AbstractTransport
from specklepy.logging.exceptions import SpeckleException
from specklepy.paths import base_path
class SQLiteTransport(AbstractTransport):
@@ -24,8 +21,8 @@ class SQLiteTransport(AbstractTransport):
def __init__(
self,
base_path: str = None,
app_name: str = None,
base_path: Optional[str] = None,
app_name: Optional[str] = None,
scope: str = None,
max_batch_size_mb: float = 10.0,
**data: Any,
@@ -56,21 +53,23 @@ class SQLiteTransport(AbstractTransport):
@staticmethod
def get_base_path(app_name):
# from appdirs https://github.com/ActiveState/appdirs/blob/master/appdirs.py
# default mac path is not the one we use (we use unix path), so using special case for this
system = sys.platform
if system.startswith("java"):
import platform
# # from appdirs https://github.com/ActiveState/appdirs/blob/master/appdirs.py
# # default mac path is not the one we use (we use unix path), so using special case for this
# system = sys.platform
# if system.startswith("java"):
# import platform
os_name = platform.java_ver()[3][0]
if os_name.startswith("Mac"):
system = "darwin"
# os_name = platform.java_ver()[3][0]
# if os_name.startswith("Mac"):
# system = "darwin"
if system != "darwin":
return user_data_dir(appname=app_name, appauthor=False, roaming=True)
# if system != "darwin":
# return user_data_dir(appname=app_name, appauthor=False, roaming=True)
path = os.path.expanduser("~/.config/")
return os.path.join(path, app_name)
# path = os.path.expanduser("~/.config/")
# return os.path.join(path, app_name)
return str(base_path(app_name))
def save_object_from_transport(
self, id: str, source_transport: AbstractTransport
+7 -2
View File
@@ -29,9 +29,14 @@ def seed_user(host):
r = requests.post(
url=f"http://{host}/auth/local/register?challenge=pyspeckletests",
data=user_dict,
# do not follow redirects here, they lead to the frontend, which might not be
# running in a test environment
# causing the response to not be OK in the end
allow_redirects=False
)
print(r.url)
access_code = r.url.split("access_code=")[1]
if not r.ok:
raise Exception(f"Cannot seed user: {r.reason}")
access_code = r.text.split("access_code=")[1]
r_tokens = requests.post(
url=f"http://{host}/auth/token",
+45
View File
@@ -0,0 +1,45 @@
import pytest
from specklepy.api.client import SpeckleClient
from specklepy.api.models import Activity, ActivityCollection, User
from specklepy.logging.exceptions import SpeckleException
@pytest.mark.run(order=2)
class TestUser:
def test_user_get_self(self, client: SpeckleClient, user_dict):
fetched_user = client.active_user.get()
assert isinstance(fetched_user, User)
assert fetched_user.name == user_dict["name"]
assert fetched_user.email == user_dict["email"]
user_dict["id"] = fetched_user.id
def test_user_update(self, client: SpeckleClient):
bio = "i am a ghost in the machine"
failed_update = client.active_user.update()
assert isinstance(failed_update, SpeckleException)
updated = client.active_user.update(bio=bio)
me = client.active_user.get()
assert updated is True
assert me.bio == bio
def test_user_activity(self, client: SpeckleClient, second_user_dict):
my_activity = client.active_user.activity(limit=10)
their_activity = client.other_user.activity(second_user_dict["id"])
assert isinstance(my_activity, ActivityCollection)
assert my_activity.items
assert isinstance(my_activity.items[0], Activity)
assert my_activity.totalCount
assert isinstance(their_activity, ActivityCollection)
older_activity = client.user.activity(before=my_activity.items[0].time)
assert isinstance(older_activity, ActivityCollection)
assert older_activity.totalCount
assert older_activity.totalCount < my_activity.totalCount
+15 -4
View File
@@ -1,11 +1,13 @@
from codecs import ascii_encode
from enum import Enum
from typing import Dict, List, Optional, Union
from contextlib import ExitStack as does_not_raise
import pytest
from specklepy.api import operations
from specklepy.logging.exceptions import SpeckleException
from specklepy.logging.exceptions import SpeckleException, SpeckleInvalidUnitException
from specklepy.objects.base import Base, DataChunk
from specklepy.objects.units import Units
@pytest.mark.parametrize(
@@ -82,13 +84,22 @@ def test_setting_units():
b = Base(units="foot")
assert b.units == "ft"
with pytest.raises(SpeckleException):
with pytest.raises(SpeckleInvalidUnitException):
b.units = "big"
b.units = None # invalid args are skipped
b.units = 7
with pytest.raises(SpeckleInvalidUnitException):
b.units = 7 # invalid args are skipped
assert b.units == "ft"
b.units = None # None should be a valid arg
assert b.units == None
b.units = Units.none
assert b.units == "none"
b.units = Units.cm
assert b.units == Units.cm.value
def test_base_of_custom_speckle_type() -> None:
b1 = Base.of_type("BirdHouse", name="Tweety's Crib")
+1 -1
View File
@@ -45,4 +45,4 @@ def test_account_from_token_and_url():
acct = get_account_from_token(token, url)
assert acct.token == token
assert acct.serverInfo.url == url
assert acct.serverInfo.url == url
+1 -1
View File
@@ -4,7 +4,7 @@ from specklepy.api.models import Commit, Stream
from specklepy.transports.server.server import ServerTransport
@pytest.mark.run(order=4)
@pytest.mark.run(order=6)
class TestCommit:
@pytest.fixture(scope="module")
def commit(self):
+4 -3
View File
@@ -5,6 +5,7 @@ import pytest
from specklepy.api import operations
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.base import Base
from specklepy.objects.units import Units
from specklepy.objects.encoding import CurveArray, ObjectArray
from specklepy.objects.geometry import (
Arc,
@@ -386,9 +387,9 @@ def test_brep_curve3d_values_serialization(curve, polyline, circle):
def test_brep_vertices_values_serialization():
brep = Brep()
brep.VerticesValue = [1, 1, 1, 1, 2, 2, 2, 3, 3, 3]
assert brep.Vertices[0].get_id() == Point(x=1, y=1, z=1, _units="mm").get_id()
assert brep.Vertices[1].get_id() == Point(x=2, y=2, z=2, _units="mm").get_id()
assert brep.Vertices[2].get_id() == Point(x=3, y=3, z=3, _units="mm").get_id()
assert brep.Vertices[0].get_id() == Point(x=1, y=1, z=1, _units=Units.mm).get_id()
assert brep.Vertices[1].get_id() == Point(x=2, y=2, z=2, _units=Units.mm).get_id()
assert brep.Vertices[2].get_id() == Point(x=3, y=3, z=3, _units=Units.mm).get_id()
def test_trims_value_serialization():
+18
View File
@@ -0,0 +1,18 @@
import pytest
from specklepy.api.host_applications import (
get_host_app_from_string,
_app_name_host_app_mapping,
)
def test_get_host_app_from_string_returns_fallback_app():
not_existing_app_name = "gmail"
host_app = get_host_app_from_string(not_existing_app_name)
assert host_app.name == not_existing_app_name
assert host_app.slug == not_existing_app_name
@pytest.mark.parametrize("app_name", _app_name_host_app_mapping.keys())
def test_get_host_app_from_string_matches_for_predefined_apps(app_name) -> None:
host_app = get_host_app_from_string(app_name)
assert app_name in host_app.slug.lower()
+49
View File
@@ -0,0 +1,49 @@
import pytest
from specklepy.api.client import SpeckleClient
from specklepy.api.models import Activity, ActivityCollection, LimitedUser
from specklepy.logging.exceptions import SpeckleException
@pytest.mark.run(order=4)
class TestOtherUser:
def test_user_get_self(self, client):
"""
Test, that a limited user query cannot query the active user.
"""
with pytest.raises(TypeError):
client.other_user.get()
def test_user_search(self, client, second_user_dict):
search_results = client.other_user.search(
search_query=second_user_dict["name"][:5]
)
assert isinstance(search_results, list)
assert len(search_results) > 0
result_user = search_results[0]
assert isinstance(result_user, LimitedUser)
assert result_user.name == second_user_dict["name"]
second_user_dict["id"] = result_user.id
assert getattr(result_user, "email", None) is None
def test_user_get(self, client, second_user_dict):
fetched_user = client.other_user.get(id=second_user_dict["id"])
assert isinstance(fetched_user, LimitedUser)
assert fetched_user.name == second_user_dict["name"]
# changed in the server, now you cannot get emails of other users
# not checking this, since the first user could or could not be an admin on the server
# admins can get emails of others, regular users can't
# assert fetched_user.email == None
second_user_dict["id"] = fetched_user.id
def test_user_activity(self, client: SpeckleClient, second_user_dict):
their_activity = client.other_user.activity(second_user_dict["id"])
assert isinstance(their_activity, ActivityCollection)
assert isinstance(their_activity.items, list)
assert isinstance(their_activity.items[0], Activity)
assert their_activity.totalCount
assert their_activity.totalCount > 0
+3 -3
View File
@@ -9,7 +9,7 @@ from specklepy.objects.geometry import Point
from specklepy.objects.fakemesh import FakeMesh
@pytest.mark.run(order=3)
@pytest.mark.run(order=5)
class TestSerialization:
def test_serialize(self, base):
serialized = operations.serialize(base)
@@ -91,7 +91,7 @@ class TestSerialization:
assert deserialised == {"foo": "bar"}
def test_big_int(self):
big_int = '{"big": ' + str(2 ** 64) + "}"
big_int = '{"big": ' + str(2**64) + "}"
deserialised = operations.deserialize(big_int)
assert deserialised == {"big": 2 ** 64}
assert deserialised == {"big": 2**64}
+5 -2
View File
@@ -22,8 +22,11 @@ class TestServer:
version = client.server.version()
assert isinstance(version, tuple)
assert isinstance(version[0], int)
assert len(version) >= 3
if len(version) == 1:
assert version[0] == "dev"
else:
assert isinstance(version[0], int)
assert len(version) >= 3
def test_server_apps(self, client: SpeckleClient):
apps = client.server.apps()
+1 -1
View File
@@ -15,7 +15,7 @@ from specklepy.logging.exceptions import (
)
@pytest.mark.run(order=2)
@pytest.mark.run(order=3)
class TestStream:
@pytest.fixture(scope="session")
def stream(self):
+1 -1
View File
@@ -129,4 +129,4 @@ def test_transform_serialisation(transform: Transform):
serialized = operations.serialize(transform)
deserialized = operations.deserialize(serialized)
assert transform.get_id() == deserialized.get_id()
assert transform.get_id() == deserialized.get_id()
+43 -31
View File
@@ -6,55 +6,67 @@ from specklepy.logging.exceptions import SpeckleException
@pytest.mark.run(order=1)
class TestUser:
def test_user_get_self(self, client, user_dict):
fetched_user = client.user.get()
def test_user_get_self(self, client: SpeckleClient, user_dict):
with pytest.deprecated_call():
fetched_user = client.user.get()
assert isinstance(fetched_user, User)
assert fetched_user.name == user_dict["name"]
assert fetched_user.email == user_dict["email"]
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
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])
with pytest.deprecated_call():
search_results = client.user.search(search_query=second_user_dict["name"][:5])
assert isinstance(search_results, list)
assert isinstance(search_results[0], User)
assert search_results[0].name == second_user_dict["name"]
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
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"])
with pytest.deprecated_call():
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"]
assert isinstance(fetched_user, User)
assert fetched_user.name == second_user_dict["name"]
# changed in the server, now you cannot get emails of other users
# not checking this, since the first user could or could not be an admin on the server
# admins can get emails of others, regular users can't
# assert fetched_user.email == None
second_user_dict["id"] = fetched_user.id
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)
with pytest.deprecated_call():
failed_update = client.user.update()
assert isinstance(failed_update, SpeckleException)
with pytest.deprecated_call():
updated = client.user.update(bio=bio)
assert updated is True
me = client.user.get()
assert isinstance(failed_update, SpeckleException)
assert updated is True
assert me.bio == bio
with pytest.deprecated_call():
me = client.user.get()
assert me.bio == bio
def test_user_activity(self, client: SpeckleClient, second_user_dict):
my_activity = client.user.activity(limit=10)
their_activity = client.user.activity(second_user_dict["id"])
with pytest.deprecated_call():
my_activity = client.user.activity(limit=10)
their_activity = client.user.activity(second_user_dict["id"])
assert isinstance(my_activity, ActivityCollection)
assert isinstance(my_activity.items[0], Activity)
assert my_activity.totalCount > 0
assert isinstance(their_activity, ActivityCollection)
assert isinstance(my_activity, ActivityCollection)
assert my_activity.items
assert isinstance(my_activity.items[0], Activity)
assert my_activity.totalCount
assert isinstance(their_activity, ActivityCollection)
older_activity = client.user.activity(before=my_activity.items[0].time)
older_activity = client.user.activity(before=my_activity.items[0].time)
assert isinstance(older_activity, ActivityCollection)
assert older_activity.totalCount < my_activity.totalCount
assert isinstance(older_activity, ActivityCollection)
assert older_activity.totalCount
assert older_activity.totalCount < my_activity.totalCount
+41 -1
View File
@@ -1,5 +1,9 @@
import pytest
import json
from specklepy.api.wrapper import StreamWrapper
from specklepy.transports.sqlite import SQLiteTransport
from specklepy.paths import accounts_path
from pathlib import Path
import pytest
def test_parse_stream():
@@ -79,3 +83,39 @@ def test_get_transport_with_token():
assert transport is not None
assert client.account.token == "super-secret-token"
@pytest.fixture
def user_path() -> Path:
path = accounts_path().joinpath("test_acc.json")
# hey, py37 doesn't support the missing_ok argument
try:
path.unlink()
except:
pass
try:
path.unlink(missing_ok=True)
except:
pass
path.parent.absolute().mkdir(exist_ok=True)
yield path
path.unlink()
def test_wrapper_url_match(user_path) -> None:
"""
The stream wrapper should only recognize exact url matches for the account
definitions and not match for subdomains.
"""
account = {
"token": "foobar",
"serverInfo": {"name": "foo", "url": "http://foo.bar.baz", "company": "Foo"},
"userInfo": {"id": "bla", "name": "A rando tester", "email": "rando@tester.me"},
}
user_path.write_text(json.dumps(account))
wrap = StreamWrapper("http://bar.baz/streams/bogus")
account = wrap.get_account()
assert account.userInfo.email is None