summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
Diffstat (limited to 'python')
-rw-r--r--python/project.py299
1 files changed, 281 insertions, 18 deletions
diff --git a/python/project.py b/python/project.py
index e1f7171d..02a4155c 100644
--- a/python/project.py
+++ b/python/project.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2015-2023 Vector 35 Inc
+# Copyright (c) 2015-2024 Vector 35 Inc
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
@@ -34,7 +34,7 @@ AsPath = Union[PathLike, str]
#TODO: notifications
-def nop(*args, **kwargs):
+def _nop(*args, **kwargs):
"""
Function that just returns True, used as default for callbacks
@@ -43,7 +43,7 @@ def nop(*args, **kwargs):
return True
-def wrap_progress(progress_func: ProgressFuncType):
+def _wrap_progress(progress_func: ProgressFuncType):
"""
Wraps a progress function in a ctypes function for passing to the FFI
@@ -55,6 +55,9 @@ def wrap_progress(progress_func: ProgressFuncType):
class ProjectFile:
+ """
+ Class representing a file in a project
+ """
def __init__(self, handle: core.BNProjectFileHandle):
self._handle = handle
@@ -80,6 +83,11 @@ class ProjectFile:
@property
def project(self):
+ """
+ Get the project that owns this file
+
+ :return: Project that owns this file
+ """
proj_handle = core.BNProjectFileGetProject(self._handle)
if proj_handle is None:
@@ -89,34 +97,74 @@ class ProjectFile:
@property
def path_on_disk(self) -> str:
+ """
+ Get the path on disk to this file's contents
+
+ :return: Path on disk as a string
+ """
return core.BNProjectFileGetPathOnDisk(self._handle) # type: ignore
@property
def exists_on_disk(self) -> bool:
+ """
+ Check if this file's contents exist on disk
+
+ :return: True if this file's contents exist on disk, False otherwise
+ """
return core.BNProjectFileExistsOnDisk(self._handle)
@property
def id(self) -> str:
+ """
+ Get the unique id of this file
+
+ :return: Unique identifier of this file
+ """
return core.BNProjectFileGetId(self._handle) # type: ignore
@property
def name(self) -> str:
+ """
+ Get the name of this file
+
+ :return: Name of this file
+ """
return core.BNProjectFileGetName(self._handle) # type: ignore
@name.setter
def name(self, new_name: str):
+ """
+ Set the name of this file
+
+ :param new_name: Desired name
+ """
return core.BNProjectFileSetName(self._handle, new_name)
@property
def description(self) -> str:
+ """
+ Get the description of this file
+
+ :return: Description of this file
+ """
return core.BNProjectFileGetDescription(self._handle) # type: ignore
@description.setter
def description(self, new_description: str):
+ """
+ Set the description of this file
+
+ :param new_description: Desired description
+ """
return core.BNProjectFileSetDescription(self._handle, new_description)
@property
def folder(self) -> Optional['ProjectFolder']:
+ """
+ Get the folder that contains this file
+
+ :return: Folder that contains this file, or None
+ """
folder_handle = core.BNProjectFileGetFolder(self._handle)
if folder_handle is None:
return None
@@ -124,14 +172,28 @@ class ProjectFile:
@folder.setter
def folder(self, new_folder: Optional['ProjectFolder']):
+ """
+ Set the folder that contains this file
+
+ :param new_parent: The folder that will contain this file, or None
+ """
folder_handle = None if new_folder is None else new_folder._handle
core.BNProjectFileSetFolder(self._handle, folder_handle)
def export(self, dest: AsPath) -> bool:
+ """
+ Export this file to disk
+
+ :param dest: Destination path for the exported contents
+ :return: True if the export succeeded, False otherwise
+ """
return core.BNProjectFileExport(self._handle, str(dest))
class ProjectFolder:
+ """
+ Class representing a folder in a project
+ """
def __init__(self, handle: core.BNProjectFolderHandle):
self._handle = handle
@@ -157,6 +219,11 @@ class ProjectFolder:
@property
def project(self):
+ """
+ Get the project that owns this folder
+
+ :return: Project that owns this folder
+ """
proj_handle = core.BNProjectFolderGetProject(self._handle)
if proj_handle is None:
@@ -166,26 +233,56 @@ class ProjectFolder:
@property
def id(self) -> str:
+ """
+ Get the unique id of this folder
+
+ :return: Unique identifier of this folder
+ """
return core.BNProjectFolderGetId(self._handle) # type: ignore
@property
def name(self) -> str:
+ """
+ Get the name of this folder
+
+ :return: Name of this folder
+ """
return core.BNProjectFolderGetName(self._handle) # type: ignore
@name.setter
def name(self, new_name: str):
+ """
+ Set the name of this folder
+
+ :param new_name: Desired name
+ """
return core.BNProjectFolderSetName(self._handle, new_name)
@property
def description(self) -> str:
+ """
+ Get the description of this folder
+
+ :return: Description of this folder
+ """
return core.BNProjectFolderGetDescription(self._handle) # type: ignore
@description.setter
def description(self, new_description: str):
+ """
+ Set the description of this folder
+
+ :param new_description: Desired description
+ """
return core.BNProjectFolderSetDescription(self._handle, new_description)
@property
def parent(self) -> Optional['ProjectFolder']:
+ """
+ Get the parent folder of this folder
+
+ :return: Folder that contains this folder, or None if it is a root folder
+ """
folder_handle = core.BNProjectFolderGetParent(self._handle)
if folder_handle is None:
return None
@@ -193,14 +290,29 @@ class ProjectFolder:
@parent.setter
def parent(self, new_parent: Optional['ProjectFolder']):
+ """
+ Set the parent folder of this folder
+
+ :param new_parent: The folder that will contain this folder, or None
+ """
parent_handle = None if new_parent is None else new_parent._handle
core.BNProjectFolderSetParent(self._handle, parent_handle)
- def export(self, dest: AsPath, progress_func: ProgressFuncType = nop) -> bool:
- return core.BNProjectFolderExport(self._handle, str(dest), None, wrap_progress(progress_func))
+ def export(self, dest: AsPath, progress_func: ProgressFuncType = _nop) -> bool:
+ """
+ Recursively export this folder to disk
+
+ :param dest: Destination path for the exported contents
+ :param progress_func: Progress function that will be called as contents are exporting
+ :return: True if the export succeeded, False otherwise
+ """
+ return core.BNProjectFolderExport(self._handle, str(dest), None, _wrap_progress(progress_func))
class Project:
+ """
+ Class representing a project
+ """
def __init__(self, handle: core.BNProjectHandle):
self._handle = handle
@@ -216,6 +328,13 @@ class Project:
@staticmethod
def open_project(path: AsPath) -> 'Project':
+ """
+ Open an existing project
+
+ :param path: Path to the project directory (.bnpr) or project metadata file (.bnpm)
+ :return: Opened project
+ :raises ProjectException: If there was an error opening the project
+ """
project_handle = core.BNOpenProject(str(path))
if project_handle is None:
raise ProjectException("Failed to open project")
@@ -223,61 +342,139 @@ class Project:
@staticmethod
def create_project(path: AsPath, name: str) -> 'Project':
+ """
+ Create a new project
+
+ :param path: Path to the project directory (.bnpr)
+ :param name: Name of the new project
+ :return: Opened project
+ :raises ProjectException: If there was an error creating the project
+ """
project_handle = core.BNCreateProject(str(path), name)
if project_handle is None:
raise ProjectException("Failed to create project")
return Project(handle=project_handle)
def open(self) -> bool:
+ """
+ Open a closed project
+
+ :return: True if the project is now open, False otherwise
+ """
return core.BNProjectOpen(self._handle)
def close(self) -> bool:
+ """
+ Close an opened project
+
+ :return: True if the project is now closed, False otherwise
+ """
return core.BNProjectClose(self._handle)
@property
def id(self) -> str:
+ """
+ Get the unique id of this project
+
+ :return: Unique identifier of project
+ """
return core.BNProjectGetId(self._handle) # type: ignore
@property
def is_open(self) -> bool:
+ """
+ Check if the project is currently open
+
+ :return: True if the project is currently open, False otherwise
+ """
return core.BNProjectIsOpen(self._handle)
@property
def path(self) -> str:
+ """
+ Get the path of the project
+
+ :return: Path of the project's .bnpr directory
+ """
return core.BNProjectGetPath(self._handle) # type: ignore
@property
def name(self) -> str:
+ """
+ Get the name of the project
+
+ :return: Name of the project
+ """
return core.BNProjectGetName(self._handle) # type: ignore
@name.setter
def name(self, new_name: str):
+ """
+ Set the name of the project
+
+ :param new_name: Desired name
+ """
core.BNProjectSetName(self._handle, new_name)
@property
def description(self) -> str:
+ """
+ Get the description of the project
+
+ :return: Description of the project
+ """
return core.BNProjectGetDescription(self._handle) # type: ignore
@description.setter
def description(self, new_description: str):
+ """
+ Set the description of the project
+
+ :param new_description: Desired description
+ """
core.BNProjectSetDescription(self._handle, new_description)
def query_metadata(self, key: str) -> MetadataValueType:
+ """
+ Retrieves metadata stored under a key from the project
+
+ :param str key: Key to query
+ """
md_handle = core.BNProjectQueryMetadata(self._handle, key)
if md_handle is None:
raise KeyError(key)
return Metadata(handle=md_handle).value
def store_metadata(self, key: str, value: MetadataValueType):
+ """
+ Stores metadata within the project
+
+ :param str key: Key under which to store the Metadata object
+ :param Varies value: Object to store
+ """
_val = value
if not isinstance(_val, Metadata):
_val = Metadata(_val)
core.BNProjectStoreMetadata(self._handle, key, _val.handle)
def remove_metadata(self, key: str):
+ """
+ Removes the metadata associated with this key from the project
+
+ :param str key: Key associated with the metadata object to remove
+ """
core.BNProjectRemoveMetadata(self._handle, key)
- def create_folder_from_path(self, path: Union[PathLike, str], parent: Optional[ProjectFolder] = None, description: str = "", progress_func: ProgressFuncType = nop) -> ProjectFolder:
+ def create_folder_from_path(self, path: Union[PathLike, str], parent: Optional[ProjectFolder] = None, description: str = "", progress_func: ProgressFuncType = _nop) -> ProjectFolder:
+ """
+ Recursively create files and folders in the project from a path on disk
+
+ :param path: Path to folder on disk
+ :param parent: Parent folder in the project that will contain the new contents
+ :param description: Description for created root folder
+ :param progress_func: Progress function that will be called
+ :return: Created root folder
+ """
parent_handle = parent._handle if parent is not None else None
folder_handle = core.BNProjectCreateFolderFromPath(
project=self._handle,
@@ -285,7 +482,7 @@ class Project:
parent=parent_handle,
description=description,
ctxt=None,
- progress=wrap_progress(progress_func)
+ progress=_wrap_progress(progress_func)
)
if folder_handle is None:
@@ -294,6 +491,14 @@ class Project:
return ProjectFolder(handle=folder_handle)
def create_folder(self, parent: Optional[ProjectFolder], name: str, description: str = "") -> ProjectFolder:
+ """
+ Recursively create files and folders in the project from a path on disk
+
+ :param parent: Parent folder in the project that will contain the new folder
+ :param name: Name for the created folder
+ :param description: Description for created folder
+ :return: Created folder
+ """
parent_handle = parent._handle if parent is not None else None
folder_handle = core.BNProjectCreateFolder(
project=self._handle,
@@ -309,6 +514,11 @@ class Project:
@property
def folders(self) -> List[ProjectFolder]:
+ """
+ Get a list of folders in the project
+
+ :return: List of folders in the project
+ """
count = ctypes.c_size_t()
value = core.BNProjectGetFolders(self._handle, count)
if value is None:
@@ -325,19 +535,37 @@ class Project:
core.BNFreeProjectFolderList(value, count.value)
def get_folder_by_id(self, id: str) -> Optional[ProjectFolder]:
+ """
+ Retrieve a folder in the project by unique id
+
+ :param id: Unique identifier for a folder
+ :return: Folder with the requested id or None
+ """
handle = core.BNProjectGetFolderById(self._handle, id)
if handle is None:
return None
folder = ProjectFolder(handle)
return folder
- def push_folder(self, folder: ProjectFolder):
- core.BNProjectPushFolder(self._handle, folder._handle)
+ def delete_folder(self, folder: ProjectFolder, progress_func: ProgressFuncType = _nop):
+ """
+ Recursively delete a folder from the project
+
+ :param folder: Folder to delete recursively
+ :param progress_func: Progress function that will be called as objects get deleted
+ """
+ core.BNProjectDeleteFolder(self._handle, folder._handle, None, _wrap_progress(progress_func))
- def delete_folder(self, folder: ProjectFolder, progress_func: ProgressFuncType = nop):
- core.BNProjectDeleteFolder(self._handle, folder._handle, None, wrap_progress(progress_func))
+ def create_file_from_path(self, path: AsPath, folder: Optional[ProjectFile], name: str, description: str = "", progress_func: ProgressFuncType = _nop) -> ProjectFile:
+ """
+ Create a file in the project from a path on disk
- def create_file_from_path(self, path: AsPath, folder: Optional[ProjectFile], name: str, description: str = "", progress_func: ProgressFuncType = nop) -> ProjectFile:
+ :param path: Path on disk
+ :param folder: Folder to place the created file in
+ :param name: Name to assign to the created file
+ :param description: Description to assign to the created file
+ :param progress_func: Progress function that will be called as the file is being added
+ """
folder_handle = folder._handle if folder is not None else None
file_handle = core.BNProjectCreateFileFromPath(
project=self._handle,
@@ -346,7 +574,7 @@ class Project:
name=name,
description=description,
ctxt=None,
- progress=wrap_progress(progress_func)
+ progress=_wrap_progress(progress_func)
)
if file_handle is None:
@@ -354,7 +582,16 @@ class Project:
return ProjectFile(handle=file_handle)
- def create_file(self, contents: bytes, folder: Optional[ProjectFile], name: str, description: str = "", progress_func: ProgressFuncType = nop) -> ProjectFile:
+ def create_file(self, contents: bytes, folder: Optional[ProjectFile], name: str, description: str = "", progress_func: ProgressFuncType = _nop) -> ProjectFile:
+ """
+ Create a file in the project
+
+ :param contents: Bytes of the file that will be created
+ :param folder: Folder to place the created file in
+ :param name: Name to assign to the created file
+ :param description: Description to assign to the created file
+ :param progress_func: Progress function that will be called as the file is being added
+ """
folder_handle = folder._handle if folder is not None else None
buf = (ctypes.c_ubyte * len(contents))()
ctypes.memmove(buf, contents, len(contents))
@@ -366,7 +603,7 @@ class Project:
name=name,
description=description,
ctxt=None,
- progress=wrap_progress(progress_func)
+ progress=_wrap_progress(progress_func)
)
if file_handle is None:
@@ -376,6 +613,11 @@ class Project:
@property
def files(self) -> List[ProjectFile]:
+ """
+ Get a list of files in the project
+
+ :return: List of files in the project
+ """
count = ctypes.c_size_t()
value = core.BNProjectGetFiles(self._handle, count)
if value is None:
@@ -392,20 +634,41 @@ class Project:
core.BNFreeProjectFileList(value, count.value)
def get_file_by_id(self, id: str) -> Optional[ProjectFile]:
+ """
+ Retrieve a file in the project by unique id
+
+ :param id: Unique identifier for a file
+ :return: File with the requested id or None
+ """
handle = core.BNProjectGetFileById(self._handle, id)
if handle is None:
return None
file = ProjectFile(handle)
return file
- def push_file(self, file: ProjectFile):
- core.BNProjectPushFile(self._handle, file._handle)
-
def delete_file(self, file: ProjectFile):
+ """
+ Delete a file from the project
+
+ :param file: File to delete
+ """
core.BNProjectDeleteFile(self._handle, file._handle)
@contextmanager
def bulk_operation(self):
+ """
+ A context manager to speed up bulk project operations.
+ Project modifications are synced to disk in chunks,
+ and the project on disk vs in memory may not agree on state
+ if an exception occurs while a bulk operation is happening.
+
+ :example:
+ >>> from pathlib import Path
+ >>> with project.bulk_operation():
+ ... for i in Path('/bin/').iterdir():
+ ... if i.is_file() and not i.is_symlink():
+ ... project.create_file_from_path(i, None, i.name)
+ """
core.BNProjectBeginBulkOperation(self._handle)
yield
core.BNProjectEndBulkOperation(self._handle)