paint-brush
So erstellen Sie ein Python-CLI-Programm für die Trello-Board-Verwaltung (Teil 2)von@elainechan01
1,780 Lesungen
1,780 Lesungen

So erstellen Sie ein Python-CLI-Programm für die Trello-Board-Verwaltung (Teil 2)

von Elaine Yun Ru Chan27m2023/11/07
Read on Terminal Reader

Zu lang; Lesen

Teil 2 der Tutorial-Reihe zum Erstellen eines Python-CLI-Programms für Trello Board Management konzentriert sich auf das Schreiben von Geschäftslogik für CLI-Befehle und die Python-Paketverteilung
featured image - So erstellen Sie ein Python-CLI-Programm für die Trello-Board-Verwaltung (Teil 2)
Elaine Yun Ru Chan HackerNoon profile picture
0-item

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 „So erstellen Sie ein Python-CLI-Programm für die Trello-Boardverwaltung (Teil 1)“ haben wir erfolgreich die Geschäftslogik für die Interaktion mit dem Trello SDK erstellt.


Hier ist eine kurze Zusammenfassung der Architektur unseres CLI-Programms:

Detaillierte Tabellenansicht der CLI-Struktur basierend auf den Anforderungen


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 PyPI verteilen.


Lass uns anfangen


Ordnerstruktur

Zuvor ist es uns gelungen, ein Gerüst zum Hosten unseres trelloservice Moduls einzurichten. Dieses Mal wollen wir einen cli Ordner mit Modulen für verschiedene Funktionalitäten implementieren, nämlich:


  • config
  • Zugang
  • Liste


Die Idee ist, dass für jede Befehlsgruppe ihre Befehle in einem eigenen Modul gespeichert werden. Den list speichern wir in der Haupt-CLI-Datei, da er zu keiner Befehlsgruppe gehört.


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 assets gibt? Dies wird verwendet, um zugehörige Assets für unsere README zu speichern, während es in trellocli einen neu implementierten shared Ordner gibt, den wir zum Speichern von Modulen verwenden, die in der gesamten Software verwendet werden sollen.


Aufstellen

Beginnen wir mit der Änderung unserer Einstiegspunktdatei __main__.py . 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, cli.py , über eine app Instanz verfügt, die wir ausführen können.

 # 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 cli.py Datei; Wir werden unsere app Instanz hier speichern. Die Idee besteht darin, ein Typer Objekt zu initialisieren, das in der gesamten Software gemeinsam genutzt werden soll.

 # 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 pyproject.toml , um unsere Befehlszeilenskripte anzugeben. Hier geben wir einen Namen für unser Paket ein und definieren den Einstiegspunkt.

 # pyproject.toml [project.scripts] trellocli = "trellocli.__main__:main"


Basierend auf dem obigen Beispiel haben wir trellocli als Paketnamen definiert und die main im __main__ Skript, das im trellocli Modul gespeichert ist, wird zur Laufzeit ausgeführt.


Nachdem der CLI-Teil unserer Software nun eingerichtet ist, ändern wir unser trelloservice Modul, um unser CLI-Programm besser zu bedienen. Wie Sie sich erinnern, ist unser trelloservice 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 config access auszuführen. Dadurch wird sichergestellt, dass unser Programm sauberer und in Bezug auf die Anweisungen aussagekräftiger ist.


Um es in Worte zu fassen: Wir werden diese Funktionen ändern:


  • __init__
  • __load_oauth_token_env_var
  • authorize
  • is_authorized


Beginnend mit der Funktion __init__ initialisieren wir einen leeren Client, anstatt uns hier mit der Client-Einrichtung zu befassen.

 # trellocli/trelloservice.py class TrelloService: def __init__(self) -> None: self.__client = None


Herausforderungsecke 💡Können Sie unsere Funktion __load_oauth_token_env_var so ändern, dass sie nicht rekursiv nach der Autorisierung des Benutzers fragt? Hinweis: Eine rekursive Funktion ist eine Funktion, die sich selbst aufruft.


