Skip to content

Commit

Permalink
Exporter Typing with mypy [AARD-1765] (#1093)
Browse files Browse the repository at this point in the history
  • Loading branch information
HunterBarclay authored Aug 21, 2024
2 parents fa453d7 + cfb58e0 commit ca0f6d0
Show file tree
Hide file tree
Showing 47 changed files with 2,592 additions and 577 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/FusionTyping.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Fusion - mypy Typing Validation

on:
workflow_dispatch: {}
push:
branches: [ prod, dev ]
pull_request:
branches: [ prod, dev ]

jobs:
mypy:
name: Run mypy
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./exporter/SynthesisFusionAddin
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install -r requirements-mypy.txt
- run: mypy
5 changes: 3 additions & 2 deletions exporter/SynthesisFusionAddin/Synthesis.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import sys
from typing import Any

import adsk.core

Expand Down Expand Up @@ -43,7 +44,7 @@


@logFailure
def run(_):
def run(_context: dict[str, Any]) -> None:
"""## Entry point to application from Fusion.
Arguments:
Expand All @@ -63,7 +64,7 @@ def run(_):


@logFailure
def stop(_):
def stop(_context: dict[str, Any]) -> None:
"""## Fusion exit point - deconstructs buttons and handlers
Arguments:
Expand Down
14 changes: 14 additions & 0 deletions exporter/SynthesisFusionAddin/mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[mypy]
files = Synthesis.py, src
warn_unused_configs = True
check_untyped_defs = True
warn_unreachable = True
warn_redundant_casts = True
warn_unused_ignores = True
warn_no_return = True
warn_return_any = True
strict = True
ignore_missing_imports = True
follow_imports = skip
disallow_subclassing_any = False
disable_error_code = no-untyped-call
2 changes: 1 addition & 1 deletion exporter/SynthesisFusionAddin/proto/build.bat
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
md .\proto_out\
@RD /S /Q "./proto_out/__pycache__"
@echo on
protoc -I=../../../mirabuf --python_out=./proto_out ../../../mirabuf/*.proto
protoc -I=../../../mirabuf --python_out=./proto_out --mypy_out=./proto_out ../../../mirabuf/*.proto
@echo off
2 changes: 1 addition & 1 deletion exporter/SynthesisFusionAddin/proto/build.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
rm -rf -v ./proto_out
mkdir ./proto_out
git submodule update --init --recursive
protoc -I=../../../mirabuf --python_out=./proto_out ../../../mirabuf/*.proto
protoc -I=../../../mirabuf --python_out=./proto_out --mypy_out=./proto_out ../../../mirabuf/*.proto
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
black
isort
pyminifier
3 changes: 3 additions & 0 deletions exporter/SynthesisFusionAddin/requirements-mypy.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mypy
types-protobuf
types-requests
45 changes: 29 additions & 16 deletions exporter/SynthesisFusionAddin/src/APS/APS.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import http.client
import json
import os
import pathlib
Expand Down Expand Up @@ -52,21 +53,21 @@ def getAPSAuth() -> APSAuth | None:
return APS_AUTH


def _res_json(res):
return json.loads(res.read().decode(res.info().get_param("charset") or "utf-8"))
def _res_json(res: http.client.HTTPResponse) -> dict[str, Any]:
return dict(json.loads(res.read().decode(str(res.info().get_param("charset")) or "utf-8")))


def getCodeChallenge() -> str | None:
endpoint = "https://synthesis.autodesk.com/api/aps/challenge/"
res = urllib.request.urlopen(endpoint)
res: http.client.HTTPResponse = urllib.request.urlopen(endpoint)
data = _res_json(res)
return data["challenge"]
return str(data["challenge"])


def getAuth() -> APSAuth | None:
global APS_AUTH
if APS_AUTH is not None:
return APS_AUTH
return APS_AUTH # type: ignore[unreachable]

currTime = time.time()
if os.path.exists(auth_path):
Expand All @@ -86,7 +87,7 @@ def getAuth() -> APSAuth | None:
return APS_AUTH


def convertAuthToken(code: str):
def convertAuthToken(code: str) -> None:
global APS_AUTH
authUrl = f'https://synthesis.autodesk.com/api/aps/code/?code={code}&redirect_uri={urllib.parse.quote_plus("https://synthesis.autodesk.com/api/aps/exporter/")}'
res = urllib.request.urlopen(authUrl)
Expand All @@ -106,14 +107,14 @@ def convertAuthToken(code: str):
_ = loadUserInfo()


def removeAuth():
def removeAuth() -> None:
global APS_AUTH, APS_USER_INFO
APS_AUTH = None
APS_USER_INFO = None
pathlib.Path.unlink(pathlib.Path(auth_path))


def refreshAuthToken():
def refreshAuthToken() -> None:
global APS_AUTH
if APS_AUTH is None or APS_AUTH.refresh_token is None:
raise Exception("No refresh token found.")
Expand Down Expand Up @@ -178,6 +179,8 @@ def loadUserInfo() -> APSUserInfo | None:
removeAuth()
logger.error(f"User Info Error:\n{e.code} - {e.reason}")
gm.ui.messageBox("Please sign in again.")
finally:
return None


def getUserInfo() -> APSUserInfo | None:
Expand Down Expand Up @@ -259,20 +262,30 @@ def upload_mirabuf(project_id: str, folder_id: str, file_name: str, file_content
global APS_AUTH
if APS_AUTH is None:
gm.ui.messageBox("You must login to upload designs to APS", "USER ERROR")
return None

auth = APS_AUTH.access_token
# Get token from APS API later

new_folder_id = get_item_id(auth, project_id, folder_id, "MirabufDir", "folders")
if new_folder_id is None:
folder_id = create_folder(auth, project_id, folder_id, "MirabufDir")
created_folder_id = create_folder(auth, project_id, folder_id, "MirabufDir")
else:
folder_id = new_folder_id
(lineage_id, file_id, file_version) = get_file_id(auth, project_id, folder_id, file_name)
created_folder_id = new_folder_id

if created_folder_id is None:
return None

file_id_data = get_file_id(auth, project_id, created_folder_id, file_name)
if file_id_data is None:
return None

(lineage_id, file_id, file_version) = file_id_data

"""
Create APS Storage Location
"""
object_id = create_storage_location(auth, project_id, folder_id, file_name)
object_id = create_storage_location(auth, project_id, created_folder_id, file_name)
if object_id is None:
gm.ui.messageBox("UPLOAD ERROR", "Object id is none; check create storage location")
return None
Expand All @@ -297,10 +310,10 @@ def upload_mirabuf(project_id: str, folder_id: str, file_name: str, file_content
return None
if file_id != "":
update_file_version(
auth, project_id, folder_id, lineage_id, file_id, file_name, file_contents, file_version, object_id
auth, project_id, created_folder_id, lineage_id, file_id, file_name, file_contents, file_version, object_id
)
else:
_lineage_info = create_first_file_version(auth, str(object_id), project_id, str(folder_id), file_name)
_lineage_info = create_first_file_version(auth, str(object_id), project_id, str(created_folder_id), file_name)
return ""


Expand Down Expand Up @@ -376,7 +389,7 @@ def get_item_id(auth: str, project_id: str, parent_folder_id: str, folder_name:
return ""
for item in data:
if item["type"] == item_type and item["attributes"]["name"] == folder_name:
return item["id"]
return str(item["id"])
return None


Expand Down Expand Up @@ -500,7 +513,7 @@ def get_file_id(auth: str, project_id: str, folder_id: str, file_name: str) -> t
elif not file_res.ok:
gm.ui.messageBox(f"UPLOAD ERROR: {file_res.text}", "Failed to get file")
return None
file_json: list[dict[str, Any]] = file_res.json()
file_json: dict[str, Any] = file_res.json()
if len(file_json["data"]) == 0:
return ("", "", "")
id: str = str(file_json["data"][0]["id"])
Expand Down
12 changes: 8 additions & 4 deletions exporter/SynthesisFusionAddin/src/Dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@


@logFailure
def getInternalFusionPythonInstillationFolder() -> str:
def getInternalFusionPythonInstillationFolder() -> str | os.PathLike[str]:
# Thank you Kris Kaplan
# Find the folder location where the Autodesk python instillation keeps the 'os' standard library module.
pythonStandardLibraryModulePath = importlib.machinery.PathFinder.find_spec("os", sys.path).origin
pythonOSModulePath = importlib.machinery.PathFinder.find_spec("os", sys.path)
if pythonOSModulePath:
pythonStandardLibraryModulePath = pythonOSModulePath.origin or "ERROR"
else:
raise BaseException("Could not locate spec 'os'")

# Depending on platform, adjust to folder to where the python executable binaries are stored.
if SYSTEM == "Windows":
Expand All @@ -36,10 +40,10 @@ def getInternalFusionPythonInstillationFolder() -> str:
return folder


def executeCommand(*args: str) -> subprocess.CompletedProcess:
def executeCommand(*args: str) -> subprocess.CompletedProcess[str]:
logger.debug(f"Running Command -> {' '.join(args)}")
try:
result: subprocess.CompletedProcess = subprocess.run(
result: subprocess.CompletedProcess[str] = subprocess.run(
args, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True
)
logger.debug(f"Command Output:\n{result.stdout}")
Expand Down
81 changes: 36 additions & 45 deletions exporter/SynthesisFusionAddin/src/GlobalManager.py
Original file line number Diff line number Diff line change
@@ -1,62 +1,53 @@
""" Initializes the global variables that are set in the run method to reduce hanging commands. """

from typing import Any

import adsk.core
import adsk.fusion


class GlobalManager(object):
"""Global Manager instance"""

class __GlobalManager:
def __init__(self):
self.app = adsk.core.Application.get()

if self.app:
self.ui = self.app.userInterface

self.connected = False
""" Is unity currently connected """

self.uniqueIds = []
""" Collection of unique ID values to not overlap """

self.elements = []
""" Unique constructed buttons to delete """

self.palettes = []
""" Unique constructed palettes to delete """
class GlobalManager:
def __init__(self) -> None:
self.app = adsk.core.Application.get()

self.handlers = []
""" Object to store all event handlers to custom events like saving. """
if self.app:
self.ui = self.app.userInterface

self.tabs = []
""" Set of Tab objects to keep track of. """
self.connected = False
""" Is unity currently connected """

self.queue = []
""" This will eventually implement the Python SimpleQueue synchronized workflow
- this is the list of objects being sent
"""
self.uniqueIds: list[str] = [] # type of HButton
""" Collection of unique ID values to not overlap """

self.files = []
self.elements: list[Any] = []
""" Unique constructed buttons to delete """

def __str__(self):
return "GlobalManager"
# Transition: AARD-1765
# Will likely be removed later as this is no longer used. Avoiding adding typing for now.
self.palettes = [] # type: ignore
""" Unique constructed palettes to delete """

def clear(self):
for attr, value in self.__dict__.items():
if isinstance(value, list):
setattr(self, attr, [])
self.handlers: list[adsk.core.EventHandler] = []
""" Object to store all event handlers to custom events like saving. """

instance = None
self.tabs: list[adsk.core.ToolbarPanel] = []
""" Set of Tab objects to keep track of. """

def __new__(cls):
if not GlobalManager.instance:
GlobalManager.instance = GlobalManager.__GlobalManager()
# Transition: AARD-1765
# Will likely be removed later as this is no longer used. Avoiding adding typing for now.
self.queue = [] # type: ignore
""" This will eventually implement the Python SimpleQueue synchronized workflow
- this is the list of objects being sent
"""

return GlobalManager.instance
# Transition: AARD-1765
# Will likely be removed later as this is no longer used. Avoiding adding typing for now.
self.files = [] # type: ignore

def __getattr__(self, name):
return getattr(self.instance, name)
def __str__(self) -> str:
return "GlobalManager"

def __setattr__(self, name):
return setattr(self.instance, name)
def clear(self) -> None:
for attr, value in self.__dict__.items():
if isinstance(value, list):
setattr(self, attr, [])
Loading

0 comments on commit ca0f6d0

Please sign in to comment.