Mittlerweile haben wir das Schulprojekt „Schere, Stein, Papier“ längst hinter uns gelassen – lasst uns gleich loslegen. Was werden wir mit diesem Tutorial erreichen? In wir erfolgreich die Geschäftslogik für die Interaktion mit dem Trello SDK erstellt. „So erstellen Sie ein Python-CLI-Programm für die Trello-Boardverwaltung (Teil 1)“ haben Hier ist eine kurze Zusammenfassung der Architektur unseres CLI-Programms: In diesem Tutorial schauen wir uns an, wie wir unser Projekt in ein CLI-Programm umwandeln, wobei wir uns auf die funktionalen und nichtfunktionalen Anforderungen konzentrieren. Andererseits lernen wir auch, wie wir unser Programm als Paket auf verteilen. PyPI Lass uns anfangen Ordnerstruktur Zuvor ist es uns gelungen, ein Gerüst zum Hosten unseres Moduls einzurichten. Dieses Mal wollen wir einen Ordner mit Modulen für verschiedene Funktionalitäten implementieren, nämlich: trelloservice cli config Zugang Liste Die Idee ist, dass für jede Befehlsgruppe ihre Befehle in einem eigenen Modul gespeichert werden. Den speichern wir in der Haupt-CLI-Datei, da er zu keiner Befehlsgruppe gehört. list Schauen wir uns andererseits die Bereinigung unserer Ordnerstruktur an. Genauer gesagt sollten wir die Skalierbarkeit der Software berücksichtigen, indem wir sicherstellen, dass die Verzeichnisse nicht überfüllt sind. Hier ist ein Vorschlag zu unserer Ordnerstruktur: trellocli/ __init__.py __main__.py trelloservice.py shared/ models.py custom_exceptions.py cli/ cli.py cli_config.py cli_create.py tests/ test_cli.py test_trelloservice.py assets/ images/ README.md pyproject.toml .env .gitignore Ist Ihnen aufgefallen, dass es den Ordner gibt? Dies wird verwendet, um zugehörige Assets für unsere zu speichern, während es in einen neu implementierten Ordner gibt, den wir zum Speichern von Modulen verwenden, die in der gesamten Software verwendet werden sollen. assets README trellocli shared Aufstellen Beginnen wir mit der Änderung unserer Einstiegspunktdatei . Beim Import selbst müssen wir solche Änderungen berücksichtigen, da wir uns entschieden haben, verwandte Module in ihren eigenen Unterordnern zu speichern. Andererseits gehen wir auch davon aus, dass das Haupt-CLI-Modul, , über eine Instanz verfügt, die wir ausführen können. __main__.py cli.py app # trellocli/__main__.py # module imports from trellocli import __app_name__ from trellocli.cli import cli from trellocli.trelloservice import TrelloService # dependencies imports # misc imports def main(): cli.app(prog_name=__app_name__) if __name__ == "__main__": main() Schneller Vorlauf zu unserer Datei; Wir werden unsere Instanz hier speichern. Die Idee besteht darin, ein Objekt zu initialisieren, das in der gesamten Software gemeinsam genutzt werden soll. cli.py app Typer # trellocli/cli/cli.py # module imports # dependencies imports from typer import Typer # misc imports # singleton instances app = Typer() Um mit diesem Konzept fortzufahren, ändern wir unsere , um unsere Befehlszeilenskripte anzugeben. Hier geben wir einen Namen für unser Paket ein und definieren den Einstiegspunkt. pyproject.toml # pyproject.toml [project.scripts] trellocli = "trellocli.__main__:main" Basierend auf dem obigen Beispiel haben wir als Paketnamen definiert und die im Skript, das im Modul gespeichert ist, wird zur Laufzeit ausgeführt. trellocli main __main__ trellocli Nachdem der CLI-Teil unserer Software nun eingerichtet ist, ändern wir unser Modul, um unser CLI-Programm besser zu bedienen. Wie Sie sich erinnern, ist unser Modul so eingerichtet, dass es rekursiv die Autorisierung des Benutzers anfordert, bis diese genehmigt wird. Wir werden dies so ändern, dass das Programm beendet wird, wenn keine Autorisierung erfolgt, und den Benutzer auffordern, den Befehl auszuführen. Dadurch wird sichergestellt, dass unser Programm sauberer und in Bezug auf die Anweisungen aussagekräftiger ist. trelloservice trelloservice config access Um es in Worte zu fassen: Wir werden diese Funktionen ändern: __init__ __load_oauth_token_env_var authorize is_authorized Beginnend mit der Funktion initialisieren wir einen leeren Client, anstatt uns hier mit der Client-Einrichtung zu befassen. __init__ # trellocli/trelloservice.py class TrelloService: def __init__(self) -> None: self.__client = None 💡Können Sie unsere Funktion so ändern, dass sie nicht rekursiv nach der Autorisierung des Benutzers fragt? Hinweis: Eine rekursive Funktion ist eine Funktion, die sich selbst aufruft. Herausforderungsecke __load_oauth_token_env_var Bei den Hilfsfunktionen und „ besteht die Idee darin, dass „ die Geschäftslogik zum Einrichten des Clients mithilfe der Funktion “ ausführt, während die Funktion „ “ lediglich einen booleschen Wert zurückgibt, der angibt, ob eine Autorisierung erteilt wurde. authorize is_authorized authorize __load_oauth_token_env_var is_authorized # trellocli/trelloservice.py class TrelloService: def authorize(self) -> AuthorizeResponse: """Method to authorize program to user's trello account Returns AuthorizeResponse: success / error """ self.__load_oauth_token_env_var() load_dotenv() if not os.getenv("TRELLO_OAUTH_TOKEN"): return AuthorizeResponse(status_code=TRELLO_AUTHORIZATION_ERROR) else: self.__client = TrelloClient( api_key=os.getenv("TRELLO_API_KEY"), api_secret=os.getenv("TRELLO_API_SECRET"), token=os.getenv("TRELLO_OAUTH_TOKEN") ) return AuthorizeResponse(status_code=SUCCESS) def is_authorized(self) -> bool: """Method to check authorization to user's trello account Returns bool: authorization to user's account """ if not self.__client: return False else: return True Beachten Sie, dass der Unterschied zwischen und darin besteht, dass eine ist, die dazu dient, das Autorisierungstoken als Umgebungsvariable zu speichern, während als öffentlich zugängliche Funktion versucht, alle erforderlichen Anmeldeinformationen abzurufen und einen Trello-Client zu initialisieren. __load_oauth_token_env_var authorize __load_oauth_token_env_var interne Funktion authorize 💡Beachten Sie, wie unsere Funktion einen -Datentyp zurückgibt. Können Sie ein Modell implementieren, das über das Attribut verfügt? Lesen Sie (Hinweis: Sehen Sie sich an, wie wir Modelle erstellt haben). Challenge Corner authorize AuthorizeResponse status_code Teil 1 von „So erstellen Sie ein Python-CLI-Programm für die Trello-Board-Verwaltung“ Zum Schluss instanziieren wir ein Singleton- Objekt am unteren Rand des Moduls. Sehen Sie sich gerne diesen Patch an, um zu sehen, wie der vollständige Code aussieht: TrelloService trello-cli-kit # trellocli/trelloservice.py trellojob = TrelloService() Schließlich möchten wir einige benutzerdefinierte Ausnahmen initialisieren, die im gesamten Programm gemeinsam genutzt werden sollen. Dies unterscheidet sich von den in unserem Initialisierer definierten , da diese Ausnahmen Unterklassen von sind und als typische benutzerdefinierte Ausnahmen fungieren, während die eher als konstante Werte beginnend bei 0 dienen. ERRORS BaseException ERRORS Beschränken wir unsere Ausnahmen auf ein Minimum und gehen wir auf einige der häufigsten Anwendungsfälle ein, insbesondere auf: Lesefehler: Wird ausgelöst, wenn beim Lesen von Trello ein Fehler auftritt Schreibfehler: Wird ausgelöst, wenn beim Schreiben in Trello ein Fehler auftritt Autorisierungsfehler: Wird ausgelöst, wenn für Trello keine Autorisierung erteilt wird Fehler aufgrund einer ungültigen Benutzereingabe: Wird ausgelöst, wenn die CLI-Eingabe des Benutzers nicht erkannt wird # trellocli/shared/custom_exceptions.py class TrelloReadError(BaseException): pass class TrelloWriteError(BaseException): pass class TrelloAuthorizationError(BaseException): pass class InvalidUserInputError(BaseException): pass Unit-Tests Wie in Teil I erwähnt, werden wir in diesem Tutorial nicht ausführlich auf Unit-Tests eingehen, also arbeiten wir nur mit den notwendigen Elementen: Testen Sie, um den Zugriff zu konfigurieren Testen Sie, um das Trello-Board zu konfigurieren Testen Sie, um eine neue Trello-Karte zu erstellen Testen Sie, um Trello-Board-Details anzuzeigen Test zur Anzeige von Trello-Board-Details (Detailansicht) Die Idee besteht darin, einen Befehlszeileninterpreter wie eine nachzubilden, um die erwarteten Ergebnisse zu testen. Das Tolle am Modul ist, dass es über ein eigenes Objekt verfügt. Was die Ausführung der Tests betrifft, werden wir sie mit dem Modul koppeln. Weitere Informationen finden Sie in den . shell Typer runner pytest offiziellen Dokumenten von Typer Lassen Sie uns gemeinsam den ersten Test durcharbeiten, nämlich die Zugriffskonfiguration. Bitte beachten Sie, dass wir testen, ob die Funktion ordnungsgemäß ausgeführt wird. Dazu überprüfen wir die Systemantwort und ob der Exit-Code ist, auch bekannt als 0. Hier ist ein großartiger Artikel von RedHat darüber . success , was Exit-Codes sind und wie das System sie zur Kommunikation von Prozessen verwendet # trellocli/tests/test_cli.py # module imports from trellocli.cli.cli import app # dependencies imports from typer.testing import CliRunner # misc imports runner = CliRunner() def test_config_access(): res = runner.invoke(app, ["config", "access"]) assert result.exit_code == 0 assert "Go to the following link in your browser:" in result.stdout 💡Können Sie nun, da Sie das Wesentliche verstanden haben, andere Testfälle selbst implementieren? (Hinweis: Sie sollten auch Tests auf Fehlerfälle in Betracht ziehen) Herausforderungsecke Geschäftslogik Haupt-CLI-Modul Beachten Sie, dass dies unser Haupt- Modul sein wird – für alle Befehlsgruppen (config, create) wird ihre Geschäftslogik zur besseren Lesbarkeit in einer eigenen separaten Datei gespeichert. cli In diesem Modul speichern wir unseren . Wenn wir tiefer in den Befehl eintauchen, wissen wir, dass wir die folgenden Optionen implementieren möchten: list Board_Name: erforderlich, wenn noch nicht festgelegt wurde config board detailliert: Anzeige in einer Detailansicht Beginnend mit der erforderlichen Option „board_name“ gibt es mehrere Möglichkeiten, dies zu erreichen. Eine davon ist die Verwendung der Callback-Funktion (weitere Informationen finden Sie hier in den ) oder einfach die Verwendung einer Standardumgebungsvariablen. Für unseren Anwendungsfall wollen wir es jedoch unkompliziert halten, indem wir unsere benutzerdefinierte Ausnahme auslösen, wenn Bedingungen nicht erfüllt sind. offiziellen Dokumenten InvalidUserInputError Um den Befehl zu erstellen, beginnen wir mit der Definition der Optionen. In Typer wären, wie in den erwähnt, die wichtigsten Bestandteile zum Definieren einer Option: offiziellen Dokumenten Datentyp Hilfstext Standardwert Um beispielsweise die Option mit den folgenden Bedingungen zu erstellen: detailed Datentyp: bool Hilfstext: „Detailansicht aktivieren“ Standardwert: Keine Unser Code würde so aussehen: detailed: Annotated[bool, typer.Option(help=”Enable detailed view)] = None Um den mit den erforderlichen Optionen zu definieren, behandeln wir insgesamt als Python-Funktion und ihre Optionen als erforderliche Parameter. list list # trellocli/cli/cli.py @app.command() def list( detailed: Annotated[bool, Option(help="Enable detailed view")] = None, board_name: Annotated[str, Option(help="Trello board to search")] = "" ) -> None: pass Beachten Sie, dass wir den Befehl der Instanz hinzufügen, die am Anfang der Datei initialisiert wird. Fühlen Sie sich frei, durch die zu navigieren, um die Optionen nach Ihren Wünschen zu ändern. app offizielle Typer-Codebasis Was den Arbeitsablauf des Befehls betrifft, gehen wir etwa so vor: Berechtigung prüfen Konfigurieren Sie, um das entsprechende Board zu verwenden (überprüfen Sie, ob die Option bereitgestellt wurde). board_name Richten Sie das Trello-Board zum Lesen ein Rufen Sie die entsprechenden Trello-Kartendaten ab und kategorisieren Sie sie basierend auf der Trello-Liste Daten anzeigen (überprüfen Sie, ob die Option ausgewählt wurde) detailed Ein paar Dinge, die Sie beachten sollten … Wir möchten Ausnahmen auslösen, wenn der Trellojob einen anderen Statuscode als erzeugt. Durch die Verwendung von Blöcken können wir schwerwiegende Abstürze unseres Programms verhindern. SUCCESS try-catch Bei der Konfiguration des geeigneten Boards versuchen wir, das Trello-Board basierend auf der abgerufenen für die Verwendung einzurichten. Daher möchten wir die folgenden Anwendungsfälle abdecken board_id Abrufen der , wenn der explizit bereitgestellt wurde, indem mit der Funktion im Trellojob nach einer Übereinstimmung gesucht wird board_id board_name get_all_boards Abrufen der , wie sie als Umgebungsvariable gespeichert ist, wenn die Option nicht verwendet wurde board_id board_name Die angezeigten Daten werden mit der des Pakets formatiert. Weitere Informationen zu finden Sie in den Table rich rich offiziellen Dokumenten Detailliert: Zeigt eine Zusammenfassung der Anzahl der Trello-Listen, der Anzahl der Karten und der definierten Beschriftungen an. Zeigen Sie für jede Trello-Liste alle Karten und ihre entsprechenden Namen, Beschreibungen und zugehörigen Beschriftungen an Nicht detailliert: Zeigt eine Zusammenfassung der Anzahl der Trello-Listen, der Anzahl der Karten und der definierten Beschriftungen an Wenn wir alles zusammenfügen, erhalten wir Folgendes. Haftungsausschluss: Möglicherweise fehlen einige Funktionen von , die wir noch implementieren müssen. Bitte lesen Sie diesen Patch, wenn Sie Hilfe bei der Implementierung benötigen: TrelloService trello-cli-kit # trellocli/cli/cli.py # module imports from trellocli.trelloservice import trellojob from trellocli.cli import cli_config, cli_create from trellocli.misc.custom_exceptions import * from trellocli import SUCCESS # dependencies imports from typer import Typer, Option from rich import print from rich.console import Console from rich.table import Table from dotenv import load_dotenv # misc imports from typing_extensions import Annotated import os # singleton instances app = Typer() console = Console() # init command groups app.add_typer(cli_config.app, name="config", help="COMMAND GROUP to initialize configurations") app.add_typer(cli_create.app, name="create", help="COMMAND GROUP to create new Trello elements") @app.command() def list( detailed: Annotated[ bool, Option(help="Enable detailed view") ] = None, board_name: Annotated[str, Option()] = "" ) -> None: """COMMAND to list board details in a simplified (default)/detailed view OPTIONS detailed (bool): request for detailed view board_name (str): board to use """ try: # check authorization res_is_authorized = trellojob.is_authorized() if not res_is_authorized: print("[bold red]Error![/] Authorization hasn't been granted. Try running `trellocli config access`") raise AuthorizationError # if board_name OPTION was given, attempt to retrieve board id using the name # else attempt to retrieve board id stored as an env var board_id = None if not board_name: load_dotenv() if not os.getenv("TRELLO_BOARD_ID"): print("[bold red]Error![/] A trello board hasn't been configured to use. Try running `trellocli config board`") raise InvalidUserInputError board_id = os.getenv("TRELLO_BOARD_ID") else: res_get_all_boards = trellojob.get_all_boards() if res_get_all_boards.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving boards from trello") raise TrelloReadError boards_list = {board.name: board.id for board in res_get_all_boards.res} # retrieve all board id(s) and find matching board name if board_name not in boards_list: print("[bold red]Error![/] An invalid trello board name was provided. Try running `trellocli config board`") raise InvalidUserInputError board_id = boards_list[board_name] # configure board to use res_get_board = trellojob.get_board(board_id=board_id) if res_get_board.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when configuring the trello board to use") raise TrelloReadError board = res_get_board.res # retrieve data (labels, trellolists) from board res_get_all_labels = trellojob.get_all_labels(board=board) if res_get_all_labels.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving data from board") raise TrelloReadError labels_list = res_get_all_labels.res res_get_all_lists = trellojob.get_all_lists(board=board) if res_get_all_lists.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving data from board") raise TrelloReadError trellolists_list = res_get_all_lists.res # store data on cards for each trellolist trellolists_dict = {trellolist: [] for trellolist in trellolists_list} for trellolist in trellolists_list: res_get_all_cards = trellojob.get_all_cards(trellolist=trellolist) if res_get_all_cards.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving cards from trellolist") raise TrelloReadError cards_list = res_get_all_cards.res trellolists_dict[trellolist] = cards_list # display data (lists count, cards count, labels) # if is_detailed OPTION is selected, display data (name, description, labels) for each card in each trellolist print() table = Table(title="Board: "+board.name, title_justify="left", show_header=False) table.add_row("[bold]Lists count[/]", str(len(trellolists_list))) table.add_row("[bold]Cards count[/]", str(sum([len(cards_list) for cards_list in trellolists_dict.values()]))) table.add_row("[bold]Labels[/]", ", ".join([label.name for label in labels_list if label.name])) console.print(table) if detailed: for trellolist, cards_list in trellolists_dict.items(): table = Table("Name", "Desc", "Labels", title="List: "+trellolist.name, title_justify="left") for card in cards_list: table.add_row(card.name, card.description, ", ".join([label.name for label in card.labels if label.name])) console.print(table) print() except (AuthorizationError, InvalidUserInputError, TrelloReadError): print("Program exited...") Um unsere Software in Aktion zu sehen, führen Sie einfach im Terminal aus. Standardmäßig füllt das Typer-Modul die Ausgabe für den Befehl selbst aus. Und beachten Sie, wie wir als Paketnamen aufrufen können – erinnern Sie sich, wie dies zuvor in unserem definiert wurde? python -m trellocli --help --help trellocli pyproject.toml Lassen Sie uns ein wenig vorspulen und auch die Befehlsgruppen “ und initialisieren. Dazu verwenden wir einfach die Funktion für unser Objekt. Die Idee ist, dass die Befehlsgruppe ein eigenes Objekt hat, und wir fügen dieses einfach zusammen mit dem Namen der Befehlsgruppe und dem Hilfstext in die Haupt in ein. Es sollte ungefähr so aussehen create config add_typer app app app cli.py # trellocli/cli/cli.py app.add_typer(cli_config.app, name="config", help="COMMAND GROUP to initialize configurations") 💡Könnten Sie die Befehlsgruppe selbst importieren? Weitere Informationen finden Sie in diesem Patch: Herausforderungsecke create trello-cli-kit Unterbefehle Um eine Befehlsgruppe für einzurichten, speichern wir die entsprechenden Befehle in einem eigenen Modul. Das Setup ähnelt dem von , erfordert jedoch die Instanziierung eines Typer-Objekts. Was die Befehle betrifft, möchten wir auch die Notwendigkeit berücksichtigen, benutzerdefinierte Ausnahmen zu verwenden. Ein weiteres Thema, das wir behandeln möchten, ist, wenn der Benutzer drückt, also den Vorgang unterbricht. Der Grund, warum wir dies für unseren Befehl nicht behandelt haben, liegt darin, dass der Unterschied hier darin besteht, dass die Befehlsgruppe aus interaktiven Befehlen besteht. Der Hauptunterschied zwischen interaktiven Befehlen besteht darin, dass sie eine kontinuierliche Benutzerinteraktion erfordern. Nehmen wir natürlich an, dass die Ausführung unseres direkten Befehls lange dauert. Es ist auch eine bewährte Methode, mit potenziellen Tastaturunterbrechungen umzugehen. create cli.py Ctrl + C list config Beginnend mit dem verwenden wir schließlich die Funktion, wie sie in unserem erstellt wurde. Da die die Konfiguration ganz alleine übernimmt, müssen wir nur die Ausführung des Prozesses überprüfen. access authorize TrelloService authorize # trellocli/cli/cli_config.py @app.command() def access() -> None: """COMMAND to configure authorization for program to access user's Trello account""" try: # check authorization res_authorize = trellojob.authorize() if res_authorize.status_code != SUCCESS: print("[bold red]Error![/] Authorization hasn't been granted. Try running `trellocli config access`") raise AuthorizationError except KeyboardInterrupt: print("[yellow]Keyboard Interrupt.[/] Program exited...") except AuthorizationError: print("Program exited...") Was den Befehl betrifft, werden wir verschiedene Module verwenden, um eine gute Benutzererfahrung zu bieten, einschließlich um eine Terminal-GUI für die Benutzerinteraktion anzuzeigen. Die Grundidee ist wie folgt: board des einfachen Terminalmenüs, Überprüfen Sie die Autorisierung Rufen Sie alle Trello-Boards vom Benutzerkonto ab Zeigen Sie ein Einzelauswahl-Terminalmenü von Trello-Boards an Legen Sie die ausgewählte Trello-Board-ID als Umgebungsvariable fest # trellocli/cli/cli_config.py @app.command() def board() -> None: """COMMAND to initialize Trello board""" try: # check authorization res_is_authorized = trellojob.is_authorized() if not res_is_authorized: print("[bold red]Error![/] Authorization hasn't been granted. Try running `trellocli config access`") raise AuthorizationError # retrieve all boards res_get_all_boards = trellojob.get_all_boards() if res_get_all_boards.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving trello boards") raise TrelloReadError boards_list = {board.name: board.id for board in res_get_all_boards.res} # for easy access to board id when given board name # display menu to select board boards_menu = TerminalMenu( boards_list.keys(), title="Select board:", raise_error_on_interrupt=True ) boards_menu.show() selected_board = boards_menu.chosen_menu_entry # set board ID as env var dotenv_path = find_dotenv() set_key( dotenv_path=dotenv_path, key_to_set="TRELLO_BOARD_ID", value_to_set=boards_list[selected_board] ) except KeyboardInterrupt: print("[yellow]Keyboard Interrupt.[/] Program exited...") except (AuthorizationError, TrelloReadError): print("Program exited...") Abschließend beschäftigen wir uns mit der Kernfunktionsanforderung unserer Software: Hinzufügen einer neuen Karte zu einer Liste auf dem Trello-Board. Wir werden die gleichen Schritte von unserem bis zum Abrufen von Daten von der Platine verwenden. list Darüber hinaus werden wir den Benutzer interaktiv um Eingaben bitten, um die neue Karte richtig zu konfigurieren: Trello-Liste, die hinzugefügt werden soll: Einzelauswahl Kartenname: Text [Optional] Kartenbeschreibung: Text [Optional] Beschriftungen: Mehrfachauswahl Bestätigung: j/n Für alle Eingabeaufforderungen, bei denen der Benutzer aus einer Liste auswählen muss, verwenden wir wie zuvor das Paket. Für andere Eingabeaufforderungen und verschiedene Elemente wie die Notwendigkeit einer Texteingabe oder die Bestätigung des Benutzers verwenden wir einfach das Paket. Es ist auch wichtig zu beachten, dass wir die optionalen Werte richtig behandeln müssen: Simple Terminal Menu rich Benutzer können auf die Bereitstellung einer Beschreibung verzichten Benutzer können eine leere Auswahl für Beschriftungen bereitstellen # trellocli/cli/cli_create.py @app.command() def card( board_name: Annotated[str, Option()] = "" ) -> None: """COMMAND to add a new trello card OPTIONS board_name (str): board to use """ try: # check authorization res_is_authorized = trellojob.is_authorized() if not res_is_authorized: print("[bold red]Error![/] Authorization hasn't been granted. Try running `trellocli config access`") raise AuthorizationError # if board_name OPTION was given, attempt to retrieve board id using the name # else attempt to retrieve board id stored as an env var board_id = None if not board_name: load_dotenv() if not os.getenv("TRELLO_BOARD_ID"): print("[bold red]Error![/] A trello board hasn't been configured to use. Try running `trellocli config board`") raise InvalidUserInputError board_id = os.getenv("TRELLO_BOARD_ID") else: res_get_all_boards = trellojob.get_all_boards() if res_get_all_boards.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving boards from trello") raise TrelloReadError boards_list = {board.name: board.id for board in res_get_all_boards.res} # retrieve all board id(s) and find matching board name if board_name not in boards_list: print("[bold red]Error![/] An invalid trello board name was provided. Try running `trellocli config board`") raise InvalidUserInputError board_id = boards_list[board_name] # configure board to use res_get_board = trellojob.get_board(board_id=board_id) if res_get_board.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when configuring the trello board to use") raise TrelloReadError board = res_get_board.res # retrieve data (labels, trellolists) from board res_get_all_labels = trellojob.get_all_labels(board=board) if res_get_all_labels.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving the labels from the trello board") raise TrelloReadError labels_list = res_get_all_labels.res labels_dict = {label.name: label for label in labels_list if label.name} res_get_all_lists = trellojob.get_all_lists(board=board) if res_get_all_lists.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when retrieving the lists from the trello board") raise TrelloReadError trellolists_list = res_get_all_lists.res trellolists_dict = {trellolist.name: trellolist for trellolist in trellolists_list} # for easy access to trellolist when given name of trellolist # request for user input (trellolist, card name, description, labels to include) interactively to configure new card to be added trellolist_menu = TerminalMenu( trellolists_dict.keys(), title="Select list:", raise_error_on_interrupt=True ) # Prompt: trellolist trellolist_menu.show() print(trellolist_menu.chosen_menu_entry) selected_trellolist = trellolists_dict[trellolist_menu.chosen_menu_entry] selected_name = Prompt.ask("Card name") # Prompt: card name selected_desc = Prompt.ask("Description (Optional)", default=None) # Prompt (Optional) description labels_menu = TerminalMenu( labels_dict.keys(), title="Select labels (Optional):", multi_select=True, multi_select_empty_ok=True, multi_select_select_on_accept=False, show_multi_select_hint=True, raise_error_on_interrupt=True ) # Prompt (Optional): labels labels_menu.show() selected_labels = [labels_dict[label] for label in list(labels_menu.chosen_menu_entries)] if labels_menu.chosen_menu_entries else None # display user selection and request confirmation print() confirmation_table = Table(title="Card to be Added", show_header=False) confirmation_table.add_row("List", selected_trellolist.name) confirmation_table.add_row("Name", selected_name) confirmation_table.add_row("Description", selected_desc) confirmation_table.add_row("Labels", ", ".join([label.name for label in selected_labels]) if selected_labels else None) console.print(confirmation_table) confirm = Confirm.ask("Confirm") # if confirm, attempt to add card to trello # else, exit if confirm: res_add_card = trellojob.add_card( col=selected_trellolist, name=selected_name, desc=selected_desc, labels=selected_labels ) if res_add_card.status_code != SUCCESS: print("[bold red]Error![/] A problem occurred when adding a new card to trello") raise TrelloWriteError else: print("Process cancelled...") except KeyboardInterrupt: print("[yellow]Keyboard Interrupt.[/] Program exited...") except (AuthorizationError, InvalidUserInputError, TrelloReadError, TrelloWriteError): print("Program exited...") 💡Können Sie einen Fortschrittsbalken für den anzeigen? Hinweis: Schauen Sie sich die Verwendung von an Herausforderungsecke add der Statusfunktion rich Paketverteilung Hier kommt der spaßige Teil – die offizielle Verbreitung unserer Software auf PyPI. Dazu folgen wir dieser Pipeline: Metadaten konfigurieren + README aktualisieren Hochladen, um PyPI zu testen Konfigurieren Sie GitHub-Aktionen Push-Code an Tag v1.0.0 Code an PyPI verteilen 🎉 Eine ausführliche Erklärung finden Sie in diesem großartigen Tutorial zu Python Packaging von Ramit Mittal. Metadatenkonfiguration Das letzte Detail, das wir für unsere benötigen, ist die Angabe, welches Modul das Paket selbst speichert. In unserem Fall ist das . Hier sind die Metadaten, die hinzugefügt werden müssen: pyproject.toml trellocli # pyproject.toml [tool.setuptools] packages = ["trellocli"] Was unsere betrifft, ist es eine gute Praxis, eine Art Leitfaden bereitzustellen, sei es Nutzungsrichtlinien oder Hinweise zum Einstieg. Wenn Sie Bilder in Ihre eingefügt haben, sollten Sie deren absolute URL verwenden, die normalerweise das folgende Format hat README.md README.md https://raw.githubusercontent.com/<user>/<repo>/<branch>/<path-to-image> TestPyPI Wir werden die und Tools verwenden, um unser Paket zu erstellen und zu veröffentlichen. Führen Sie den folgenden Befehl in Ihrem Terminal aus, um ein Quellarchiv und ein Rad für Ihr Paket zu erstellen: build twine python -m build Stellen Sie sicher, dass Sie bereits ein Konto bei TestPyPI eingerichtet haben, und führen Sie den folgenden Befehl aus twine upload -r testpypi dist/* Sie werden aufgefordert, Ihren Benutzernamen und Ihr Passwort einzugeben. Da die Zwei-Faktor-Authentifizierung aktiviert ist, müssen Sie ein API-Token verwenden (Weitere Informationen zum Erwerb eines TestPyPI-API-Tokens: ). Geben Sie einfach die folgenden Werte ein: Link zur Dokumentation Nutzername: Zeichen Passwort: <Ihr TestPyPI-Token> Sobald dies abgeschlossen ist, sollten Sie in der Lage sein, zu TestPyPI zu gehen, um Ihr neu verteiltes Paket auszuprobieren! GitHub-Setup Das Ziel besteht darin, GitHub als Mittel zu nutzen, um neue Versionen Ihres Pakets basierend auf Tags kontinuierlich zu aktualisieren. Gehen Sie zunächst zur Registerkarte in Ihrem GitHub-Workflow und wählen Sie einen neuen Workflow aus. Wir verwenden den Workflow , der von GitHub Actions erstellt wurde. Ist Ihnen aufgefallen, dass der Workflow das Lesen von Geheimnissen aus dem Repository erfordert? Stellen Sie sicher, dass Sie Ihr PyPI-Token unter dem angegebenen Namen gespeichert haben (der Erwerb eines PyPI-API-Tokens ähnelt dem von TestPyPI). Actions Publish Python Package Sobald der Workflow erstellt ist, übertragen wir unseren Code auf das Tag v1.0.0. Für weitere Informationen zur Versionsbenennungssyntax finden Sie hier eine tolle Erklärung von Py-Pkgs: Link zur Dokumentation Führen Sie einfach die üblichen , und Befehle aus. Erstellen Sie als Nächstes ein Tag für Ihr letztes Commit, indem Sie den folgenden Befehl ausführen (Weitere Informationen zu Tags: ) pull add commit Link zur Dokumentation git tag <tagname> HEAD Zum Schluss übertragen Sie Ihr neues Tag in das Remote-Repository git push <remote name> <tag name> Hier ist ein großartiger Artikel von wenn Sie mehr erfahren möchten. Aber lehnen Sie sich vorerst zurück und genießen Sie Ihre neueste Errungenschaft 🎉. Fühlen Sie sich frei, dem Zauber dabei zuzusehen, wie er sich als GitHub Actions-Workflow entfaltet, während er Ihr Paket an PyPI verteilt. Karol Horosin über die Integration von CI/CD in Ihr Python-Paket, Einpacken Das war langwierig 😓. In diesem Tutorial haben Sie gelernt, Ihre Software mithilfe des Moduls in ein CLI-Programm umzuwandeln und Ihr Paket an PyPI zu verteilen. Um tiefer einzutauchen, haben Sie gelernt, Befehle und Befehlsgruppen zu definieren, eine interaktive CLI-Sitzung zu entwickeln und sich mit gängigen CLI-Szenarien wie Tastaturunterbrechungen auseinanderzusetzen. Typer Du warst ein absoluter Meister darin, das alles durchzuhalten. Kommen Sie nicht zu Teil 3, wo wir die optionalen Funktionen implementieren?