Mittlerweile haben wir das Schulprojekt „Schere, Stein, Papier“ längst hinter uns gelassen – lasst uns gleich loslegen.
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:
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.
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:
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.
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:
# trellocli/shared/custom_exceptions.py class TrelloReadError(BaseException): pass class TrelloWriteError(BaseException): pass class TrelloAuthorizationError(BaseException): pass class InvalidUserInputError(BaseException): pass
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:
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)
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:
config board
noch nicht festgelegt wurde
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:
Um beispielsweise die detailed
Option mit den folgenden Bedingungen zu erstellen:
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:
board_name
bereitgestellt wurde).detailed
Option ausgewählt wurde)
Ein paar Dinge, die Sie beachten sollten …
SUCCESS
erzeugt. Durch die Verwendung von try-catch
Blöcken können wir schwerwiegende Abstürze unseres Programms verhindern.board_id
für die Verwendung einzurichten. Daher möchten wir die folgenden Anwendungsfälle abdeckenboard_id
, wenn der board_name
explizit bereitgestellt wurde, indem mit der Funktion get_all_boards
im Trellojob nach einer Übereinstimmung gesucht wirdboard_id
, wie sie als Umgebungsvariable gespeichert ist, wenn die Option board_name
nicht verwendet wurdeTable
des rich
Pakets formatiert. Weitere Informationen zu rich
finden Sie in den offiziellen Dokumenten
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
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:
# 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:
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:
# 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
Hier kommt der spaßige Teil – die offizielle Verbreitung unserer Software auf PyPI. Dazu folgen wir dieser Pipeline:
Eine ausführliche Erklärung finden Sie in diesem großartigen Tutorial zu Python Packaging von Ramit Mittal.
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>
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:
Sobald dies abgeschlossen ist, sollten Sie in der Lage sein, zu TestPyPI zu gehen, um Ihr neu verteiltes Paket auszuprobieren!
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.
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?