From 271f9b346900c9b994ea20af7022bb0c0da69cfa Mon Sep 17 00:00:00 2001 From: Joachim Happel Date: Tue, 28 May 2024 15:22:28 +0200 Subject: [PATCH] - recoded lib - add sync.py (synchronisize boards) --- README.md | 73 +++++++++++------ __pycache__/lib.cpython-312.pyc | Bin 5163 -> 5518 bytes backup.py | 12 ++- clone.py | 80 ++++++++++-------- lib.py | 140 +++++++++++--------------------- sync.py | 107 ++++++++++++++++++++++++ 6 files changed, 260 insertions(+), 152 deletions(-) create mode 100644 sync.py diff --git a/README.md b/README.md index 3a6b3f0..ce6fdef 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,74 @@ # Nextcloud Deck importieren/exportieren -## Installation +## Voraussetzung -1. Download https://github.com/johappel/nextcloud-import-export/archive/refs/heads/main.zip - oder auf der Komandozeile git clone https://github.com/johappel/nextcloud-import-export.git -2. Gehe in das Verzeichnis nextcloud-import-export (cd nextcloud-import-export) -3. Führe auf der Komandozeile aus:`pip install requests` -4. Kopiere die Datei "sample.config.py" nach "config.py" und trage dort die Daten zu deinen Nextcloudinstanzen ein +[Python](https://www.python.org/downloads/) muss installiert sein. Du kannst diese [Anleitung](https://kinsta.com/de/wissensdatenbank/python-installieren/) nutzen. +## Optional: Git installieren -## Verwendung -Um ein bestimmtes Deck auf eine andfere Nextcloud Instanz zu kopieren gibst du auf der Komandozeile an: +Installiere [Git](https://git-scm.com/book/de/v2/Erste-Schritte-Git-installieren): Das ermöglicht dir, die Skripte aktuell zu halten und dich an der Entwicklung zu beteiligen. -```python -python clone.py --"Name des Decks" -``` -oder -```python -python3 clone.py --"Name des Decks" +## Installation der Skripte + +1. [Download](https://github.com/johappel/nextcloud-import-export/archive/refs/heads/main.zip) der Skripte oder mit Git + `git clone https://github.com/johappel/nextcloud-import-export.git` +2. Gehe in das Verzeichnis nextcloud-import-export (cd nextcloud-import-export) +3. Führe auf der Kommandozeile aus: `pip install requests` +4. Kopiere die Datei "sample.config.py" nach "config.py" und trage dort die Daten zu deinen Nextcloud-Instanzen ein. +5. Mit diesem Befehl auf der Kommandozeile holst du die neueste Version: + `git pull` + +## Anwendungsfälle + +Um ein bestimmtes Deck auf eine andere Nextcloud-Instanz zu kopieren, gibst du auf der Kommandozeile einen der folgenden Befehle ein: + +1. Um ein Deck zu kopieren: + +```sh +python clone.py --board "Name des Decks" ``` -Um alle Decks zu kopieren, gibst du ein: +2. Um ein bestehendes Deck auf der Zielinstanz zu löschen und zu ersetzen: -```python +```sh +python sync.py --board "Name des Decks" --replace +``` + +3. Um ein bestehendes Deck auf der Zielinstanz mit den Daten, Karten und Stacks der Originalinstanz synchron zu halten: + +```sh +python sync.py --board "Name des Decks" +``` + +4. Um ein Backup aller Decks mit Datum des Backups im Titel auf der Zielinstanz zu sichern: + +```sh python backup.py ``` - Dank der großartigen Arbeit von @svbergerem: https://gist.github.com/svbergerem/5914d7f87764901aefddba125af99938 ### Funktionen des Skripts 1. **Daten von der Quellinstanz abrufen:** - - `getBoards()`: Ruft die Liste aller Boards ab. - - `getBoardDetails(boardId)`: Ruft Details eines spezifischen Boards ab. - - `getStacks(boardId)`: Ruft die Stacks eines Boards ab. - - `getStacksArchived(boardId)`: Ruft die archivierten Stacks eines Boards ab. + Gib bei den folgenden Funktionen als Parameter 'from' oder 'to' ein, je nachdem, ob du die Daten von der Quellinstanz oder der Zielinstanz abfragst: + - `getBoards(from_or_to)`: Ruft die Liste aller Boards ab. + - `getBoardDetails(boardId, from_or_to)`: Ruft Details eines spezifischen Boards ab. + - `getStacks(boardId, from_or_to)`: Ruft die Stacks eines Boards ab. + - `getStacksArchived(boardId, from_or_to)`: Ruft die archivierten Stacks eines Boards ab. 2. **Daten zur Zielinstanz übertragen:** - `createBoard(title, color)`: Erstellt ein Board. - `createLabel(title, color, boardId)`: Erstellt ein Label in einem Board. - `createStack(title, order, boardId)`: Erstellt einen Stack in einem Board. - `createCard(title, ctype, order, description, duedate, boardId, stackId)`: Erstellt eine Karte in einem Stack. - - `assignLabel(labelId, cardId, boardId, stackId)`: Weist ein Label einer Karte zu. + - `assignLabel(labelId, cardId, boardId, stackId)`: Weist einer Karte ein Label zu. - `archiveCard(card, boardId, stackId)`: Archiviert eine Karte. - - `copyCard(card, boardIdTo, stackIdTo, labelsMap)`: Kopiert eine Karte, einschließlich ihrer Labels und archiviertem Status. + - `copyCard(card, boardIdTo, stackIdTo, labelsMap)`: Kopiert eine Karte, einschließlich ihrer Labels und des archivierten Status. + - `deleteBoard(boardIdTo)`: Löscht ein Board. + - `deleteStacks(boardIdTo)`: Löscht alle Listen eines Boards. + - `deleteLabels(boardIdTo)`: Löscht alle Karten eines Boards. + +# @todo: + - Synchronisation verbessern (nur dezidierte Karten ersetzen) + - Grafisches User Interface diff --git a/__pycache__/lib.cpython-312.pyc b/__pycache__/lib.cpython-312.pyc index a9ea882f139f282aee12731cdc062f827151fba6..3d4f287104d2f4ec2c56a2c330eb2004afbb5a87 100644 GIT binary patch literal 5518 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 literal 5163 zcmd^DU2GKB6`uc@-JSjOzj2IBV$yC1UeYRYLLe@&4Y&lSElx-l6140cU}4$W-kFVT zYh10OK6pzc8>uRm61VAFT>8+b%8xuE^<`ZZWi|B!QlI+f*gWynbMEZV*f=&;-`Wd% z?(f{WbG~!FbFTju4hK0Xkv~jclUg|LU)ZS^vDVmp0F8Sb;Rv7OQeMrcc!R$w)t5Hw zlmO#OvtMteL>SlVy=I+~V4R(yIfkv2Oavkl=_?_n5c$_!N+k+Nji?|4L<1Ql0gxdQ z1gVn{$S~1CMo1WBlte(rNEBpznoq?3joVJRl1+=oOe|d}aAvNH?;I6 zXmCle8``lO`uMYOzozY3&f#u`norkT_iGIv=fm~UW?i4Zz$L_qrvu4so)}Yug&Tz= ze5sWrM&=f@vdPb08$6c0YNjc%mIYTz7wsFadc#N)gIdd~t5NUcEmyp5*sfZnxzp6V z?P|CXTNKai#%MZg8CR~E^onJt?V{yMOo=>hU=_@~Wmu?`{--aWK7Ki46612-n6fiD zvq+9+Zx>8zA2p`1x}3{i9V|?{0eIcJ>{t}+kc-35dK*NETZ?wiJ7s4r+A)`{biQ5D zPprke*V=lXsA8#jYMnn>K6z)jA|2R-$GJ=c8|XA}`z>_# zhnvHF$t{U{V8=WB9@z1FV|Vy@uuD?3A{qbzFscmDaYAFzx6lUr`zfl>jn>sVYS`&) z&Z1o~+KsD*L2Os5@jGbjU%^CJl6F+F*l>^kUd7S` zZHLo6Rg+-4nOjyihln8L3J~+n(C5inz@nv z%pkkzFy7GNsru?(P+)vqL{YJDkH4;VZ#M z)Z+abozDu6u!pk?yE*%;=ssYwfu=e0R9^lH)5wD~ShhK0NafUiSgK&I@5}y>l9b zut5Ao)^ChDZp(Jdq#oY9kM%gSEQd&F=A<~-t2 zyf7pdiuO*#0}$g{SHW!n6h=G8@oAP?V<_}t6Y{?U^TIs*te=DMC5SA<_}PE*Qswg1 z%B4&tV^;J6q7>_199kCzz(zrA!V1GC(!l1C)b}j^VD8V{YY`gv;95v{mQ9G@IH2bf zFzU-5{MixAVA+yANDLR5#RvtFT2h+`Ih+q2bzkUc6Z9o`*CgB^>)9V>2MjHG5?dG# z=sO@1vMZt5eM`5c$r0Rc|b@`1tgPgXz|EH$(Z?T z+3Qq?GgACp%yuThzhJPNHpN>b#p@2sKv{yq!sa$zQvB{^FNQMOf2y6hZIWWnc!y#x z1GgB?DuRH6^899y<75Bi!jFTo`N8sFNv($B^TXxglJ;1O%dB@SK diff --git a/backup.py b/backup.py index 634d900..d5448aa 100644 --- a/backup.py +++ b/backup.py @@ -1,4 +1,10 @@ -from .lib import * +from datetime import datetime +from lib import * + +# datetime object containing current date and time +now = datetime.now() +# dd/mm/YY H:M:S +dt_string = now.strftime("_%Y-%m-%d-%H-%M-%S") boards = getBoards() @@ -6,9 +12,9 @@ boards = getBoards() for board in boards: boardIdFrom = board['id'] # create board - createdBoard = createBoard(board['title'], board['color']) + createdBoard = createBoard(board['title']+dt_string, board['color']) boardIdTo = createdBoard['id'] - print('Created board', board['title']) + print('Created board', board['title'], dt_string) # create labels boardDetails = getBoardDetails(board['id']) diff --git a/clone.py b/clone.py index 6bb7feb..b43eb72 100644 --- a/clone.py +++ b/clone.py @@ -4,52 +4,68 @@ import lib # Argumente von der Kommandozeile einlesen parser = argparse.ArgumentParser(description='Klonen eines bestimmten Boards von einer Nextcloud-Instanz zu einer anderen.') parser.add_argument('--board', type=str, required=True, help='Der Titel des zu klonenden Boards.') +parser.add_argument('--replace', action='store_true', help='Ersetze das Board im Ziel, falls es bereits existiert.') args = parser.parse_args() # Board-Titel, den wir klonen möchten board_to_clone = args.board # Hole alle Boards von der Quellinstanz -boards = lib.getBoards() +boards_from = lib.getBoards() # Finde das gewünschte Board -board_to_clone_data = next((board for board in boards if board['title'] == board_to_clone), None) +board_to_clone_data = next((board for board in boards_from if board['title'] == board_to_clone), None) if not board_to_clone_data: print(f'Board "{board_to_clone}" nicht gefunden.') else: boardIdFrom = board_to_clone_data['id'] - # Erstelle das Board in der Zielinstanz - createdBoard = lib.createBoard(board_to_clone_data['title'], board_to_clone_data['color']) - boardIdTo = createdBoard['id'] - print(f'Board "{board_to_clone}" erstellt') + + # Überprüfe, ob das Board im Ziel bereits existiert + boards_to = lib.getBoards('to') + existing_board_to = next((board for board in boards_to if board['title'] == board_to_clone and 0 == board['deletedAt']), None) + + # Löschen wenn der parameter --replace gesetzt wurde und das Board existiert + if args.replace and existing_board_to: + # Lösche das bestehende Board im Ziel + print(f'Lösche Board: {existing_board_to["id"]}') + lib.deleteBoard(existing_board_to['id']) + print(f'Board "{board_to_clone}" im Ziel gelöscht') - # Kopiere die Labels des Boards - boardDetails = lib.getBoardDetails(boardIdFrom) - labelsMap = {} - for label in boardDetails['labels']: - createdLabel = lib.createLabel(label['title'], label['color'], boardIdTo) - labelsMap[label['id']] = createdLabel['id'] + # Erstelle das Board in der Zielinstanz, wenn es nicht existiert oder ersetzt werden soll + if not existing_board_to or args.replace: + createdBoard = lib.createBoard(board_to_clone_data['title'], board_to_clone_data['color']) + boardIdTo = createdBoard['id'] + print(f'Board "{board_to_clone}" erstellt') - # Kopiere die Stacks und Karten des Boards - stacks = lib.getStacks(boardIdFrom) - stacksMap = {} - for stack in stacks: - createdStack = lib.createStack(stack['title'], stack['order'], boardIdTo) - stackIdTo = createdStack['id'] - stacksMap[stack['id']] = stackIdTo - print(f' Stapel "{stack['title']}" erstellt') + # Kopiere die Labels des Boards + boardDetails = lib.getBoardDetails(boardIdFrom) + labelsMap = {} + for label in boardDetails['labels']: + createdLabel = lib.createLabel(label['title'], label['color'], boardIdTo) + labelsMap[label['id']] = createdLabel['id'] - if 'cards' in stack: - for card in stack['cards']: - lib.copyCard(card, boardIdTo, stackIdTo, labelsMap) - print(f' {len(stack["cards"])} Karten erstellt') + # Kopiere die Stacks und Karten des Boards + stacks = lib.getStacks(boardIdFrom) + stacksMap = {} + for stack in stacks: + createdStack = lib.createStack(stack['title'], stack['order'], boardIdTo) + stackIdTo = createdStack['id'] + stacksMap[stack['id']] = stackIdTo + print(f' Stapel "{stack["title"]}" erstellt') - # Kopiere die archivierten Stacks und Karten des Boards - stacks = lib.getStacksArchived(boardIdFrom) - for stack in stacks: - if 'cards' in stack: - print(f' Stack "{stack['title']}"') - for card in stack['cards']: - lib.copyCard(card, boardIdTo, stacksMap[stack['id']], labelsMap) - print(f' {len(stack["cards"])} archivierte Karten erstellt') + if 'cards' in stack: + for card in stack['cards']: + lib.copyCard(card, boardIdTo, stackIdTo, labelsMap) + print(f' {len(stack["cards"])} Karten erstellt') + + # Kopiere die archivierten Stacks und Karten des Boards + stacks = lib.getStacksArchived(boardIdFrom) + for stack in stacks: + if 'cards' in stack: + print(f' Stack "{stack["title"]}"') + for card in stack['cards']: + lib.copyCard(card, boardIdTo, stacksMap[stack['id']], labelsMap) + print(f' {len(stack["cards"])} archivierte Karten erstellt') + else: + print(f'Board "{board_to_clone}" existiert bereits und wird nicht ersetzt (verwenden Sie --replace, um zu ersetzen).') diff --git a/lib.py b/lib.py index 41027d7..68f654c 100644 --- a/lib.py +++ b/lib.py @@ -1,9 +1,6 @@ -# thanks to the awesome work of @svbergerem -# -> svbergerem/nextcloud-deck-export-import.py -# https://gist.github.com/svbergerem/5914d7f87764901aefddba125af99938 - import requests import config +import base64 urlFrom = config.urlFrom authFrom = config.authFrom @@ -11,123 +8,66 @@ authFrom = config.authFrom urlTo = config.urlTo authTo = config.authTo - headers = {'OCS-APIRequest': 'true', 'Content-Type': 'application/json'} -def getBoards(): - response = requests.get( - f'{urlFrom}/index.php/apps/deck/api/v1.0/boards', - auth=authFrom, - headers=headers) +def make_request(method, endpoint, from_to='from', json=None): + if from_to == 'from': + url = urlFrom + auth = authFrom + else: # from_to == 'to' + url = urlTo + auth = authTo + + response = requests.request(method, f'{url}{endpoint}', auth=auth, headers=headers, json=json) response.raise_for_status() return response.json() -def getBoardDetails(boardId): - response = requests.get( - f'{urlFrom}/index.php/apps/deck/api/v1.0/boards/{boardId}', - auth=authFrom, - headers=headers) - response.raise_for_status() - return response.json() +def getBoards(from_to='from'): + boards = make_request('GET', '/index.php/apps/deck/api/v1.0/boards', from_to) + return [board for board in boards if 0 == board['deletedAt']] -def getStacks(boardId): - response = requests.get( - f'{urlFrom}/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks', - auth=authFrom, - headers=headers) - response.raise_for_status() - return response.json() +def getBoardDetails(boardId, from_to='from'): + return make_request('GET', f'/index.php/apps/deck/api/v1.0/boards/{boardId}', from_to) -def getStacksArchived(boardId): - response = requests.get( - f'{urlFrom}/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks/archived', - auth=authFrom, - headers=headers) - response.raise_for_status() - return response.json() +def getStacks(boardId, from_to='from'): + return make_request('GET', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks', from_to) + +def getStacksArchived(boardId, from_to='from'): + return make_request('GET', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks/archived', from_to) def createBoard(title, color): - response = requests.post( - f'{urlTo}/index.php/apps/deck/api/v1.0/boards', - auth=authTo, - json={ - 'title': title, - 'color': color - }, - headers=headers) - response.raise_for_status() - board = response.json() + board = make_request('POST', '/index.php/apps/deck/api/v1.0/boards', 'to', json={'title': title, 'color': color}) boardId = board['id'] # remove all default labels for label in board['labels']: labelId = label['id'] - response = requests.delete( - f'{urlTo}/index.php/apps/deck/api/v1.0/boards/{boardId}/labels/{labelId}', - auth=authTo, - headers=headers) - response.raise_for_status() + make_request('DELETE', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/labels/{labelId}', 'to') return board def createLabel(title, color, boardId): - response = requests.post( - f'{urlTo}/index.php/apps/deck/api/v1.0/boards/{boardId}/labels', - auth=authTo, - json={ - 'title': title, - 'color': color - }, - headers=headers) - response.raise_for_status() - return response.json() + return make_request('POST', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/labels', 'to', json={'title': title, 'color': color}) def createStack(title, order, boardId): - response = requests.post( - f'{urlTo}/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks', - auth=authTo, - json={ - 'title': title, - 'order': order - }, - headers=headers) - response.raise_for_status() - return response.json() + return make_request('POST', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks', 'to', json={'title': title, 'order': order}) def createCard(title, ctype, order, description, duedate, boardId, stackId): - response = requests.post( - f'{urlTo}/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks/{stackId}/cards', - auth=authTo, - json={ - 'title': title, - 'type': ctype, - 'order': order, - 'description': description, - 'duedate': duedate - }, - headers=headers) - response.raise_for_status() - return response.json() + try: + return make_request('POST', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks/{stackId}/cards', 'to', + json={'title': title, 'type': ctype, 'order': order, 'description': description, 'duedate': duedate}) + except requests.exceptions.HTTPError as e: + print(f"Error creating card: {e}") + print(f"Response: {e.response.text}") + raise def assignLabel(labelId, cardId, boardId, stackId): - response = requests.put( - f'{urlTo}/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/assignLabel', - auth=authTo, - json={ - 'labelId': labelId - }, - headers=headers) - response.raise_for_status() + make_request('PUT', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}/assignLabel', 'to', json={'labelId': labelId}) def archiveCard(card, boardId, stackId): - cardId = card['id'] card['archived'] = True - response = requests.put( - f'{urlTo}/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{cardId}', - auth=authTo, - json=card, - headers=headers) - response.raise_for_status() + make_request('PUT', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks/{stackId}/cards/{card["id"]}', 'to', json=card) def copyCard(card, boardIdTo, stackIdTo, labelsMap): + print(f"Copying card '{card['title']}' to board {boardIdTo}, stack {stackIdTo}") createdCard = createCard( card['title'], card['type'], @@ -146,4 +86,16 @@ def copyCard(card, boardIdTo, stackIdTo, labelsMap): if card['archived']: archiveCard(createdCard, boardIdTo, stackIdTo) +# Löschfunktionen auf der Zielinstanz +def deleteBoard(boardId): + make_request('DELETE', f'/index.php/apps/deck/api/v1.0/boards/{boardId}', 'to') +def deleteStacks(boardId): + stacks = getStacks(boardId, 'to') + for stack in stacks: + make_request('DELETE', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/stacks/{stack["id"]}', 'to') + +def deleteLabels(boardId): + boardDetails = getBoardDetails(boardId, 'to') + for label in boardDetails['labels']: + make_request('DELETE', f'/index.php/apps/deck/api/v1.0/boards/{boardId}/labels/{label["id"]}', 'to') diff --git a/sync.py b/sync.py new file mode 100644 index 0000000..35d9cdd --- /dev/null +++ b/sync.py @@ -0,0 +1,107 @@ +import argparse +import requests +import lib + +# Argumente von der Kommandozeile einlesen +parser = argparse.ArgumentParser(description='Clone or sync a specific board from one Nextcloud instance to another.') +parser.add_argument('--board', type=str, required=True, help='The title of the board to clone or sync.') +args = parser.parse_args() + +# Board-Titel, den wir klonen oder synchronisieren möchten +board_to_clone = args.board + +# Hole alle Boards von der Quellinstanz +source_boards = lib.getBoards() + +# Finde das gewünschte Board in der Quellinstanz +board_to_clone_data = next((board for board in source_boards if board['title'] == board_to_clone), None) + +if not board_to_clone_data: + print(f'Board "{board_to_clone}" nicht gefunden.') + exit() + +boardIdFrom = board_to_clone_data['id'] + +# Hole alle Boards von der Zielinstanz +target_boards = lib.getBoards('to') + +# Überprüfen, ob das Board in der Zielinstanz existiert +target_board_data = next((board for board in target_boards if board['title'] == board_to_clone), None) + +if target_board_data: + boardIdTo = target_board_data['id'] + print(f'Board "{board_to_clone}" already exists. Syncing...') +else: + # Erstelle das Board in der Zielinstanz + createdBoard = lib.createBoard(board_to_clone_data['title'], board_to_clone_data['color']) + boardIdTo = createdBoard['id'] + print(f'Created board "{board_to_clone}"') + +# Kopiere oder synchronisiere die Labels des Boards +boardDetails = lib.getBoardDetails(boardIdFrom) +labelsMap = {} +target_board_details = lib.getBoardDetails(boardIdTo,'to') + +# Existierende Labels in der Zielinstanz sammeln +existing_labels = {label['title']: label['id'] for label in target_board_details['labels']} + +for label in boardDetails['labels']: + if label['title'] in existing_labels: + labelsMap[label['id']] = existing_labels[label['title']] + else: + createdLabel = lib.createLabel(label['title'], label['color'], boardIdTo) + labelsMap[label['id']] = createdLabel['id'] + +# Kopiere oder synchronisiere die Stacks und Karten des Boards +stacks = lib.getStacks(boardIdFrom) +target_stacks = lib.getStacks(boardIdTo,'to') +stacksMap = {} + +# Existierende Stacks in der Zielinstanz sammeln +existing_stacks = {stack['title']: stack['id'] for stack in target_stacks} + +for stack in stacks: + if stack['title'] in existing_stacks: + stackIdTo = existing_stacks[stack['title']] + stacksMap[stack['id']] = stackIdTo + print(f' Stack "{stack["title"]}" already exists. Syncing...') + else: + createdStack = lib.createStack(stack['title'], stack['order'], boardIdTo) + stackIdTo = createdStack['id'] + stacksMap[stack['id']] = stackIdTo + print(f' Created stack "{stack["title"]}"') + + if 'cards' in stack: + for card in stack['cards']: + try: + lib.copyCard(card, boardIdTo, stackIdTo, labelsMap) + except requests.exceptions.HTTPError as e: + print(f' Failed to create card "{card["title"]}". Error: {e}') + print(f' Response: {e.response.text}') + print(f' Created {len(stack["cards"])} cards') + +# Kopiere oder synchronisiere die archivierten Stacks und Karten des Boards +archived_stacks = lib.getStacksArchived(boardIdFrom) +target_archived_stacks = lib.getStacksArchived(boardIdTo,'to') + +# Existierende archivierte Stacks in der Zielinstanz sammeln +existing_archived_stacks = {stack['title']: stack['id'] for stack in target_archived_stacks} + +for stack in archived_stacks: + if stack['title'] in existing_archived_stacks: + stackIdTo = existing_archived_stacks[stack['title']] + print(f' Archived stack "{stack['title']}" already exists. Syncing...') + else: + createdStack = lib.createStack(stack['title'], stack['order'], boardIdTo) + stackIdTo = createdStack['id'] + stacksMap[stack['id']] = stackIdTo + print(f' Created archived stack "{stack['title']}"') + + if 'cards' in stack: + for card in stack['cards']: + try: + lib.copyCard(card, boardIdTo, stackIdTo, labelsMap) + except requests.exceptions.HTTPError as e: + print(f' Failed to create archived card "{card["title"]}". Error: {e}') + print(f' Response: {e.response.text}') + print(f' Created {len(stack["cards"])} archived cards')