Compare commits

..

11 Commits

Author SHA1 Message Date
KatKatKateryna 6469b6f757 Merge pull request #334 from specklesystems/jsdb/doc-strings-patch
Corrects and enhances user API class documentation (CNX-9172)
2024-03-28 23:42:09 +08:00
KatKatKateryna b28db0881c formatting 2024-03-28 16:22:21 +01:00
Jonathon Broughton b0b442de23 fix poetry in dockerfile 2024-03-28 14:48:34 +00:00
Jonathon Broughton 32d2fe8ead Merge branch 'main' into jsdb/doc-strings-patch 2024-03-26 12:48:45 +00:00
Jonathon Broughton 9fd40eac23 Update other_user.py 2024-03-26 12:43:14 +00:00
Benjamin Ottensten b22ba1f1f1 Update web app link in the README (#333) 2024-03-26 12:39:59 +00:00
Jonathon Broughton 5e20fe7bf1 Corrected/updated docstrings for method signatures 2024-03-26 11:06:35 +00:00
Jedd Morgan 6da5da23c4 feat(core): [CNX-9108] Added server migration support (#331)
* Added server migration support

* fix obvious mistake

* Fixed slightly less obvious mistake

* Run black

* isort
2024-03-25 11:15:41 +00:00
Gergő Jedlicska 1b59f0b026 feat: fix authenticate with token mechanism (#330) 2024-02-26 16:30:28 +00:00
Jedd Morgan 78123936d2 Merge pull request #329 from specklesystems/jrm/spirals-fix
fix(objects): [CNX-9014] Fixed issue with Spiral turns not deserializing in SpecklePy
2024-02-19 13:05:38 +00:00
Jedd Morgan dbc1aefed3 Fixed issue with Spiral turns not deserializing in SpecklePy 2024-02-19 12:41:35 +00:00
10 changed files with 177 additions and 86 deletions
+1 -1
View File
@@ -22,6 +22,6 @@ RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/
USER vscode
RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python -
RUN curl -sSL https://install.python-poetry.org | python3 -
ENV PATH=$PATH:$HOME/.poetry/env
+1 -1
View File
@@ -33,7 +33,7 @@ What is Speckle? Check our ![YouTube Video Views](https://img.shields.io/youtube
Give Speckle a try in no time by:
- [![speckle XYZ](https://img.shields.io/badge/https://-speckle.xyz-0069ff?style=flat-square&logo=hackthebox&logoColor=white)](https://speckle.xyz) ⇒ creating an account at our public server
- [![speckle](https://img.shields.io/badge/https://-app.speckle.systems-0069ff?style=flat-square&logo=hackthebox&logoColor=white)](https://app.speckle.systems) ⇒ creating an account at our public server
- [![create a droplet](https://img.shields.io/badge/Create%20a%20Droplet-0069ff?style=flat-square&logo=digitalocean&logoColor=white)](https://marketplace.digitalocean.com/apps/speckle-server?refcode=947a2b5d7dc1) ⇒ deploying an instance in 1 click
### Resources
+26 -38
View File
@@ -7,7 +7,8 @@ from specklepy.logging import metrics
class Resource(CoreResource):
"""API Access class for users"""
"""API Access class for users. This class provides methods to get and update
the user profile, fetch user activity, and manage pending stream invitations."""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
@@ -19,13 +20,9 @@ class Resource(CoreResource):
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
"""Gets the profile of the current authenticated user's profile
(as extracted from the authorization header).
Arguments:
id {str} -- the user id
Returns:
User -- the retrieved user
"""
@@ -41,11 +38,11 @@ class Resource(CoreResource):
):
"""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
Args:
name (Optional[str]): The user's name.
company (Optional[str]): The company the user works for.
bio (Optional[str]): A brief user biography.
avatar (Optional[str]): A URL to an avatar image for the user.
Returns @deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT):
bool -- True if your profile was updated successfully
@@ -62,35 +59,30 @@ class Resource(CoreResource):
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).
Fetches collection the current authenticated user's activity
as filtered by given parameters
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
Args:
limit (int): The maximum number of activity items to return.
action_type (Optional[str]): Filter results to a single action type.
before (Optional[datetime]): Latest cutoff for activity to include.
after (Optional[datetime]): Oldest cutoff for an activity to include.
cursor (Optional[datetime]): Timestamp cursor for pagination.
Returns:
Activity collection, filtered according to the provided parameters.
"""
metrics.track(metrics.SDK, self.account, {"name": "User Active Activity"})
return super().activity(limit, action_type, before, after, cursor)
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
"""Fetches all of the current user's pending stream invitations.
Returns:
List[PendingStreamCollaborator]
-- a list of pending invites for the current user
List[PendingStreamCollaborator]: A list of pending stream invitations.
"""
metrics.track(
metrics.SDK, self.account, {"name": "User Active Invites All Get"}
@@ -100,18 +92,14 @@ class Resource(CoreResource):
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.
"""Fetches a specific pending invite for the current user on a given stream.
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)
Args:
stream_id (str): The ID of the stream to look for invites on.
token (Optional[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)
Optional[PendingStreamCollaborator]: The invite for the given stream, or None if not found.
"""
metrics.track(metrics.SDK, self.account, {"name": "User Active Invite Get"})
return super().get_pending_invite(stream_id, token)
+30 -25
View File
@@ -8,7 +8,11 @@ from specklepy.logging.exceptions import SpeckleException
class Resource(CoreResource):
"""API Access class for other users, that are not the currently active user."""
"""
Provides API access to other users' profiles and activities on the platform.
This class enables fetching limited information about users, searching for users by name or email,
and accessing user activity logs with appropriate privacy and access control measures in place.
"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
@@ -21,13 +25,13 @@ class Resource(CoreResource):
def get(self, id: str) -> LimitedUser:
"""
Gets the profile of another user.
Retrieves the profile of a user specified by their user ID.
Arguments:
id {str} -- the user id
Args:
id (str): The unique identifier of the user.
Returns:
LimitedUser -- the retrieved profile of another user
LimitedUser: The profile of the user with limited information.
"""
metrics.track(metrics.SDK, self.account, {"name": "Other User Get"})
return super().get(id)
@@ -35,18 +39,21 @@ class Resource(CoreResource):
def search(
self, search_query: str, limit: int = 25
) -> Union[List[LimitedUser], SpeckleException]:
"""Searches for user by name or email. The search query must be at least
3 characters long
"""
Searches for users by name or email.
The search requires a minimum query length of 3 characters.
Args:
search_query (str): The search string.
limit (int): Maximum number of search results to return.
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
Union[List[LimitedUser], SpeckleException]: A list of users matching the search
query or an exception if the query is too short.
"""
if len(search_query) < 3:
return SpeckleException(
message="User search query must be at least 3 characters"
message="User search query must be at least 3 characters."
)
metrics.track(metrics.SDK, self.account, {"name": "Other User Search"})
@@ -62,21 +69,19 @@ class Resource(CoreResource):
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.
Retrieves a collection of activities for a specified user, with optional filters for activity type,
time frame, and pagination.
Note: all timestamps arguments should be `datetime` of
any tz as they will be converted to UTC ISO format strings
Args:
user_id (str): The ID of the user whose activities are being requested.
limit (int): The maximum number of activity items to return.
action_type (Optional[str]): A specific type of activity to filter.
before (Optional[datetime]): Latest timestamp to include activities before.
after (Optional[datetime]): Earliest timestamp to include activities after.
cursor (Optional[datetime]): Timestamp for pagination cursor.
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
Returns:
ActivityCollection: A collection of user activities filtered according to specified criteria.
"""
metrics.track(metrics.SDK, self.account, {"name": "Other User Activity"})
return super().activity(user_id, limit, action_type, before, after, cursor)
+2 -3
View File
@@ -123,8 +123,7 @@ class SpeckleClient:
Arguments:
token {str} -- an api token
"""
self.authenticate_with_token(token)
self._set_up_client()
self.authenticate_with_account(get_account_from_token(token))
def authenticate_with_token(self, token: str) -> None:
"""
@@ -135,7 +134,7 @@ class SpeckleClient:
Arguments:
token {str} -- an api token
"""
self.account = get_account_from_token(token, self.url)
self.account = Account.from_token(token, self.url)
self._set_up_client()
def authenticate_with_account(self, account: Account) -> None:
+23
View File
@@ -1,6 +1,7 @@
import os
from pathlib import Path
from typing import List, Optional
from urllib.parse import urlparse
from pydantic import BaseModel, Field # pylint: disable=no-name-in-module
@@ -142,6 +143,28 @@ def get_account_from_token(token: str, server_url: str = None) -> Account:
return Account.from_token(token, server_url)
def get_accounts_for_server(host: str) -> List[Account]:
all_accounts = get_local_accounts()
filtered: List[Account] = []
for acc in all_accounts:
moved_from = (
acc.serverInfo.migration.movedFrom if acc.serverInfo.migration else None
)
if moved_from and host == urlparse(moved_from).netloc:
filtered.append(acc)
for acc in all_accounts:
if any([x for x in filtered if x.userInfo.id == acc.userInfo.id]):
continue
if host == urlparse(acc.serverInfo.url).netloc:
filtered.append(acc)
return filtered
class StreamWrapper:
def __init__(self, url: str = None) -> None:
raise SpeckleException(
+6
View File
@@ -185,6 +185,11 @@ class ActivityCollection(BaseModel):
return self.__repr__()
class ServerMigration(BaseModel):
movedTo: Optional[str] = None
movedFrom: Optional[str] = None
class ServerInfo(BaseModel):
name: Optional[str] = None
company: Optional[str] = None
@@ -197,3 +202,4 @@ class ServerInfo(BaseModel):
authStrategies: Optional[List[dict]] = None
version: Optional[str] = None
frontend2: Optional[bool] = None
migration: Optional[ServerMigration] = None
+2 -9
View File
@@ -7,7 +7,7 @@ from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.credentials import (
Account,
get_account_from_token,
get_local_accounts,
get_accounts_for_server,
)
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
from specklepy.transports.server.server import ServerTransport
@@ -178,14 +178,7 @@ class StreamWrapper:
if self._account and self._account.token:
return self._account
self._account = next(
(
a
for a in get_local_accounts()
if self.host == urlparse(a.serverInfo.url).netloc
),
None,
)
self._account = next(iter(get_accounts_for_server(self.host)), None)
if not self._account:
self._account = get_account_from_token(token, self.server_url)
+9 -9
View File
@@ -303,15 +303,15 @@ class Polyline(Base, speckle_type=GEOMETRY + "Polyline", chunkable={"value": 200
class SpiralType(Enum):
Biquadratic = (0,)
BiquadraticParabola = (1,)
Bloss = (2,)
Clothoid = (3,)
Cosine = (4,)
Cubic = (5,)
CubicParabola = (6,)
Radioid = (7,)
Sinusoid = (8,)
Biquadratic = 0
BiquadraticParabola = 1
Bloss = 2
Clothoid = 3
Cosine = 4
Cubic = 5
CubicParabola = 6
Radioid = 7
Sinusoid = 8
Unknown = 9
@@ -0,0 +1,77 @@
import os
import uuid
from typing import List, Optional, Tuple
from urllib.parse import urlparse
import pytest
from specklepy.core.api.credentials import Account, UserInfo, get_accounts_for_server
from specklepy.core.api.models import ServerInfo, ServerMigration
from specklepy.core.helpers import speckle_path_provider
def _create_account(
id: str, url: str, movedFrom: Optional[str], movedTo: Optional[str]
) -> Account:
return Account(
id=uuid.uuid4().hex[:6].lower(),
token="myToken",
serverInfo=ServerInfo(
url=url,
name="myServer",
migration=ServerMigration(movedTo=movedTo, movedFrom=movedFrom),
),
userInfo=UserInfo(id=id),
)
def _test_cases() -> List[Tuple[List[Account], str, List[Account]]]:
user_id_1 = uuid.uuid4().hex[:6].lower()
user_id_2 = uuid.uuid4().hex[:6].lower()
old = _create_account(
user_id_1, "https://old.example.com", None, "https://new.example.com"
)
new = _create_account(
user_id_1, "https://new.example.com", "https://old.example.com", None
)
other = _create_account(user_id_2, "https://other.example.com", None, None)
given_accounts = [old, new, other]
reversed = [other, new, old]
return [
(given_accounts, "https://old.example.com", [new]),
(given_accounts, "https://new.example.com", [new]),
(reversed, "https://old.example.com", [new]),
]
def _clean_accounts(accounts: List[Account]) -> None:
json_accounts = speckle_path_provider.accounts_folder_path()
for acc in accounts:
# deleting acc json file in json_accounts path
os.remove(os.path.join(json_accounts, f"{acc.id}.json"))
pass
def _add_accounts(accounts: List[Account]) -> None:
json_accounts = speckle_path_provider.accounts_folder_path()
for acc in accounts:
data = Account.model_dump_json(acc)
with open(os.path.join(json_accounts, f"{acc.id}.json"), "w") as f:
f.write(data)
@pytest.mark.parametrize("accounts, requested_url, expected", _test_cases())
def test_server_migration(
accounts: List[Account], requested_url: str, expected: List[Account]
) -> None:
_add_accounts(accounts)
try:
res = get_accounts_for_server(urlparse(requested_url).netloc)
assert res == expected
finally:
_clean_accounts(accounts)