From fb49fdf42b7d0e09644b9a812f035829c11b8f7c Mon Sep 17 00:00:00 2001 From: Gheorghita Hurmuz Date: Wed, 5 Feb 2025 12:54:50 +0200 Subject: [PATCH] chore: add cli to publishable sdk --- pyproject.toml | 13 +- src/uipath_cli/.python-version | 1 - src/uipath_cli/cli.py | 17 +++ src/uipath_cli/{cli-init.py => cli_init.py} | 70 ++++++---- src/uipath_cli/{cli-pack.py => cli_pack.py} | 142 ++++++++++++-------- src/uipath_cli/dummy-main.py | 2 + src/uipath_cli/dummy_sdk.py | 2 + src/uipath_cli/parse-ast.py | 52 +++++-- uv.lock | 4 +- 9 files changed, 207 insertions(+), 96 deletions(-) delete mode 100644 src/uipath_cli/.python-version create mode 100644 src/uipath_cli/cli.py rename src/uipath_cli/{cli-init.py => cli_init.py} (51%) rename src/uipath_cli/{cli-pack.py => cli_pack.py} (66%) diff --git a/pyproject.toml b/pyproject.toml index 7d7db5b4a..d1fd4dc2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,10 @@ [project] name = "uipath_sdk" -version = "0.0.3" -description = "UiPath Client for the UiPath API" +version = "0.0.4" +description = "UiPath SDK" readme = "README.md" requires-python = ">=3.13" dependencies = [ - "click>=8.1.8", "httpx>=0.28.1", "python-dotenv>=1.0.1", ] @@ -20,6 +19,9 @@ classifiers = [ Homepage = "https://uipath.com" Repository = "https://github.com/UiPath/platform-sdk.git" +[project.scripts] +uipath = "uipath_cli.cli:cli" + [build-system] requires = ["hatchling"] @@ -28,6 +30,7 @@ build-backend = "hatchling.build" [dependency-groups] dev = [ + "click>=8.1.8", "bandit>=1.8.2", "mypy>=1.14.1", "ruff>=0.9.4", @@ -40,8 +43,10 @@ indent-width = 4 [tool.ruff.lint] -select = ["E", "F", "B", "I"] +select = ["E", "F", "B", "I"] +[tool.ruff.lint.per-file-ignores] +"*" = ["E501"] [tool.ruff.format] quote-style = "double" diff --git a/src/uipath_cli/.python-version b/src/uipath_cli/.python-version deleted file mode 100644 index e4fba2183..000000000 --- a/src/uipath_cli/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.12 diff --git a/src/uipath_cli/cli.py b/src/uipath_cli/cli.py new file mode 100644 index 000000000..0131e0c3e --- /dev/null +++ b/src/uipath_cli/cli.py @@ -0,0 +1,17 @@ +# type: ignore +import click + +from .cli_init import cli_init as init +from .cli_pack import cli_pack as pack + + +@click.group() +def cli() -> None: + pass + + +cli.add_command(init) +cli.add_command(pack) + +if __name__ == "__main__": + cli() diff --git a/src/uipath_cli/cli-init.py b/src/uipath_cli/cli_init.py similarity index 51% rename from src/uipath_cli/cli-init.py rename to src/uipath_cli/cli_init.py index 6233503ed..4b49107bf 100644 --- a/src/uipath_cli/cli-init.py +++ b/src/uipath_cli/cli_init.py @@ -1,7 +1,10 @@ -import click +# type: ignore +import json import os import shutil -import json + +import click + def get_final_path(target_directory, project_name): final_path = os.path.abspath(target_directory) @@ -15,49 +18,68 @@ def get_final_path(target_directory, project_name): return final_path + def generateInitFile(target_directory, project_name): final_path = get_final_path(target_directory, project_name) - - template_path = os.path.join(os.path.dirname(__file__), 'templates/main.py.template') - target_path = os.path.join(final_path, 'main.py') + template_path = os.path.join( + os.path.dirname(__file__), "templates/main.py.template" + ) + target_path = os.path.join(final_path, "main.py") shutil.copyfile(template_path, target_path) + def generateRequirementsFile(target_directory, project_name): final_path = get_final_path(target_directory, project_name) - - requirements_path = os.path.join(final_path, 'requirements.txt') - with open(requirements_path, 'w') as f: + + requirements_path = os.path.join(final_path, "requirements.txt") + with open(requirements_path, "w") as f: f.write("uipath==1.0.1\n") + def generateConfigFile(target_directory, project_name, description, type): final_path = get_final_path(target_directory, project_name) - - config_path = os.path.join(final_path, 'config.json') + + config_path = os.path.join(final_path, "config.json") config_data = { "project_name": project_name, "description": description, "type": type, } - with open(config_path, 'w') as config_file: + with open(config_path, "w") as config_file: json.dump(config_data, config_file, indent=4) + @click.command() -@click.option("--name", prompt="Name", default="my-first-project", help="Name of your project") -@click.option("--type", prompt="Type (process/agent)", help="Whether the project is a process or an agent") -@click.option("--description", prompt="Description", default="", help="Description for your project") -@click.option("--directory", prompt="Target Directory", default="./proj", help="Target directory for your project") -def hello(name, description, directory, type): - click.echo(f"Initializing project {name} with description {description} in directory {directory}") +@click.option( + "--name", prompt="Name", default="my-first-project", help="Name of your project" +) +@click.option( + "--type", + prompt="Type (process/agent)", + help="Whether the project is a process or an agent", +) +@click.option( + "--description", + prompt="Description", + default="", + help="Description for your project", +) +@click.option( + "--directory", + prompt="Target Directory", + default="./proj", + help="Target directory for your project", +) +def cli_init(name, description, directory, type): + click.echo( + f"Initializing project {name} with description {description} in directory {directory}" + ) generateInitFile(directory, name) generateRequirementsFile(directory, name) generateConfigFile(directory, name, description, type) - click.echo(f"Make sure to run `pip install -r {os.path.join(directory, 'requirements.txt')}` to install dependencies") - -if __name__ == '__main__': - hello() - - - + click.echo( + f"Make sure to run `pip install -r {os.path.join(directory, 'requirements.txt')}` to install dependencies" + ) diff --git a/src/uipath_cli/cli-pack.py b/src/uipath_cli/cli_pack.py similarity index 66% rename from src/uipath_cli/cli-pack.py rename to src/uipath_cli/cli_pack.py index bd3315603..4f7167714 100644 --- a/src/uipath_cli/cli-pack.py +++ b/src/uipath_cli/cli_pack.py @@ -1,35 +1,39 @@ -from string import Template -import zipfile -import click -import os -import shutil +# type: ignore import json +import os import uuid +import zipfile +from string import Template + +import click schema = "https://cloud.uipath.com/draft/2024-12/operate" mainFileEntrypoint = "content/main.py" + def validate_config_structure(config_data): required_fields = ["project_name", "description", "type"] for field in required_fields: if field not in config_data: raise Exception(f"config.json is missing the required field: {field}") + def check_config_file(directory): - config_path = os.path.join(directory, 'config.json') + config_path = os.path.join(directory, "config.json") if not os.path.isfile(config_path): raise Exception("config.json file does not exist in the target directory") - with open(config_path, 'r') as config_file: + with open(config_path, "r") as config_file: config_data = json.load(config_file) validate_config_structure(config_data) return config_data + def generate_operate_file(type): project_id = str(uuid.uuid4()) - + operate_json_data = { "$schema": schema, "projectId": project_id, @@ -37,14 +41,12 @@ def generate_operate_file(type): "contentType": type, "targetFramework": "Portable", "targetRuntime": "python", - "runtimeOptions": { - "requiresUserInteraction": False, - "isAttended": False - } + "runtimeOptions": {"requiresUserInteraction": False, "isAttended": False}, } return operate_json_data + def generate_entrypoints_file(): unique_id = str(uuid.uuid4()) entrypoint_json_data = { @@ -55,30 +57,30 @@ def generate_entrypoints_file(): "filePath": mainFileEntrypoint, "uniqueId": unique_id, "type": "codeagent", - "input": { - }, - "output": { - } + "input": {}, + "output": {}, } - ] + ], } return entrypoint_json_data + def generate_bindings_content(): - bindings_content = { - "version":"2.0", - "resources":[ ] - } + bindings_content = {"version": "2.0", "resources": []} return bindings_content + def generate_content_types_content(): - templates_path = os.path.join(os.path.dirname(__file__), 'templates', '[Content_Types].xml.template') - with open(templates_path, 'r') as file: + templates_path = os.path.join( + os.path.dirname(__file__), "templates", "[Content_Types].xml.template" + ) + with open(templates_path, "r") as file: content_types_content = file.read() return content_types_content + def generate_nuspec_content(projectName, packageVersion, description): authors = "UiPath" variables = { @@ -87,14 +89,19 @@ def generate_nuspec_content(projectName, packageVersion, description): "description": description, "authors": authors, } - templates_path = os.path.join(os.path.dirname(__file__), 'templates', 'package.nuspec.template') - with open(templates_path, 'r') as f: + templates_path = os.path.join( + os.path.dirname(__file__), "templates", "package.nuspec.template" + ) + with open(templates_path, "r") as f: content = f.read() return Template(content).substitute(variables) -def generate_rels_content(nuspecPath, psmdcpPath): + +def generate_rels_content(nuspecPath, psmdcpPath): # /package/services/metadata/core-properties/254324ccede240e093a925f0231429a0.psmdcp - templates_path = os.path.join(os.path.dirname(__file__), 'templates', '.rels.template') + templates_path = os.path.join( + os.path.dirname(__file__), "templates", ".rels.template" + ) nuspecId = "R" + str(uuid.uuid4()).replace("-", "")[:16] psmdcpId = "R" + str(uuid.uuid4()).replace("-", "")[:16] variables = { @@ -103,14 +110,17 @@ def generate_rels_content(nuspecPath, psmdcpPath): "psmdcpPath": psmdcpPath, "psmdcpId": psmdcpId, } - with open(templates_path, 'r') as f: + with open(templates_path, "r") as f: content = f.read() return Template(content).substitute(variables) + def generate_psmdcp_content(projectName, version, description): - templates_path = os.path.join(os.path.dirname(__file__), 'templates', '.psmdcp.template') + templates_path = os.path.join( + os.path.dirname(__file__), "templates", ".psmdcp.template" + ) creator = "UiPath" - + token = str(uuid.uuid4()).replace("-", "")[:32] random_file_name = f"{uuid.uuid4().hex[:16]}.psmdcp" variables = { @@ -120,11 +130,12 @@ def generate_psmdcp_content(projectName, version, description): "projectName": projectName, "publicKeyToken": token, } - with open(templates_path, 'r') as f: + with open(templates_path, "r") as f: content = f.read() - + return [random_file_name, Template(content).substitute(variables)] + def generate_package_desriptor_content(): package_descriptor_content = { "$schema": "https://cloud.uipath.com/draft/2024-12/package-descriptor", @@ -132,56 +143,81 @@ def generate_package_desriptor_content(): "operate.json": "content/operate.json", "entry-points.json": "content/entry-points.json", "bindings.json": "content/bindings_v2.json", - "main.py": "content/main.py" - } + "main.py": "content/main.py", + }, } return package_descriptor_content + def get_user_script(directory): - main_py_path = os.path.join(directory, 'main.py') + main_py_path = os.path.join(directory, "main.py") if not os.path.isfile(main_py_path): raise Exception("main.py file does not exist in the content directory") - with open(main_py_path, 'r') as main_py_file: + with open(main_py_path, "r") as main_py_file: main_py_content = main_py_file.read() return main_py_content + def pack(projectName, description, type, version, directory): operate_file = generate_operate_file(type) entrypoints_file = generate_entrypoints_file() bindings_content = generate_bindings_content() content_types_content = generate_content_types_content() - [psmdcp_file_name, psmdcp_content] = generate_psmdcp_content(projectName, version, description) + [psmdcp_file_name, psmdcp_content] = generate_psmdcp_content( + projectName, version, description + ) nuspec_content = generate_nuspec_content(projectName, version, description) - rels_content = generate_rels_content(f"/{projectName}.nuspec", f"/package/services/metadata/core-properties/{psmdcp_file_name}") + rels_content = generate_rels_content( + f"/{projectName}.nuspec", + f"/package/services/metadata/core-properties/{psmdcp_file_name}", + ) package_descriptor_content = generate_package_desriptor_content() main_py_content = get_user_script(directory) with zipfile.ZipFile(f"{projectName}:{version}.nupkg", "w") as z: - z.writestr(f"./package/services/metadata/core-properties/{psmdcp_file_name}", psmdcp_content) + z.writestr( + f"./package/services/metadata/core-properties/{psmdcp_file_name}", + psmdcp_content, + ) z.writestr("[Content_Types].xml", content_types_content) z.writestr("./content/project.json", "") - z.writestr("./content/package-descriptor.json", json.dumps(package_descriptor_content, indent=4)) + z.writestr( + "./content/package-descriptor.json", + json.dumps(package_descriptor_content, indent=4), + ) z.writestr("./content/operate.json", json.dumps(operate_file, indent=4)) - z.writestr("./content/entry-points.json", json.dumps(entrypoints_file, indent=4)) + z.writestr( + "./content/entry-points.json", json.dumps(entrypoints_file, indent=4) + ) z.writestr("./content/bindings_v2.json", json.dumps(bindings_content, indent=4)) - + z.writestr(f"{projectName}.nuspec", nuspec_content) z.writestr("./_rels/.rels", rels_content) z.writestr("./content/main.py", main_py_content) + @click.command() -@click.option("--directory", prompt="Target Directory", default=".", help="The directory of your project") -@click.option("--version", prompt="Version", default="1.0.0", help="Version of this project") -def hello(directory, version): +@click.option( + "--directory", + prompt="Target Directory", + default=".", + help="The directory of your project", +) +@click.option( + "--version", prompt="Version", default="1.0.0", help="Version of this project" +) +def cli_pack(directory, version): config = check_config_file(directory) - click.echo(f"Packaging project {config['project_name']}:{version} description {config['description']} and type {config['type']}") - pack(config['project_name'], config['description'], config['type'], version, directory) - -if __name__ == '__main__': - hello() - - - + click.echo( + f"Packaging project {config['project_name']}:{version} description {config['description']} and type {config['type']}" + ) + pack( + config["project_name"], + config["description"], + config["type"], + version, + directory, + ) diff --git a/src/uipath_cli/dummy-main.py b/src/uipath_cli/dummy-main.py index 0f19f09aa..d957e1379 100644 --- a/src/uipath_cli/dummy-main.py +++ b/src/uipath_cli/dummy-main.py @@ -1,3 +1,5 @@ +# type: ignore + import dummy_sdk as sdk sdk.AssetManager.setAsset("asset1", "value1") diff --git a/src/uipath_cli/dummy_sdk.py b/src/uipath_cli/dummy_sdk.py index 8959e888c..47f64baa1 100644 --- a/src/uipath_cli/dummy_sdk.py +++ b/src/uipath_cli/dummy_sdk.py @@ -1,3 +1,4 @@ +# type: ignore class AssetManager: def __init__(self): self.assets = {} @@ -11,4 +12,5 @@ def retrieveAsset(self, asset_id): def setAsset(self, asset_id, asset_value): self.assets[asset_id] = asset_value + AssetManager = AssetManager() diff --git a/src/uipath_cli/parse-ast.py b/src/uipath_cli/parse-ast.py index 07516d9f4..e6c217385 100644 --- a/src/uipath_cli/parse-ast.py +++ b/src/uipath_cli/parse-ast.py @@ -1,9 +1,11 @@ +# type: ignore + import ast -import sys -f = open('dummy-main.py', 'r') +f = open("dummy-main.py", "r") source = f.read() + def infer_type(node, env): if isinstance(node, ast.Constant): return type(node.value).__name__ @@ -21,24 +23,31 @@ def infer_type(node, env): return "dict" return "unknown" + class AssignmentCollector(ast.NodeVisitor): def __init__(self): self.env = {} + def visit_Assign(self, node): t = infer_type(node.value, self.env) for target in node.targets: if isinstance(target, ast.Name): if not isinstance(node.value, ast.Constant): - self.env[target.id] = [ast.get_source_segment(source, node.value), t] + self.env[target.id] = [ + ast.get_source_segment(source, node.value), + t, + ] else: self.env[target.id] = [node.value.value, t] self.generic_visit(node) + def visit_AnnAssign(self, node): if isinstance(node.target, ast.Name): t = infer_type(node.value, self.env) if node.value else "unknown" self.env[node.target.id] = [node.value.value, t] self.generic_visit(node) + class AssetManagerCallVisitor(ast.NodeVisitor): def __init__(self, env): self.env = env @@ -51,23 +60,41 @@ def visit_Call(self, node): method = node.func.attr # Assume calls are made as AssetManager.setAsset(...) or .retrieveAsset(...) # node.func.value could be alias, need to have a map for alias imports - attrId = node.func.value.attr if isinstance(node.func.value, ast.Attribute) else node.func.value.id - func = node.func.value if isinstance(node.func.value, ast.Name) else node.func.value.value + attrId = ( + node.func.value.attr + if isinstance(node.func.value, ast.Attribute) + else node.func.value.id + ) + func = ( + node.func.value + if isinstance(node.func.value, ast.Name) + else node.func.value.value + ) if isinstance(func, ast.Name) and attrId == "AssetManager": if method == "setAsset" and len(node.args) >= 2: name_node = node.args[0] value_node = node.args[1] - if (not isinstance(name_node, ast.Constant) and not isinstance(name_node, ast.Name)): - self.set_mapping['expression'] = ast.get_source_segment(source, name_node) + if not isinstance(name_node, ast.Constant) and not isinstance( + name_node, ast.Name + ): + self.set_mapping["expression"] = ast.get_source_segment( + source, name_node + ) else: - asset_name = ([name_node.value] if isinstance(name_node, ast.Constant) - else infer_type(name_node, self.env)) + asset_name = ( + [name_node.value] + if isinstance(name_node, ast.Constant) + else infer_type(name_node, self.env) + ) value_type = infer_type(value_node, self.env) self.set_mapping[asset_name[0]] = value_type elif method == "retrieveAsset" and len(node.args) >= 1: name_node = node.args[0] - asset_name = (name_node.value if isinstance(name_node, ast.Constant) - else infer_type(name_node, self.env)) + asset_name = ( + name_node.value + if isinstance(name_node, ast.Constant) + else infer_type(name_node, self.env) + ) default_type = "optional" for kw in node.keywords: if kw.arg == "default": @@ -76,6 +103,7 @@ def visit_Call(self, node): self.retrieve_mapping[asset_name] = default_type self.generic_visit(node) + if __name__ == "__main__": tree = ast.parse(source, filename="dummy-main.py") collector = AssignmentCollector() @@ -90,7 +118,7 @@ def __init__(self): def visit_ImportFrom(self, node): module = node.module if node.module else "" for alias in node.names: - if module == 'sdk' and alias.name == 'AssetManager': + if module == "sdk" and alias.name == "AssetManager": self.imports[alias.asname] = f"{module}.{alias.name}" self.generic_visit(node) diff --git a/uv.lock b/uv.lock index a596d7740..7adcdb15d 100644 --- a/uv.lock +++ b/uv.lock @@ -271,7 +271,6 @@ name = "uipath-sdk" version = "0.0.3" source = { editable = "." } dependencies = [ - { name = "click" }, { name = "httpx" }, { name = "python-dotenv" }, ] @@ -279,13 +278,13 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "bandit" }, + { name = "click" }, { name = "mypy" }, { name = "ruff" }, ] [package.metadata] requires-dist = [ - { name = "click", specifier = ">=8.1.8" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "python-dotenv", specifier = ">=1.0.1" }, ] @@ -293,6 +292,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "bandit", specifier = ">=1.8.2" }, + { name = "click", specifier = ">=8.1.8" }, { name = "mypy", specifier = ">=1.14.1" }, { name = "ruff", specifier = ">=0.9.4" }, ]