Bei den Hilfsfunktionen authorize und „ is_authorized besteht die Idee darin, dass „ authorize die Geschäftslogik zum Einrichten des Clients mithilfe der Funktion __load_oauth_token_env_var “ ausführt, während die Funktion „ is_authorized “ lediglich einen booleschen Wert zurückgibt, der angibt, ob eine Autorisierung erteilt wurde.

 # 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 __load_oauth_token_env_var und authorize darin besteht, dass __load_oauth_token_env_var eine interne Funktion ist, die dazu dient, das Autorisierungstoken als Umgebungsvariable zu speichern, während authorize als öffentlich zugängliche Funktion versucht, alle erforderlichen Anmeldeinformationen abzurufen und einen Trello-Client zu initialisieren.


Challenge Corner 💡Beachten Sie, wie unsere authorize Funktion einen AuthorizeResponse -Datentyp zurückgibt. Können Sie ein Modell implementieren, das über das Attribut status_code verfügt? Lesen Sie Teil 1 von „So erstellen Sie ein Python-CLI-Programm für die Trello-Board-Verwaltung“ (Hinweis: Sehen Sie sich an, wie wir Modelle erstellt haben).


Zum Schluss instanziieren wir ein Singleton- TrelloService Objekt am unteren Rand des Moduls. Sehen Sie sich gerne diesen Patch an, um zu sehen, wie der vollständige Code aussieht: 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 ERRORS , da diese Ausnahmen Unterklassen von BaseException sind und als typische benutzerdefinierte Ausnahmen fungieren, während die ERRORS eher als konstante Werte beginnend bei 0 dienen.


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 shell nachzubilden, um die erwarteten Ergebnisse zu testen. Das Tolle am Typer Modul ist, dass es über ein eigenes runner Objekt verfügt. Was die Ausführung der Tests betrifft, werden wir sie mit dem pytest Modul koppeln. Weitere Informationen finden Sie in den 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 success ist, auch bekannt als 0. Hier ist ein großartiger Artikel von RedHat darüber , 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


Herausforderungsecke 💡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)


Geschäftslogik


Haupt-CLI-Modul

Beachten Sie, dass dies unser Haupt- cli Modul sein wird – für alle Befehlsgruppen (config, create) wird ihre Geschäftslogik zur besseren Lesbarkeit in einer eigenen separaten Datei gespeichert.


In diesem Modul speichern wir unseren list . Wenn wir tiefer in den Befehl eintauchen, wissen wir, dass wir die folgenden Optionen implementieren möchten:


  • Board_Name: erforderlich, wenn config board noch nicht festgelegt wurde
  • 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 offiziellen Dokumenten ) oder einfach die Verwendung einer Standardumgebungsvariablen. Für unseren Anwendungsfall wollen wir es jedoch unkompliziert halten, indem wir unsere benutzerdefinierte Ausnahme InvalidUserInputError auslösen, wenn Bedingungen nicht erfüllt sind.


Um den Befehl zu erstellen, beginnen wir mit der Definition der Optionen. In Typer wären, wie in den offiziellen Dokumenten erwähnt, die wichtigsten Bestandteile zum Definieren einer Option:


  • Datentyp
  • Hilfstext
  • Standardwert


Um beispielsweise die detailed Option mit den folgenden Bedingungen zu erstellen:


  • 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 list mit den erforderlichen Optionen zu definieren, behandeln wir list insgesamt als Python-Funktion und ihre Optionen als erforderliche Parameter.

 # 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 app Instanz hinzufügen, die am Anfang der Datei initialisiert wird. Fühlen Sie sich frei, durch die offizielle Typer-Codebasis zu navigieren, um die Optionen nach Ihren Wünschen zu ändern.


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 board_name bereitgestellt wurde).
  • 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 detailed Option ausgewählt wurde)


Ein paar Dinge, die Sie beachten sollten …


  • Wir möchten Ausnahmen auslösen, wenn der Trellojob einen anderen Statuscode als SUCCESS erzeugt. Durch die Verwendung von try-catch Blöcken können wir schwerwiegende Abstürze unseres Programms verhindern.
  • Bei der Konfiguration des geeigneten Boards versuchen wir, das Trello-Board basierend auf der abgerufenen board_id für die Verwendung einzurichten. Daher möchten wir die folgenden Anwendungsfälle abdecken
    • Abrufen der board_id , wenn der board_name explizit bereitgestellt wurde, indem mit der Funktion get_all_boards im Trellojob nach einer Übereinstimmung gesucht wird
    • Abrufen der board_id , wie sie als Umgebungsvariable gespeichert ist, wenn die Option board_name nicht verwendet wurde
  • Die angezeigten Daten werden mit der Table des rich Pakets formatiert. Weitere Informationen zu rich finden Sie in den 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 TrelloService , die wir noch implementieren müssen. Bitte lesen Sie diesen Patch, wenn Sie Hilfe bei der Implementierung benötigen: 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 python -m trellocli --help im Terminal aus. Standardmäßig füllt das Typer-Modul die Ausgabe für den Befehl --help selbst aus. Und beachten Sie, wie wir trellocli als Paketnamen aufrufen können – erinnern Sie sich, wie dies zuvor in unserem pyproject.toml definiert wurde?


