From 611d7ad39f3849abd68b40303a48b9c05b5da92b Mon Sep 17 00:00:00 2001 From: sqozz Date: Thu, 23 Feb 2023 11:29:44 +0100 Subject: [PATCH] Initial commit --- .gitignore | 1 + README.md | 0 config.ini | 12 +++ importer.py | 268 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 + 5 files changed, 283 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 config.ini create mode 100644 importer.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2fa7ce7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +config.ini diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..67b6d0b --- /dev/null +++ b/config.ini @@ -0,0 +1,12 @@ +[taiga] +url = https://taiga.example.com +username = taigauser@example.com +password = mysupersecretpassword + +project_slug = 3d-printing + +userstory_use_custom_field = True +userstory_custom_field_name = Platform link + +initial_task_status = New + diff --git a/importer.py b/importer.py new file mode 100644 index 0000000..f73dc48 --- /dev/null +++ b/importer.py @@ -0,0 +1,268 @@ +import os +import uuid +import tempfile +import requests +import configparser +import urllib.parse +from taiga import TaigaAPI + +# Config parsing +CONFIG=configparser.ConfigParser() +CONFIG.read("config.ini") + +# Static variables for scrape requests to printables.com +UUID=str(uuid.uuid4()) +ENDPOINT_URL="https://api.printables.com/graphql/" +HEADERS = { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/109.0", + "Accept": "application/json, text/plain, */*", + "Accept-Language": "en", + "Content-Type": "application/json", + "Referer": "https://www.printables.com/", + "operation": "PrintProfile", + "apollographql-client-version": "v2.46.2", + "Client-Uid": UUID, + "Origin": "https://www.printables.com", + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "same-site", + "Authorization": "", + "Connection": "keep-alive" +} + +def get_printables_print_data(model_id): + PARAMS = '{"operationName":"PrintProfile","variables":{"id":"' + str(model_id) + '''"},"query":"query PrintProfile($id: ID!) { + print(id: $id) { + ...PrintDetailFragment + __typename + } + } + + fragment PrintDetailFragment on PrintType { + id + slug + name + authorship + description + hasModel + summary + printDuration + numPieces + weight + nozzleDiameters + usedMaterial + layerHeights + materials { + id + name + __typename + } + filesCount + pdfFilePath + userGcodeCount + printer { + id + name + __typename + } + images { + ...ImageSimpleFragment + __typename + } + tags { + name + id + __typename + } + thingiverseLink + filesType + foundInUserGcodes + remixParents { + ...remixParentDetail + __typename + } + gcodes { + id + name + filePath + fileSize + filePreviewPath + __typename + } + stls { + id + name + filePath + fileSize + filePreviewPath + __typename + } + slas { + id + name + filePath + fileSize + filePreviewPath + __typename + } + ...LatestCompetitionResult + competitions { + id + name + slug + description + isOpened + __typename + } + competitionResults { + placement + competition { + id + name + slug + printsCount + openedFrom + openedTo + __typename + } + __typename + } + __typename + } + + fragment ImageSimpleFragment on PrintImageType { + id + filePath + rotation + __typename + } + + fragment remixParentDetail on PrintRemixType { + id + parentPrintId + parentPrintName + parentPrintAuthor { + id + slug + publicUsername + company + verified + handle + __typename + } + parentPrint { + id + name + slug + datePublished + images { + ...ImageSimpleFragment + __typename + } + license { + id + name + disallowRemixing + __typename + } + eduProject { + id + __typename + } + __typename + } + url + urlAuthor + urlImage + urlTitle + __typename + } + + fragment LatestCompetitionResult on PrintType { + latestCompetitionResult { + placement + competitionId + __typename + } + __typename + }"}''' + PARAMS=PARAMS.replace("\n", "\\n") + + req = requests.post(ENDPOINT_URL, headers=HEADERS, data=PARAMS) + print_data = req.json()["data"]["print"] + return print_data + +def get_modelid_from_url(url): + parsed_url = urllib.parse.urlparse(url) + path = parsed_url.path + path_components = path.split("/") + path_components = list(filter(lambda x: x != "", path_components)) #filter empty elements + if path_components[0] != "model": + raise Exception("Only direct links to models are supported") + model_slug = path_components[1] + model_id = model_slug.split("-")[0] + return model_id + +link = input("Please paste link: ") +model_id = get_modelid_from_url(link) +print_data = get_printables_print_data(model_id) +api = TaigaAPI(host=CONFIG["taiga"]["url"]) +api.auth( + username=CONFIG["taiga"]["username"], + password=CONFIG["taiga"]["password"] +) +proj = api.projects.get_by_slug(CONFIG["taiga"]["project_slug"]) + +# Create userstory for printable +story = proj.add_user_story( + print_data["name"], + description = print_data["description"], + tags = list(map(lambda x: x.get("name"), print_data["tags"]))) + +# Set custom field for platform link if enabled and configured +if bool(CONFIG["taiga"]["userstory_use_custom_field"]): + us_attributes = list(map(lambda x: {"id": x.id, "name": x.name}, api.user_story_attributes.list())) + attribute_id = list(filter(lambda x: CONFIG["taiga"]["userstory_custom_field_name"] in x["name"], us_attributes))[0]["id"] + story.set_attribute(attribute_id, link) + +# Find id for desired status of newly tasks +task_statuses = list(map(lambda x: {"id": x.id, "name": x.name}, api.task_statuses.list())) +task_status_id = list(filter(lambda x: CONFIG["taiga"]["initial_task_status"] == x["name"], task_statuses))[0]["id"] + + +tmpdir = tempfile.TemporaryDirectory() #workdir for downloads + +imagepath = print_data["images"][0]["filePath"] +imageurl = urllib.parse.urljoin("https://media.printables.com", imagepath) +local_file = os.path.join(tmpdir.name, os.path.basename(imagepath)) +print("Downloading first image of printable to {}…".format(local_file), end="") +with requests.get(imageurl, stream=True) as r: + r.raise_for_status() + with open(local_file, "wb") as f: + for chunk in r.iter_content(chunk_size=8192): + f.write(chunk) +print(" done.") + +story.attach(local_file) + +stls = list(filter(lambda x: x["filePath"].endswith(".stl"), print_data["stls"])) +stl_files = list(map(lambda x: {"name": x["name"], "filePath": x["filePath"]},stls)) +for stl_file in stl_files: + stlpath = stl_file["filePath"] + filename = os.path.basename(stlpath) + print("Creating task for file {}…".format(filename), end="") + + task = story.add_task(filename, task_status_id) + + stlurl = urllib.parse.urljoin("https://files.printables.com", stlpath) + local_file = os.path.join(tmpdir.name, filename) + with requests.get(stlurl, stream=True) as r: + r.raise_for_status() + with open(local_file, "wb") as f: + for chunk in r.iter_content(chunk_size=8192): + f.write(chunk) + task.attach(local_file) + print(" done.") + +newstory_url= urllib.parse.urljoin(CONFIG["taiga"]["url"], os.path.join("project", CONFIG["taiga"]["project_slug"], "us", str(story.ref))) +print("New story created at: {}".format(newstory_url)) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a9c7fbb --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +python-taiga +requests