mirror of
https://github.com/platformio/platformio-core.git
synced 2025-07-30 18:17:13 +02:00
New Account Management System (#3443)
* add login for PIO account to account cli * Remove PyJWT lib. Fixes. * Add password change for account * Refactoring. Add Account Client. * Fixes. * http -> https. * adding error handling for expired session. * Change broker requests from json to form-data. * Add pio accoint register command. fixes * Fixes. * Fixes. * Add username and password validation * fixes * Add token, forgot commands to pio account * fix domain * add update command for pio account * fixes * refactor profile update output * lint * Update exception text. * Fix logout * Add custom user-agent for pio account * add profile show command. minor fixes. * Fix pio account show output format. * Move account related exceptions * cleaning * minor fix * Remove try except for account command authenticated/non-authenticated errors * fix profile update cli command * rename first name and last name vars to 'firstname' and 'lastname'
This commit is contained in:
@ -1,72 +0,0 @@
|
|||||||
# Copyright (c) 2014-present PlatformIO <contact@platformio.org>
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import click
|
|
||||||
|
|
||||||
from platformio.managers.core import pioplus_call
|
|
||||||
|
|
||||||
|
|
||||||
@click.group("account", short_help="Manage PIO Account")
|
|
||||||
def cli():
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command("register", short_help="Create new PIO Account")
|
|
||||||
@click.option("-u", "--username")
|
|
||||||
def account_register(**kwargs):
|
|
||||||
pioplus_call(sys.argv[1:])
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command("login", short_help="Log in to PIO Account")
|
|
||||||
@click.option("-u", "--username")
|
|
||||||
@click.option("-p", "--password")
|
|
||||||
def account_login(**kwargs):
|
|
||||||
pioplus_call(sys.argv[1:])
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command("logout", short_help="Log out of PIO Account")
|
|
||||||
def account_logout():
|
|
||||||
pioplus_call(sys.argv[1:])
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command("password", short_help="Change password")
|
|
||||||
@click.option("--old-password")
|
|
||||||
@click.option("--new-password")
|
|
||||||
def account_password(**kwargs):
|
|
||||||
pioplus_call(sys.argv[1:])
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command("token", short_help="Get or regenerate Authentication Token")
|
|
||||||
@click.option("-p", "--password")
|
|
||||||
@click.option("--regenerate", is_flag=True)
|
|
||||||
@click.option("--json-output", is_flag=True)
|
|
||||||
def account_token(**kwargs):
|
|
||||||
pioplus_call(sys.argv[1:])
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command("forgot", short_help="Forgot password")
|
|
||||||
@click.option("-u", "--username")
|
|
||||||
def account_forgot(**kwargs):
|
|
||||||
pioplus_call(sys.argv[1:])
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command("show", short_help="PIO Account information")
|
|
||||||
@click.option("--offline", is_flag=True)
|
|
||||||
@click.option("--json-output", is_flag=True)
|
|
||||||
def account_show(**kwargs):
|
|
||||||
pioplus_call(sys.argv[1:])
|
|
13
platformio/commands/account/__init__.py
Normal file
13
platformio/commands/account/__init__.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Copyright (c) 2014-present PlatformIO <contact@platformio.org>
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
217
platformio/commands/account/client.py
Normal file
217
platformio/commands/account/client.py
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
# Copyright (c) 2014-present PlatformIO <contact@platformio.org>
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
import requests.adapters
|
||||||
|
from requests.packages.urllib3.util.retry import Retry # pylint:disable=import-error
|
||||||
|
|
||||||
|
from platformio import app
|
||||||
|
from platformio.commands.account import exception
|
||||||
|
|
||||||
|
|
||||||
|
class AccountClient(object):
|
||||||
|
def __init__(
|
||||||
|
self, api_base_url="https://api.accounts.platformio.org", retries=3,
|
||||||
|
):
|
||||||
|
if api_base_url.endswith("/"):
|
||||||
|
api_base_url = api_base_url[:-1]
|
||||||
|
self.api_base_url = api_base_url
|
||||||
|
self._session = requests.Session()
|
||||||
|
self._session.headers.update({"User-Agent": app.get_user_agent()})
|
||||||
|
retry = Retry(
|
||||||
|
total=retries,
|
||||||
|
read=retries,
|
||||||
|
connect=retries,
|
||||||
|
backoff_factor=2,
|
||||||
|
method_whitelist=list(Retry.DEFAULT_METHOD_WHITELIST) + ["POST"],
|
||||||
|
)
|
||||||
|
adapter = requests.adapters.HTTPAdapter(max_retries=retry)
|
||||||
|
self._session.mount(api_base_url, adapter)
|
||||||
|
|
||||||
|
def login(self, username, password):
|
||||||
|
try:
|
||||||
|
self.fetch_authentication_token()
|
||||||
|
except: # pylint:disable=bare-except
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise exception.AccountAlreadyAuthenticated(
|
||||||
|
app.get_state_item("account", {}).get("email", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self._session.post(
|
||||||
|
self.api_base_url + "/v1/login",
|
||||||
|
data={"username": username, "password": password},
|
||||||
|
)
|
||||||
|
result = self.raise_error_from_response(response)
|
||||||
|
app.set_state_item("account", result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def logout(self):
|
||||||
|
try:
|
||||||
|
refresh_token = self.get_refresh_token()
|
||||||
|
except: # pylint:disable=bare-except
|
||||||
|
raise exception.AccountNotAuthenticated()
|
||||||
|
response = requests.post(
|
||||||
|
self.api_base_url + "/v1/logout", data={"refresh_token": refresh_token},
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
self.raise_error_from_response(response)
|
||||||
|
except exception.AccountError:
|
||||||
|
pass
|
||||||
|
app.delete_state_item("account")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def change_password(self, old_password, new_password):
|
||||||
|
try:
|
||||||
|
token = self.fetch_authentication_token()
|
||||||
|
except: # pylint:disable=bare-except
|
||||||
|
raise exception.AccountNotAuthenticated()
|
||||||
|
response = self._session.post(
|
||||||
|
self.api_base_url + "/v1/password",
|
||||||
|
headers={"Authorization": "Bearer %s" % token},
|
||||||
|
data={"old_password": old_password, "new_password": new_password},
|
||||||
|
)
|
||||||
|
self.raise_error_from_response(response)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def registration(
|
||||||
|
self, username, email, password, firstname, lastname
|
||||||
|
): # pylint:disable=too-many-arguments
|
||||||
|
try:
|
||||||
|
self.fetch_authentication_token()
|
||||||
|
except: # pylint:disable=bare-except
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise exception.AccountAlreadyAuthenticated(
|
||||||
|
app.get_state_item("account", {}).get("email", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self._session.post(
|
||||||
|
self.api_base_url + "/v1/registration",
|
||||||
|
data={
|
||||||
|
"username": username,
|
||||||
|
"email": email,
|
||||||
|
"password": password,
|
||||||
|
"firstname": firstname,
|
||||||
|
"lastname": lastname,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return self.raise_error_from_response(response)
|
||||||
|
|
||||||
|
def auth_token(self, password, regenerate):
|
||||||
|
try:
|
||||||
|
token = self.fetch_authentication_token()
|
||||||
|
except: # pylint:disable=bare-except
|
||||||
|
raise exception.AccountNotAuthenticated()
|
||||||
|
response = self._session.post(
|
||||||
|
self.api_base_url + "/v1/token",
|
||||||
|
headers={"Authorization": "Bearer %s" % token},
|
||||||
|
data={"password": password, "regenerate": 1 if regenerate else 0},
|
||||||
|
)
|
||||||
|
return self.raise_error_from_response(response).get("auth_token")
|
||||||
|
|
||||||
|
def forgot_password(self, username):
|
||||||
|
response = self._session.post(
|
||||||
|
self.api_base_url + "/v1/forgot", data={"username": username},
|
||||||
|
)
|
||||||
|
return self.raise_error_from_response(response).get("auth_token")
|
||||||
|
|
||||||
|
def get_profile(self):
|
||||||
|
try:
|
||||||
|
token = self.fetch_authentication_token()
|
||||||
|
except: # pylint:disable=bare-except
|
||||||
|
raise exception.AccountNotAuthenticated()
|
||||||
|
response = self._session.get(
|
||||||
|
self.api_base_url + "/v1/profile",
|
||||||
|
headers={"Authorization": "Bearer %s" % token},
|
||||||
|
)
|
||||||
|
return self.raise_error_from_response(response)
|
||||||
|
|
||||||
|
def update_profile(self, profile, current_password):
|
||||||
|
try:
|
||||||
|
token = self.fetch_authentication_token()
|
||||||
|
except: # pylint:disable=bare-except
|
||||||
|
raise exception.AccountNotAuthenticated()
|
||||||
|
profile["current_password"] = current_password
|
||||||
|
response = self._session.put(
|
||||||
|
self.api_base_url + "/v1/profile",
|
||||||
|
headers={"Authorization": "Bearer %s" % token},
|
||||||
|
data=profile,
|
||||||
|
)
|
||||||
|
return self.raise_error_from_response(response)
|
||||||
|
|
||||||
|
def get_account_info(self, offline):
|
||||||
|
if offline:
|
||||||
|
account = app.get_state_item("account")
|
||||||
|
if not account:
|
||||||
|
raise exception.AccountNotAuthenticated()
|
||||||
|
return {
|
||||||
|
"profile": {
|
||||||
|
"email": account.get("email"),
|
||||||
|
"username": account.get("username"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
token = self.fetch_authentication_token()
|
||||||
|
except: # pylint:disable=bare-except
|
||||||
|
raise exception.AccountNotAuthenticated()
|
||||||
|
response = self._session.get(
|
||||||
|
self.api_base_url + "/v1/summary",
|
||||||
|
headers={"Authorization": "Bearer %s" % token},
|
||||||
|
)
|
||||||
|
return self.raise_error_from_response(response)
|
||||||
|
|
||||||
|
def fetch_authentication_token(self):
|
||||||
|
if "PLATFORMIO_AUTH_TOKEN" in os.environ:
|
||||||
|
return os.environ["PLATFORMIO_AUTH_TOKEN"]
|
||||||
|
auth = app.get_state_item("account", {}).get("auth", {})
|
||||||
|
if auth.get("access_token") and auth.get("access_token_expire"):
|
||||||
|
if auth.get("access_token_expire") > time.time():
|
||||||
|
return auth.get("access_token")
|
||||||
|
if auth.get("refresh_token"):
|
||||||
|
response = self._session.post(
|
||||||
|
self.api_base_url + "/v1/login",
|
||||||
|
headers={"Authorization": "Bearer %s" % auth.get("refresh_token")},
|
||||||
|
)
|
||||||
|
result = self.raise_error_from_response(response)
|
||||||
|
app.set_state_item("account", result)
|
||||||
|
return result.get("auth").get("access_token")
|
||||||
|
raise exception.AccountNotAuthenticated()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_refresh_token():
|
||||||
|
try:
|
||||||
|
auth = app.get_state_item("account").get("auth").get("refresh_token")
|
||||||
|
return auth
|
||||||
|
except: # pylint:disable=bare-except
|
||||||
|
raise exception.AccountNotAuthenticated()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def raise_error_from_response(response, expected_codes=(200, 201, 202)):
|
||||||
|
if response.status_code in expected_codes:
|
||||||
|
try:
|
||||||
|
return response.json()
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
message = response.json()["message"]
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
message = response.text
|
||||||
|
if "Authorization session has been expired" in message:
|
||||||
|
app.delete_state_item("account")
|
||||||
|
raise exception.AccountError(message)
|
278
platformio/commands/account/command.py
Normal file
278
platformio/commands/account/command.py
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
# Copyright (c) 2014-present PlatformIO <contact@platformio.org>
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
import click
|
||||||
|
from tabulate import tabulate
|
||||||
|
|
||||||
|
from platformio.commands.account import exception
|
||||||
|
from platformio.commands.account.client import AccountClient
|
||||||
|
|
||||||
|
|
||||||
|
@click.group("account", short_help="Manage PIO Account")
|
||||||
|
def cli():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def validate_username(value):
|
||||||
|
value = str(value).strip()
|
||||||
|
if not re.match(r"^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){3,38}$", value, flags=re.I):
|
||||||
|
raise click.BadParameter(
|
||||||
|
"Invalid username format. "
|
||||||
|
"Username must contain at least 4 characters including single hyphens,"
|
||||||
|
" and cannot begin or end with a hyphen"
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def validate_email(value):
|
||||||
|
value = str(value).strip()
|
||||||
|
if not re.match(r"^[a-z\d_.+-]+@[a-z\d\-]+\.[a-z\d\-.]+$", value, flags=re.I):
|
||||||
|
raise click.BadParameter("Invalid E-Mail address")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def validate_password(value):
|
||||||
|
value = str(value).strip()
|
||||||
|
if not re.match(r"^(?=.*[a-z])(?=.*\d).{8,}$", value):
|
||||||
|
raise click.BadParameter(
|
||||||
|
"Invalid password format. "
|
||||||
|
"Password must contain at least 8 characters"
|
||||||
|
" including a number and a lowercase letter"
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("register", short_help="Create new PIO Account")
|
||||||
|
@click.option(
|
||||||
|
"-u",
|
||||||
|
"--username",
|
||||||
|
prompt=True,
|
||||||
|
callback=lambda _, __, value: validate_username(value),
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"-e", "--email", prompt=True, callback=lambda _, __, value: validate_email(value)
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"-p",
|
||||||
|
"--password",
|
||||||
|
prompt=True,
|
||||||
|
hide_input=True,
|
||||||
|
confirmation_prompt=True,
|
||||||
|
callback=lambda _, __, value: validate_password(value),
|
||||||
|
)
|
||||||
|
@click.option("--firstname", prompt=True)
|
||||||
|
@click.option("--lastname", prompt=True)
|
||||||
|
def account_register(username, email, password, firstname, lastname):
|
||||||
|
client = AccountClient()
|
||||||
|
client.registration(username, email, password, firstname, lastname)
|
||||||
|
return click.secho(
|
||||||
|
"An account has been successfully created. "
|
||||||
|
"Please check your mail to activate your account and verify your email address.",
|
||||||
|
fg="green",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("login", short_help="Log in to PIO Account")
|
||||||
|
@click.option("-u", "--username", prompt="Username or e-mail")
|
||||||
|
@click.option("-p", "--password", prompt=True, hide_input=True)
|
||||||
|
def account_login(username, password):
|
||||||
|
client = AccountClient()
|
||||||
|
client.login(username, password)
|
||||||
|
return click.secho("Successfully logged in!", fg="green")
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("logout", short_help="Log out of PIO Account")
|
||||||
|
def account_logout():
|
||||||
|
client = AccountClient()
|
||||||
|
client.logout()
|
||||||
|
return click.secho("Successfully logged out!", fg="green")
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("password", short_help="Change password")
|
||||||
|
@click.option("--old-password", prompt=True, hide_input=True)
|
||||||
|
@click.option("--new-password", prompt=True, hide_input=True, confirmation_prompt=True)
|
||||||
|
def account_password(old_password, new_password):
|
||||||
|
client = AccountClient()
|
||||||
|
client.change_password(old_password, new_password)
|
||||||
|
return click.secho("Password successfully changed!", fg="green")
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("token", short_help="Get or regenerate Authentication Token")
|
||||||
|
@click.option("-p", "--password", prompt=True, hide_input=True)
|
||||||
|
@click.option("--regenerate", is_flag=True)
|
||||||
|
@click.option("--json-output", is_flag=True)
|
||||||
|
def account_token(password, regenerate, json_output):
|
||||||
|
client = AccountClient()
|
||||||
|
auth_token = client.auth_token(password, regenerate)
|
||||||
|
if json_output:
|
||||||
|
return click.echo(json.dumps({"status": "success", "result": auth_token}))
|
||||||
|
return click.secho("Personal Authentication Token: %s" % auth_token, fg="green")
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("forgot", short_help="Forgot password")
|
||||||
|
@click.option("--username", prompt="Username or e-mail")
|
||||||
|
def account_forgot(username):
|
||||||
|
client = AccountClient()
|
||||||
|
client.forgot_password(username)
|
||||||
|
return click.secho(
|
||||||
|
"If this account is registered, we will send the "
|
||||||
|
"further instructions to your E-Mail.",
|
||||||
|
fg="green",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("update", short_help="Update profile information")
|
||||||
|
@click.option("--current-password", prompt=True, hide_input=True)
|
||||||
|
@click.option("--username")
|
||||||
|
@click.option("--email")
|
||||||
|
@click.option("--firstname")
|
||||||
|
@click.option("--lastname")
|
||||||
|
def account_update(current_password, **kwargs):
|
||||||
|
client = AccountClient()
|
||||||
|
profile = client.get_profile()
|
||||||
|
new_profile = profile.copy()
|
||||||
|
if not any(kwargs.values()):
|
||||||
|
for field in profile:
|
||||||
|
new_profile[field] = click.prompt(
|
||||||
|
field.replace("_", " ").capitalize(), default=profile[field]
|
||||||
|
)
|
||||||
|
if field == "email":
|
||||||
|
validate_email(new_profile[field])
|
||||||
|
if field == "username":
|
||||||
|
validate_username(new_profile[field])
|
||||||
|
else:
|
||||||
|
new_profile.update({key: value for key, value in kwargs.items() if value})
|
||||||
|
client.update_profile(new_profile, current_password)
|
||||||
|
click.secho("Profile successfully updated!", fg="green")
|
||||||
|
username_changed = new_profile["username"] != profile["username"]
|
||||||
|
email_changed = new_profile["email"] != profile["email"]
|
||||||
|
if not username_changed and not email_changed:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
client.logout()
|
||||||
|
except exception.AccountNotAuthenticated:
|
||||||
|
pass
|
||||||
|
if email_changed:
|
||||||
|
return click.secho(
|
||||||
|
"Please check your mail to verify your new email address and re-login. ",
|
||||||
|
fg="yellow",
|
||||||
|
)
|
||||||
|
return click.secho("Please re-login.", fg="yellow")
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("show", short_help="PIO Account information")
|
||||||
|
@click.option("--offline", is_flag=True)
|
||||||
|
@click.option("--json-output", is_flag=True)
|
||||||
|
def account_show(offline, json_output):
|
||||||
|
client = AccountClient()
|
||||||
|
info = client.get_account_info(offline)
|
||||||
|
if json_output:
|
||||||
|
return click.echo(json.dumps(info))
|
||||||
|
click.echo()
|
||||||
|
if info.get("profile"):
|
||||||
|
print_profile(info["profile"])
|
||||||
|
if info.get("packages"):
|
||||||
|
print_packages(info["packages"])
|
||||||
|
if info.get("subscriptions"):
|
||||||
|
print_subscriptions(info["subscriptions"])
|
||||||
|
return click.echo()
|
||||||
|
|
||||||
|
|
||||||
|
def print_profile(profile):
|
||||||
|
click.secho("Profile", fg="cyan", bold=True)
|
||||||
|
click.echo("=" * len("Profile"))
|
||||||
|
data = []
|
||||||
|
if profile.get("username"):
|
||||||
|
data.append(("Username:", profile["username"]))
|
||||||
|
if profile.get("email"):
|
||||||
|
data.append(("Email:", profile["email"]))
|
||||||
|
if profile.get("firstname"):
|
||||||
|
data.append(("First name:", profile["firstname"]))
|
||||||
|
if profile.get("lastname"):
|
||||||
|
data.append(("Last name:", profile["lastname"]))
|
||||||
|
click.echo(tabulate(data, tablefmt="plain"))
|
||||||
|
|
||||||
|
|
||||||
|
def print_packages(packages):
|
||||||
|
click.echo()
|
||||||
|
click.secho("Packages", fg="cyan")
|
||||||
|
click.echo("=" * len("Packages"))
|
||||||
|
for package in packages:
|
||||||
|
click.echo()
|
||||||
|
click.secho(package.get("name"), bold=True)
|
||||||
|
click.echo("-" * len(package.get("name")))
|
||||||
|
if package.get("description"):
|
||||||
|
click.echo(package.get("description"))
|
||||||
|
data = []
|
||||||
|
expire = "-"
|
||||||
|
if "subscription" in package:
|
||||||
|
expire = datetime.datetime.strptime(
|
||||||
|
(
|
||||||
|
package["subscription"].get("end_at")
|
||||||
|
or package["subscription"].get("next_bill_at")
|
||||||
|
),
|
||||||
|
"%Y-%m-%dT%H:%M:%SZ",
|
||||||
|
).strftime("%Y-%m-%d")
|
||||||
|
data.append(("Expire:", expire))
|
||||||
|
services = []
|
||||||
|
for key in package:
|
||||||
|
if not key.startswith("service."):
|
||||||
|
continue
|
||||||
|
if isinstance(package[key], dict):
|
||||||
|
services.append(package[key].get("title"))
|
||||||
|
else:
|
||||||
|
services.append(package[key])
|
||||||
|
if services:
|
||||||
|
data.append(("Services:", ", ".join(services)))
|
||||||
|
click.echo(tabulate(data, tablefmt="plain"))
|
||||||
|
|
||||||
|
|
||||||
|
def print_subscriptions(subscriptions):
|
||||||
|
click.echo()
|
||||||
|
click.secho("Subscriptions", fg="cyan")
|
||||||
|
click.echo("=" * len("Subscriptions"))
|
||||||
|
for subscription in subscriptions:
|
||||||
|
click.echo()
|
||||||
|
click.secho(subscription.get("product_name"), bold=True)
|
||||||
|
click.echo("-" * len(subscription.get("product_name")))
|
||||||
|
data = [("State:", subscription.get("status"))]
|
||||||
|
begin_at = datetime.datetime.strptime(
|
||||||
|
subscription.get("begin_at"), "%Y-%m-%dT%H:%M:%SZ"
|
||||||
|
).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
data.append(("Start date:", begin_at or "-"))
|
||||||
|
end_at = subscription.get("end_at")
|
||||||
|
if end_at:
|
||||||
|
end_at = datetime.datetime.strptime(
|
||||||
|
subscription.get("end_at"), "%Y-%m-%dT%H:%M:%SZ"
|
||||||
|
).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
data.append(("End date:", end_at or "-"))
|
||||||
|
next_bill_at = subscription.get("next_bill_at")
|
||||||
|
if next_bill_at:
|
||||||
|
next_bill_at = datetime.datetime.strptime(
|
||||||
|
subscription.get("next_bill_at"), "%Y-%m-%dT%H:%M:%SZ"
|
||||||
|
).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
data.append(("Next payment:", next_bill_at or "-"))
|
||||||
|
data.append(
|
||||||
|
("Edit:", click.style(subscription.get("update_url"), fg="blue") or "-")
|
||||||
|
)
|
||||||
|
data.append(
|
||||||
|
("Cancel:", click.style(subscription.get("cancel_url"), fg="blue") or "-")
|
||||||
|
)
|
||||||
|
click.echo(tabulate(data, tablefmt="plain"))
|
30
platformio/commands/account/exception.py
Normal file
30
platformio/commands/account/exception.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Copyright (c) 2014-present PlatformIO <contact@platformio.org>
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from platformio.exception import PlatformioException
|
||||||
|
|
||||||
|
|
||||||
|
class AccountError(PlatformioException):
|
||||||
|
|
||||||
|
MESSAGE = "{0}"
|
||||||
|
|
||||||
|
|
||||||
|
class AccountNotAuthenticated(AccountError):
|
||||||
|
|
||||||
|
MESSAGE = "You are not authenticated! Please login to PIO Account."
|
||||||
|
|
||||||
|
|
||||||
|
class AccountAlreadyAuthenticated(AccountError):
|
||||||
|
|
||||||
|
MESSAGE = "You are already authenticated with {0} account."
|
@ -96,7 +96,7 @@ class PIOCoreRPC(object):
|
|||||||
to_json = "--json-output" in args
|
to_json = "--json-output" in args
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if args and args[0] in ("account", "remote"):
|
if args and args[0] == "remote":
|
||||||
result = yield PIOCoreRPC._call_subprocess(args, options)
|
result = yield PIOCoreRPC._call_subprocess(args, options)
|
||||||
defer.returnValue(PIOCoreRPC._process_result(result, to_json))
|
defer.returnValue(PIOCoreRPC._process_result(result, to_json))
|
||||||
else:
|
else:
|
||||||
|
Reference in New Issue
Block a user