Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b50e658333 | |||
| 88248353ab | |||
| aec94f8f7f | |||
| e6b1604bc3 | |||
| de29b93b8b | |||
| 10aa8b59b6 | |||
| b86faa6a14 | |||
| 7430611c52 | |||
| ddd52f4af9 | |||
| 35bc6b0350 | |||
| 9585d46c4e |
Generated
+525
-274
File diff suppressed because it is too large
Load Diff
+7
-5
@@ -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,16 +21,15 @@ gql = {extras = ["requests", "websockets"], version = "^3.3.0"}
|
||||
ujson = "^5.3.0"
|
||||
Deprecated = "^1.2.13"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
[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.971"
|
||||
|
||||
mypy = "^0.982"
|
||||
|
||||
[tool.black]
|
||||
exclude = '''
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -39,6 +39,7 @@ class HostApplication:
|
||||
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")
|
||||
@@ -99,16 +100,17 @@ _app_name_host_app_mapping = {
|
||||
"archicad": ARCHICAD,
|
||||
"topsolid": TOPSOLID,
|
||||
"python": PYTHON,
|
||||
"net": NET
|
||||
"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):
|
||||
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
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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.
|
||||
@@ -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:
|
||||
+1
-1
@@ -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,10 +1,6 @@
|
||||
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
|
||||
@@ -25,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,
|
||||
+7
-2
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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):
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import pytest
|
||||
from specklepy.api.host_applications import get_host_app_from_string, _app_name_host_app_mapping
|
||||
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"
|
||||
@@ -7,7 +11,8 @@ def test_get_host_app_from_string_returns_fallback_app():
|
||||
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()
|
||||
assert app_name in host_app.slug.lower()
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import pytest
|
||||
from specklepy.api.client import SpeckleClient
|
||||
from specklepy.api.models import Activity, ActivityCollection, LimitedUser
|
||||
from specklepy.logging.exceptions import SpeckleException
|
||||
|
||||
|
||||
@pytest.mark.run(order=4)
|
||||
class TestOtherUser:
|
||||
def test_user_get_self(self, client):
|
||||
"""
|
||||
Test, that a limited user query cannot query the active user.
|
||||
"""
|
||||
with pytest.raises(TypeError):
|
||||
client.other_user.get()
|
||||
|
||||
def test_user_search(self, client, second_user_dict):
|
||||
search_results = client.other_user.search(
|
||||
search_query=second_user_dict["name"][:5]
|
||||
)
|
||||
|
||||
assert isinstance(search_results, list)
|
||||
assert len(search_results) > 0
|
||||
result_user = search_results[0]
|
||||
assert isinstance(result_user, LimitedUser)
|
||||
assert result_user.name == second_user_dict["name"]
|
||||
|
||||
second_user_dict["id"] = result_user.id
|
||||
assert getattr(result_user, "email", None) is None
|
||||
|
||||
def test_user_get(self, client, second_user_dict):
|
||||
fetched_user = client.other_user.get(id=second_user_dict["id"])
|
||||
|
||||
assert isinstance(fetched_user, LimitedUser)
|
||||
assert fetched_user.name == second_user_dict["name"]
|
||||
# changed in the server, now you cannot get emails of other users
|
||||
# not checking this, since the first user could or could not be an admin on the server
|
||||
# admins can get emails of others, regular users can't
|
||||
# assert fetched_user.email == None
|
||||
|
||||
second_user_dict["id"] = fetched_user.id
|
||||
|
||||
def test_user_activity(self, client: SpeckleClient, second_user_dict):
|
||||
their_activity = client.other_user.activity(second_user_dict["id"])
|
||||
|
||||
assert isinstance(their_activity, ActivityCollection)
|
||||
assert isinstance(their_activity.items, list)
|
||||
assert isinstance(their_activity.items[0], Activity)
|
||||
assert their_activity.totalCount
|
||||
assert their_activity.totalCount > 0
|
||||
@@ -9,7 +9,7 @@ from specklepy.objects.geometry import Point
|
||||
from specklepy.objects.fakemesh import FakeMesh
|
||||
|
||||
|
||||
@pytest.mark.run(order=3)
|
||||
@pytest.mark.run(order=5)
|
||||
class TestSerialization:
|
||||
def test_serialize(self, base):
|
||||
serialized = operations.serialize(base)
|
||||
|
||||
@@ -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):
|
||||
|
||||
+43
-34
@@ -6,58 +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"]
|
||||
# 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
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user