Skip to content

Commit 0256073

Browse files
Merge pull request #9 from Unity-Technologies/release/AME.2025.02
Release 0.3.0
2 parents 2cf5941 + 1e62a09 commit 0256073

File tree

13 files changed

+260
-65
lines changed

13 files changed

+260
-65
lines changed

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@ All notable changes to this package will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
66
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
77

8+
## [0.3.0] - 2025-02-04
9+
10+
### Added
11+
- Added the column description in the csv to allow edit of individual asset descriptions.
12+
- Add multiple user input validations during the interactive config mode.
13+
- Added `app_settings.json` file to configure applications amount of parallel workers and environment variables when needed.
14+
15+
### Changed
16+
- Organizations and projects are now selected via a list in the interactive config mode and delete mode.
17+
18+
### Fixed
19+
- Bug with special characters when reading CSV files.
20+
- Bug with boolean metadata always being read as false.
21+
- CSV generated headlessly now respect the same template as CSV generated in an interactive run.
22+
- Relative path used to indicate assets location are now supported.
23+
- Fix tags and collection not being added to config when saving it.
24+
825
## [0.2.0] - 2024-11-19
926

1027
### Added

bulk_upload_cli/README.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Find and connect support services on the [Help & Support](https://cloud.unity.co
1919
- [Creating a csv from a Unity Cloud project](#creating-a-csv-from-a-unity-cloud-project)
2020
- [Editing metadata in the csv file](#editing-metadata-in-the-csv-file)
2121
- [Use an existing configuration file](#use-an-existing-configuration-file)
22+
- [Fine-tune the asset creation and upload](#fine-tune-the-asset-creation-and-upload)
23+
- [Troubleshoot](#troubleshoot)
2224
- [See also](#see-also)
2325
- [Tell us what you think](#tell-us-what-you-think)
2426

@@ -62,10 +64,10 @@ The bulk upload sample script is provided under the [Unity ToS license](../LICEN
6264

6365
### Select the input method
6466

65-
If you have a CSV respecting the template, when prompted about it, you can select yes and provide the path to the CSV file. Otherwise, you will be prompted about your assets location.
66-
6767
Select one of the three strategies as the input method for bulk asset creation:
6868

69+
- **listed in a casv respecting the CLI tool template**: Select this option if you built a CSV listing your assets location and details using the provided template.
70+
* Provide the path to the csv file.
6971
- **in a .unitypackage file**: Select this option if your assets are in a .unitypackage file. The tool extracts the assets from the .unitypackage file and uploads them to the cloud.
7072
* Provide the path to the .unitypackage file.
7173
- **in a local unity project**: Select this option if your assets are in a local Unity project.
@@ -125,6 +127,21 @@ To use an existing configuration file, follow these steps:
125127
3. On the next run with the `--create` flag, you can add the `--config` flag followed by the name of the configuration file you created. All the answers you gave during the first run will be loaded from the configuration file.
126128
4. Alternatively, you can use the `--config-select` flag to select a configuration file from the list of existing configuration files.
127129

130+
### Fine-tune the asset creation and upload
131+
132+
With the `app_settings.json` file, you can fine-tune the amount of assets created and uploaded in parallel. Depending on your network, the number of assets, and the size of the assets, you can adjust the following settings:
133+
- `parallelCreationEdit`: The number of assets created and updated in parallel. This settings can be kept high as it is not resource intensive.
134+
- `parallelAssetUpload`: The number of assets that will have their files uploaded in parallel. This setting should be adjusted depending on the size of the assets and the network speed. When dealing with large files (>100MB), it is recommended to keep this setting low (1-2) to avoid time out.
135+
- `parallelFileUploadPerAsset`: The number of files uploaded in parallel for each asset. This setting should be adjusted depending on the number of files and the network speed. It is recommended to adjust it according to `parallelAssetUpload`, as the total number of files uploaded in parallel will be `parallelAssetUpload * parallelFileUploadPerAsset`.
136+
137+
In the `app_settings.json` file, you can also add environment variables that will be set at runtime. This is useful when running the CLI tool in a private network environment.
138+
139+
## Troubleshoot
140+
141+
Here's a list of common problems you might encounter while using the CLI Tool.
142+
- `error ModuleNotFoundError: No module named ...`: This can be caused by a uncompleted installation. Start by uninstalling `unity_cloud` with `pip(3) uninstall unity_cloud`, then re-run the CLI tool installation.
143+
- Timeout exception during the upload step: When uploading large files, it is recommended to lower the amount of parallel uploads allowed. To do so, refer to the [Fine-tune the asset creation and upload](#fine-tune-the-asset-creation-and-upload) section.
144+
128145
## See also
129146
For more information, see the [Unity Cloud Python SDK](https://docs.unity.com/cloud/en-us/asset-manager/python-sdk) documentation.
130147

bulk_upload_cli/app_settings.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"parallelCreationEdit": 20,
3+
"parallelAssetUpload": 2,
4+
"parallelFileUploadPerAsset": 5,
5+
"environmentVariables": {}
6+
}

bulk_upload_cli/bulk_upload/asset_deleter.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,22 @@ def delete_assets_in_project():
3939
key_id, key = ask_for_login()
4040
login(key_id, key)
4141

42-
org_id = inquirer.text(message="Enter your organization ID:").execute()
43-
project_id = inquirer.text(message="Enter your project ID:").execute()
42+
organizations = uc.identity.get_organization_list()
43+
if len(organizations) == 0:
44+
print("No organizations found. Please create an organization first.")
45+
exit(1)
46+
org_selected = inquirer.select(message="Select an organization:",
47+
choices=[org.name for org in organizations]).execute()
48+
org_id = [org.id for org in organizations if org.name == org_selected][0]
49+
50+
projects = uc.identity.get_project_list(org_id)
51+
if len(projects) == 0:
52+
print("No projects found. Please create a project first.")
53+
exit(1)
54+
55+
selected_project = inquirer.select(message="Select a project:",
56+
choices=[project.name for project in projects]).execute()
57+
project_id = [project.id for project in projects if project.name == selected_project][0]
4458

4559
project_assets = uc.assets.get_asset_list(org_id, project_id)
4660
confirm = inquirer.confirm(message=f"Are you sure you want to delete {len(project_assets)} assets?").execute()

bulk_upload_cli/bulk_upload/asset_mappers.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,9 @@ class FolderGroupingAssetMapper(AssetMapper):
104104

105105
def map_assets(self, config: ProjectUploaderConfig) -> [AssetInfo]:
106106

107-
hierarchical_level = int(config.hierarchical_level) + os.path.abspath(config.assets_path).count(os.sep)
108-
folders = [x[0] for x in os.walk(config.assets_path) if x[0].count(os.sep) == hierarchical_level]
107+
abs_path = os.path.abspath(config.assets_path)
108+
hierarchical_level = int(config.hierarchical_level) + abs_path.count(os.sep)
109+
folders = [x[0] for x in os.walk(abs_path) if x[0].count(os.sep) == hierarchical_level]
109110

110111
if len(folders) == 0:
111112
print(f"No folders found in the assets path. Only the root folder will be considered as an asset")
@@ -116,7 +117,7 @@ def map_assets(self, config: ProjectUploaderConfig) -> [AssetInfo]:
116117
asset_name = PurePath(folder).name
117118
assets[asset_name] = AssetInfo(asset_name)
118119

119-
files = [get_file_info(PurePath(f), config.assets_path) for x in os.walk(folder) for f in glob(os.path.join(x[0], '*'))]
120+
files = [get_file_info(PurePath(f), abs_path) for x in os.walk(folder) for f in glob(os.path.join(x[0], '*'))]
120121
# remove files with excluded extensions
121122
files = [f for f in files if not any(f.path.suffix.endswith(ext) for ext in config.excluded_file_extensions)]
122123
assets[asset_name].files = files
@@ -129,7 +130,7 @@ def map_assets(self, config: ProjectUploaderConfig) -> [AssetInfo]:
129130
asset.files.remove(file)
130131

131132
if config.preview_detection and self.is_preview_file(file.path):
132-
asset.preview_files.append(get_file_info(file.path, config.assets_path))
133+
asset.preview_files.append(get_file_info(file.path, abs_path))
133134
asset.files.remove(file)
134135

135136
return list(assets.values())
@@ -219,6 +220,7 @@ def clean_up(self):
219220
pass
220221

221222
def map_assets(self, config: ProjectUploaderConfig) -> [AssetInfo]:
223+
absolute_path = os.path.abspath(config.assets_path)
222224
# find all files in the assets folder and sub folders
223225
files = [y for x in os.walk(config.assets_path) for y in glob(os.path.join(x[0], '*'))]
224226
# remove files with excluded extensions
@@ -247,7 +249,7 @@ def map_assets(self, config: ProjectUploaderConfig) -> [AssetInfo]:
247249

248250
file_path = PurePath(file)
249251
asset = AssetInfo(os.path.basename(file))
250-
asset.files.append(FileInfo(file_path, config.assets_path))
252+
asset.files.append(FileInfo(file_path, PurePosixPath(file_path.relative_to(config.assets_path))))
251253

252254
if file_path.stem.lower() in potential_previews:
253255
preview_file = potential_previews[file_path.stem.lower()]
@@ -285,7 +287,7 @@ def clean_up(self):
285287

286288
def map_assets(self, config: ProjectUploaderConfig) -> [AssetInfo]:
287289
assets = []
288-
with open(config.assets_path, 'r') as file:
290+
with open(config.assets_path, 'r', encoding='utf-8') as file:
289291
reader = csv.DictReader(file)
290292
input_row = next(reader)
291293

bulk_upload_cli/bulk_upload/assets_customization_providers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ def apply_asset_customization(self, assets: [AssetInfo], config: ProjectUploader
2121
asset_customization.tags.append(PurePath(config.assets_path).name.replace(".unitypackage", ""))
2222

2323
asset_customization.collection = self.get_collection(config.org_id, config.project_id)
24-
#asset_customization.metadata = self.get_metadata()
2524

25+
config.tags = asset_customization.tags
26+
config.collection = asset_customization.collection
2627
for asset in assets:
2728
asset.customization.tags = asset_customization.tags
2829
asset.customization.collection = asset_customization.collection

bulk_upload_cli/bulk_upload/assets_uploaders.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
1-
import json
21
import logging
2+
import time
33

4-
import unity_cloud as uc
54
import unity_cloud.assets.asset_reference
65

76
from bulk_upload.asset_mappers import *
7+
from bulk_upload.models import *
88
from concurrent.futures import ThreadPoolExecutor, wait
99
from unity_cloud.models import *
10-
from pathlib import PurePath, PurePosixPath
1110

1211

1312
logger = logging.getLogger(__name__)
1413

1514

1615
class AssetUploader(ABC):
1716
@abstractmethod
18-
def upload_assets(self, asset_infos: [AssetInfo], config: ProjectUploaderConfig):
17+
def upload_assets(self, asset_infos: [AssetInfo], config: ProjectUploaderConfig, app_settings: AppSettings):
1918
pass
2019

2120

@@ -25,7 +24,7 @@ def __init__(self):
2524
self.config = None
2625
self.futures = list()
2726

28-
def upload_assets(self, asset_infos: [AssetInfo], config: ProjectUploaderConfig):
27+
def upload_assets(self, asset_infos: [AssetInfo], config: ProjectUploaderConfig, app_settings: AppSettings):
2928
self.config = config
3029

3130
cloud_assets = [] if config.strategy == Strategy.CLOUD_ASSET else uc.assets.get_asset_list(self.config.org_id, self.config.project_id)
@@ -42,7 +41,7 @@ def upload_assets(self, asset_infos: [AssetInfo], config: ProjectUploaderConfig)
4241
asset.is_frozen_in_cloud = project_asset.is_frozen
4342
break
4443

45-
with ThreadPoolExecutor(max_workers=self.config.amount_of_parallel_uploads) as executor:
44+
with ThreadPoolExecutor(max_workers=app_settings.parallel_creation_edit) as executor:
4645
for asset in asset_infos:
4746
if not asset.already_in_cloud:
4847
self.futures.append(executor.submit(self.create_asset, asset))
@@ -53,25 +52,28 @@ def upload_assets(self, asset_infos: [AssetInfo], config: ProjectUploaderConfig)
5352
self.futures = list()
5453

5554
print("Setting asset dependencies", flush=True)
56-
with ThreadPoolExecutor(max_workers=self.config.amount_of_parallel_uploads) as executor:
55+
with ThreadPoolExecutor(max_workers=app_settings.parallel_creation_edit) as executor:
5756
for asset in asset_infos:
5857
self.futures.append(executor.submit(self.set_asset_references, asset, asset_infos))
5958

6059
wait(self.futures)
6160
self.futures = list()
6261

62+
#sleep for 5 seconds to allow the asset to be created with their dataset
63+
time.sleep(5)
64+
6365
if self.config.update_files and self.config.strategy == Strategy.CLOUD_ASSET:
6466
self.config.update_files = False
6567
print("File update not supported for cloud assets, skipping file upload", flush=True)
66-
with ThreadPoolExecutor(max_workers=self.config.amount_of_parallel_uploads) as executor:
68+
with ThreadPoolExecutor(max_workers=app_settings.parallel_asset_upload) as executor:
6769
for asset in asset_infos:
6870
if not asset.already_in_cloud or self.config.update_files:
69-
self.futures.append(executor.submit(self.upload_asset_files, asset))
71+
self.futures.append(executor.submit(self.upload_asset_files, asset, app_settings))
7072

7173
self.futures = list()
7274

7375
print("Setting tags and collections for assets", flush=True)
74-
with ThreadPoolExecutor(max_workers=self.config.amount_of_parallel_uploads) as executor:
76+
with ThreadPoolExecutor(max_workers=app_settings.parallel_creation_edit) as executor:
7577
for asset in asset_infos:
7678
self.futures.append(executor.submit(self.set_asset_decorations, asset))
7779

@@ -108,7 +110,7 @@ def create_new_version(self, asset: AssetInfo):
108110
print(f'Failed to create new version for asset: {asset.name}', flush=True)
109111
print(e, flush=True)
110112

111-
def upload_asset_files(self, asset: AssetInfo):
113+
def upload_asset_files(self, asset: AssetInfo, app_settings: AppSettings):
112114
try:
113115

114116
dataset_id = uc.assets.get_dataset_list(self.config.org_id, self.config.project_id, asset.am_id, asset.version)[0].id
@@ -118,7 +120,7 @@ def upload_asset_files(self, asset: AssetInfo):
118120

119121
print(f"Uploading files for asset: {asset.name}", flush=True)
120122
files_upload_futures = []
121-
with ThreadPoolExecutor(max_workers=200) as executor:
123+
with ThreadPoolExecutor(max_workers=app_settings.parallel_file_upload_per_asset) as executor:
122124
for file in asset.files:
123125
files_upload_futures.append(executor.submit(self.upload_file, asset, dataset_id, file))
124126

@@ -203,7 +205,8 @@ def set_asset_decorations(self, asset: AssetInfo):
203205
for metadata_field in asset.customization.metadata:
204206
asset_update.metadata[metadata_field.field_definition] = metadata_field.field_value
205207

206-
if asset.customization.description is not None:
208+
if asset.customization.description is not None and asset.customization.description != "":
209+
print(asset.customization.description)
207210
asset_update.description = asset.customization.description
208211

209212
try:

bulk_upload_cli/bulk_upload/bulk_upload_pipeline.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import unity_cloud as uc
2+
import os
23

34
from bulk_upload.config_providers import InteractiveConfigProvider, FileConfigProvider, SelectConfigProvider
4-
from bulk_upload.models import ProjectUploaderConfig, Strategy, DependencyStrategy
5+
from bulk_upload.models import ProjectUploaderConfig, Strategy, DependencyStrategy, AppSettings
56
from bulk_upload.asset_mappers import NameGroupingAssetMapper, FolderGroupingAssetMapper, UnityPackageAssetMapper, \
67
UnityProjectAssetMapper, SingleFileAssetMapper, CsvAssetMapper, CloudAssetMapper
78
from bulk_upload.assets_uploaders import AssetUploader, CloudAssetUploader
@@ -39,6 +40,10 @@ def login_with_user_account():
3940
uc.identity.user_login.login()
4041

4142
def run(self, config_file=None, select_config=False):
43+
app_settings = AppSettings()
44+
app_settings.load_from_json()
45+
self.set_environment_variables(app_settings)
46+
4247
is_headless_run = config_file is not None or select_config
4348

4449
# Step 1: Get base the configuration
@@ -69,11 +74,16 @@ def run(self, config_file=None, select_config=False):
6974

7075
# Step 7: Upload
7176
asset_uploader = self.get_asset_uploader(config)
72-
asset_uploader.upload_assets(assets, config)
77+
asset_uploader.upload_assets(assets, config, app_settings)
7378

7479
# Step 8: Post upload actions, Clean up
7580
asset_mapper.clean_up()
7681

82+
@staticmethod
83+
def set_environment_variables(app_settings: AppSettings):
84+
for key, value in app_settings.environment_variables.items():
85+
os.environ[key] = value
86+
7787
@staticmethod
7888
def get_config_provider(select_config=False, config_file=None):
7989
if select_config:

0 commit comments

Comments
 (0)