Lassen Sie uns ein wenig vorspulen und auch die Befehlsgruppen create “ und config initialisieren. Dazu verwenden wir einfach die Funktion add_typer für unser app Objekt. Die Idee ist, dass die Befehlsgruppe ein eigenes app Objekt hat, und wir fügen dieses einfach zusammen mit dem Namen der Befehlsgruppe und dem Hilfstext in die Haupt app in cli.py ein. Es sollte ungefähr so aussehen

 # trellocli/cli/cli.py app.add_typer(cli_config.app, name="config", help="COMMAND GROUP to initialize configurations")


Herausforderungsecke 💡Könnten Sie die Befehlsgruppe create selbst importieren? Weitere Informationen finden Sie in diesem Patch: trello-cli-kit


Unterbefehle

Um eine Befehlsgruppe für create einzurichten, speichern wir die entsprechenden Befehle in einem eigenen Modul. Das Setup ähnelt dem von cli.py , 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 Ctrl + C drückt, also den Vorgang unterbricht. Der Grund, warum wir dies für unseren Befehl list nicht behandelt haben, liegt darin, dass der Unterschied hier darin besteht, dass die Befehlsgruppe config 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.


Beginnend mit dem access verwenden wir schließlich die authorize Funktion, wie sie in unserem TrelloService erstellt wurde. Da die authorize die Konfiguration ganz alleine übernimmt, müssen wir nur die Ausführung des Prozesses überprüfen.

 # 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 board Befehl betrifft, werden wir verschiedene Module verwenden, um eine gute Benutzererfahrung zu bieten, einschließlich des einfachen Terminalmenüs, um eine Terminal-GUI für die Benutzerinteraktion anzuzeigen. Die Grundidee ist wie folgt:


  • Ü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 list bis zum Abrufen von Daten von der Platine verwenden.


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 Simple Terminal Menu Paket. Für andere Eingabeaufforderungen und verschiedene Elemente wie die Notwendigkeit einer Texteingabe oder die Bestätigung des Benutzers verwenden wir einfach das rich Paket. Es ist auch wichtig zu beachten, dass wir die optionalen Werte richtig behandeln müssen:


  • 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...")


Herausforderungsecke 💡Können Sie einen Fortschrittsbalken für den add anzeigen? Hinweis: Schauen Sie sich die Verwendung der Statusfunktion von rich an


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 pyproject.toml benötigen, ist die Angabe, welches Modul das Paket selbst speichert. In unserem Fall ist das trellocli . Hier sind die Metadaten, die hinzugefügt werden müssen:

 # pyproject.toml [tool.setuptools] packages = ["trellocli"]


Was unsere README.md betrifft, ist es eine gute Praxis, eine Art Leitfaden bereitzustellen, sei es Nutzungsrichtlinien oder Hinweise zum Einstieg. Wenn Sie Bilder in Ihre README.md eingefügt haben, sollten Sie deren absolute URL verwenden, die normalerweise das folgende Format hat

 https://raw.githubusercontent.com/<user>/<repo>/<branch>/<path-to-image>


TestPyPI

Wir werden die build und twine 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:

 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: Link zur Dokumentation ). Geben Sie einfach die folgenden Werte ein:


  • 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 Actions in Ihrem GitHub-Workflow und wählen Sie einen neuen Workflow aus. Wir verwenden den Workflow Publish Python Package , 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).


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 pull , add und commit 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: 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 Karol Horosin über die Integration von CI/CD in Ihr Python-Paket, 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.


Einpacken

Das war langwierig 😓. In diesem Tutorial haben Sie gelernt, Ihre Software mithilfe des Typer 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.


Du warst ein absoluter Meister darin, das alles durchzuhalten. Kommen Sie nicht zu Teil 3, wo wir die optionalen Funktionen implementieren?