import os
import zipfile
import sys
from typing import Dict, List, Optional, Optional, Union, TYPE_CHECKING
from urllib.parse import urljoin
from .base import AsyncBase
from ..exception import ApiRequestError
from ..utils import get_save_path
if TYPE_CHECKING:
from . import AsyncGeoboxClient
from .user import AsyncUser
[docs]
class AsyncModel(AsyncBase):
BASE_ENDPOINT = '3dmodels/'
[docs]
def __init__(self,
api: 'AsyncGeoboxClient',
uuid: str,
data: Optional[Dict] = {}):
"""
Initialize a 3D Model instance.
Args:
api (AsyncGeoboxClient): The AsyncGeoboxClient instance for making requests.
uuid (str): The unique identifier for the model.
data (Dict, optional): The data of the model.
"""
super().__init__(api, uuid=uuid, data=data)
[docs]
@classmethod
async def get_models(cls, api: 'AsyncGeoboxClient', **kwargs) -> Union[List['AsyncModel'], int]:
"""
[async] Get a list of models with optional filtering and pagination.
Args:
api (AsyncGeoboxClient): The AsyncGeoboxClient instance for making requests.
Keyword Args:
q (str): query filter based on OGC CQL standard. e.g. "field1 LIKE '%GIS%' AND created_at > '2021-01-01'".
search (str): search term for keyword-based searching among search_fields or all textual fields if search_fields does not have value. NOTE: if q param is defined this param will be ignored.
search_fields (str): comma separated list of fields for searching.
order_by (str): comma separated list of fields for sorting results [field1 A|D, field2 A|D, …]. e.g. name A, type D. NOTE: "A" denotes ascending order and "D" denotes descending order.
return_count (bool): whether to return total count. default is False.
skip (int): number of models to skip. default is 0.
limit (int): maximum number of models to return. default is 10.
user_id (int): specific user. privileges required.
shared (bool): Whether to return shared models. default is False.
Returns:
List[AsyncModel] | int: A list of Model objects or the count number.
Example:
>>> from geobox.aio import AsyncGeoboxClient
>>> from geobox.aio.model3d import AsyncModel
>>> async with AsyncgeoboxClient() as client:
>>> models = await AsyncModel.get_models(api=client,
... search="my_model",
... search_fields="name, description",
... order_by="name A",
... return_count=True,
... skip=0,
... limit=10,
... shared=False)
or
>>> models = await client.get_models(search="my_model",
... search_fields="name, description",
... order_by="name A",
... return_count=True,
... skip=0,
... limit=10,
... shared=False)
"""
params = {
'f': 'json',
'q': kwargs.get('q'),
'search': kwargs.get('search'),
'search_fields': kwargs.get('search_fields'),
'order_by': kwargs.get('order_by'),
'return_count': kwargs.get('return_count', False),
'skip': kwargs.get('skip', 0),
'limit': kwargs.get('limit', 10),
'user_id': kwargs.get('user_id'),
'shared': kwargs.get('shared', False)
}
return await super()._get_list(api, cls.BASE_ENDPOINT, params, factory_func=lambda api, item: AsyncModel(api, item['uuid'], item))
[docs]
@classmethod
async def get_model(cls, api: 'AsyncGeoboxClient', uuid: str, user_id: int = None) -> 'AsyncModel':
"""
[async] Get a model by its UUID.
Args:
api (AsyncGeoboxClient): The AsyncGeoboxClient instance for making requests.
uuid (str): The UUID of the model to get.
user_id (int, optional): Specific user. privileges required.
Returns:
AsyncModel: The model object.
Raises:
NotFoundError: If the model with the specified UUID is not found.
Example:
>>> from geobox.aio import AsyncGeoboxClient
>>> from geobox.aio.model3d import AsyncModel
>>> async with AsyncgeoboxClient() as client:
>>> model = await AsyncModel.get_model(client, uuid="12345678-1234-5678-1234-567812345678")
or
>>> model = await client.get_model(uuid="12345678-1234-5678-1234-567812345678")
"""
params = {
'f': 'json',
'user_id': user_id
}
return await super()._get_detail(api, cls.BASE_ENDPOINT, uuid, params, factory_func=lambda api, item: AsyncModel(api, item['uuid'], item))
[docs]
@classmethod
async def get_model_by_name(cls, api: 'AsyncGeoboxClient', name: str, user_id: int = None) -> Union['AsyncModel', None]:
"""
[async] Get a model by name
Args:
api (AsyncGeoboxClient): The AsyncGeoboxClient instance for making requests.
name (str): the name of the model to get
user_id (int, optional): specific user. privileges required.
Returns:
AsyncModel | None: returns the model if a model matches the given name, else None
Example:
>>> from geobox.aio import AsyncGeoboxClient
>>> from geobox.aio.model3d import AsyncModel
>>> async with AsyncgeoboxClient() as client:
>>> model = await AsyncModel.get_model_by_name(client, name='test')
or
>>> model = await client.get_model_by_name(name='test')
"""
models = await cls.get_models(api, q=f"name = '{name}'", user_id=user_id)
if models and models[0].name == name:
return models[0]
else:
return None
[docs]
async def update(self, **kwargs) -> Dict:
"""
[async] Update the model's properties.
Keyword Args:
name (str): The new name for the model.
display_name (str): The new display name.
description (str): The new description for the model.
settings (Dict): The new settings for the model.
thumbnail (str): The new thumbnail for the model.
Returns:
Dict: The updated model data.
Raises:
ValidationError: If the update data is invalid.
Example:
>>> from geobox.aio import AsyncGeoboxClient
>>> from geobox.aio.model3d import AsyncModel
>>> async with AsyncgeoboxClient() as client:
>>> model = await AsyncModel.get_model(api=client, uuid="12345678-1234-5678-1234-567812345678")
>>> settings = {
... "model_settings": {
... "scale": 0,
... "rotation": [
... 0
... ],
... "location": [
... 0
... ]
... },
... "view_settings": {
... "center": [
... 0
... ],
... "zoom": 0,
... "pitch": 0,
... "bearing": 0
... }
... }
>>> await model.update(name="new_name", description="new_description", settings=settings, thumbnail="new_thumbnail")
"""
data = {
'name': kwargs.get('name'),
'display_name': kwargs.get('display_name'),
'description': kwargs.get('description'),
'settings': kwargs.get('settings'),
'thumbnail': kwargs.get('thumbnail')
}
return await super()._update(self.endpoint, data)
[docs]
async def delete(self) -> None:
"""
[async] Delete the model.
Returns:
None
Example:
>>> from geobox.aio import AsyncGeoboxClient
>>> from geobox.aio.model3d import AsyncModel
>>> async with AsyncgeoboxClient() as client:
>>> model = await AsyncModel.get_model(api=client, uuid="12345678-1234-5678-1234-567812345678")
>>> await model.delete()
"""
await super()._delete(self.endpoint)
[docs]
def _create_progress_bar(self) -> 'tqdm':
"""Creates a progress bar for the task."""
try:
from tqdm.auto import tqdm
except ImportError:
from .api import logger
logger.warning("[tqdm] extra is required to show the progress bar. install with: pip insatll geobox[tqdm]")
return None
return tqdm(unit="B",
total=int(self.size),
file=sys.stdout,
dynamic_ncols=True,
desc="Downloading",
unit_scale=True,
unit_divisor=1024,
ascii=True
)
[docs]
async def download(self, save_path: str = None, progress_bar: bool = True) -> str:
"""
[async] Download the 3D model, save it as a .glb file, zip it, and return the zip file path.
Args:
save_path (str, optional): Directory where the file should be saved.
progress_bar (bool, optional): Whether to show a progress bar. Default: True
Returns:
str: Path to the .zip file containing the .glb model.
Raises:
ApiRequestError: If the API request fails.
Example:
>>> from geobox.aio import AsyncGeoboxClient
>>> async with AsyncgeoboxClient() as client:
>>> model = await client.get_models()[0]
>>> await model.download()
"""
if not self.uuid:
raise ValueError("Model UUID is required to download content")
if self.data.get('obj'):
model = await self.api.get_model(self.obj)
else:
model = self
save_path = get_save_path(save_path)
os.makedirs(save_path, exist_ok=True)
endpoint = urljoin(model.api.base_url, f"{model.endpoint}/content/")
async with model.api.session.session.get(endpoint) as response:
if response.status != 200:
raise ApiRequestError(f"Failed to get model content: {response.status}")
glb_filename = f"{model.name}.glb"
glb_path = os.path.join(save_path, glb_filename)
pbar = model._create_progress_bar() if progress_bar else None
with open(glb_path, "wb") as f:
async for chunk in response.content.iter_chunked(8192):
f.write(chunk)
if pbar:
pbar.update(len(chunk))
pbar.refresh()
if pbar:
pbar.close()
zip_filename = f"{model.name}.zip"
zip_path = os.path.join(save_path, zip_filename)
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
zipf.write(glb_path, arcname=os.path.basename(glb_path))
os.remove(glb_path)
return os.path.abspath(zip_path)
@property
def thumbnail(self) -> str:
"""
Get the thumbnail URL of the model.
Returns:
str: The thumbnail of the model.
Example:
>>> from geobox.aio import AsyncGeoboxClient
>>> from geobox.aio.model3d import AsyncModel
>>> async with AsyncgeoboxClient() as client:
>>> model = await AsyncModel.get_model(api=client, uuid="12345678-1234-5678-1234-567812345678")
>>> model.thumbnail
"""
return super()._thumbnail()
[docs]
async def share(self, users: List['AsyncUser']) -> None:
"""
[async] Shares the model with specified users.
Args:
users (List[AsyncUsers]): The list of user objects to share the model with.
Returns:
None
Example:
>>> from geobox.aio import AsyncGeoboxClient
>>> from geobox.aio.model3d import AsyncModel
>>> async with AsyncgeoboxClient() as client:
>>> model = await AsyncModel.get_model(client, uuid="12345678-1234-5678-1234-567812345678")
>>> users = await client.search_users(search='John')
>>> await model.share(users=users)
"""
await super()._share(self.endpoint, users)
[docs]
async def unshare(self, users: List['AsyncUser']) -> None:
"""
[async] Unshares the model with specified users.
Args:
users (List[AsyncUser]): The list of user objects to unshare the model with.
Returns:
None
Example:
>>> from geobox.aio import AsyncGeoboxClient
>>> from geobox.aio.model3d import AsyncModel
>>> async with AsyncgeoboxClient() as client:
>>> model = await AsyncModel.get_model(client, uuid="12345678-1234-5678-1234-567812345678")
>>> users = await client.search_users(search='John')
>>> await model.unshare(users=users)
"""
await super()._unshare(self.endpoint, users)
[docs]
async def get_shared_users(self, search: str = None, skip: int = 0, limit: int = 10) -> List['AsyncUser']:
"""
[async] Retrieves the list of users the model is shared with.
Args:
search (str, optional): The search query.
skip (int, optional): The number of users to skip.
limit (int, optional): The maximum number of users to retrieve.
Returns:
List[AsyncUser]: The list of shared users.
Example:
>>> from geobox.aio import AsyncGeoboxClient
>>> from geobox.aio.model3d import AsyncModel
>>> async with AsyncgeoboxClient() as client:
>>> model = await AsyncModel.get_model(client, uuid="12345678-1234-5678-1234-567812345678")
>>> await model.get_shared_users(search='John', skip=0, limit=10)
"""
params = {
'search': search,
'skip': skip,
'limit': limit
}
return await super()._get_shared_users(self.endpoint, params)