Skip to content

Commit

Permalink
Merge pull request #4 from stepanzubkov/dev
Browse files Browse the repository at this point in the history
Version 0.6.0: Config view, debug, url list
  • Loading branch information
stepanzubkov authored May 6, 2023
2 parents 9ff0bd5 + ae00f15 commit 6a3504b
Show file tree
Hide file tree
Showing 13 changed files with 292 additions and 58 deletions.
3 changes: 2 additions & 1 deletion florgon_cc_cli/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
from .login import login
from .logout import logout
from .host import host
from .config import config

__all__ = ["url", "login", "logout", "host"]
__all__ = ["url", "login", "logout", "host", "config"]
40 changes: 40 additions & 0 deletions florgon_cc_cli/commands/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""
Command for working with user config
"""
import click

import florgon_cc_cli.config
from florgon_cc_cli.services.config import deserialize_config


@click.group()
def config():
"""
Work with user config.
"""


@config.command()
@click.option(
"-r", "--raw", is_flag=True, default=False, help="Prints config 'as is', in toml format."
)
def show(raw: bool):
"""
Prints user config.
"""
if raw:
with open(florgon_cc_cli.config.CONFIG_FILE, "r") as f:
click.echo(f.read())
return

user_config = deserialize_config()
for key, value in user_config.items():
click.echo(click.style(f"{key:20}", fg="green") + f"{value}")


@config.command()
def show_path():
"""
Prints path of config file.
"""
click.echo(str(florgon_cc_cli.config.CONFIG_FILE))
55 changes: 45 additions & 10 deletions florgon_cc_cli/commands/url.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@
from datetime import datetime

import click
from florgon_cc_cli.services.config import get_value_from_config

from florgon_cc_cli.services.config import get_value_from_config
from florgon_cc_cli.services.url import (
build_open_url,
create_url,
extract_hash_from_short_url,
get_url_info_by_hash,
get_url_stats_by_hash,
get_urls_list,
request_hash_from_urls_list,
)


Expand Down Expand Up @@ -66,10 +68,15 @@ def create(


@url.command()
@click.argument("short_url", type=str)
@click.option("-s", "--short-url", type=str, help="Short url.")
def info(short_url: str):
"""Prints main information about short url."""
short_url_hash = extract_hash_from_short_url(short_url)
if short_url:
short_url_hash = extract_hash_from_short_url(short_url)
else:
click.echo("Short url is not specified, requesting for list of your urls.")
short_url_hash = request_hash_from_urls_list()

success, response = get_url_info_by_hash(short_url_hash)
if not success:
click.secho(response["message"], err=True, fg="red")
Expand All @@ -83,7 +90,7 @@ def info(short_url: str):


@url.command()
@click.argument("short_url", type=str)
@click.option("-s", "--short-url", type=str, help="Short url.")
@click.option(
"-r",
"--referers-as",
Expand All @@ -100,7 +107,12 @@ def info(short_url: str):
)
def stats(short_url: str, referers_as: str, dates_as: str):
"""Prints url views statistics."""
short_url_hash = extract_hash_from_short_url(short_url)
if short_url:
short_url_hash = extract_hash_from_short_url(short_url)
else:
click.echo("Short url is not specified, requesting for list of your urls.")
short_url_hash = request_hash_from_urls_list()

success, response = get_url_stats_by_hash(
short_url_hash,
url_views_by_referers_as=referers_as,
Expand All @@ -111,15 +123,38 @@ def stats(short_url: str, referers_as: str, dates_as: str):
click.secho(response["message"], err=True, fg="red")
return

click.echo("Total views: " + click.style(response['total'], fg="green"))
click.echo("Total views: " + click.style(response["total"], fg="green"))
click.echo("Views by referers:")
if response.get("by_referers"):
for referer in response["by_referers"]:
click.echo(f"\t{referer} - {response['by_referers'][referer]}"
+ "%" * int(referers_as == "percent"))
click.echo(
f"\t{referer} - {response['by_referers'][referer]}"
+ "%" * int(referers_as == "percent")
)

click.echo("Views by dates:")
if response.get("by_dates"):
for date in response["by_dates"]:
click.echo(f"\t{date} - {response['by_dates'][date]}"
+ "%" * int(dates_as == "percent"))
click.echo(
f"\t{date} - {response['by_dates'][date]}" + "%" * int(dates_as == "percent")
)


@url.command()
def list():
"""
Prints list of short urls created by user. Auth required.
"""
success, response = get_urls_list(access_token=get_value_from_config("access_token"))
if not success:
click.secho(response["message"], err=True, fg="red")
return

click.echo("Your urls:")
for url in response:
if url["is_expired"]:
click.secho(
f"{build_open_url(url['hash'])} - {url['redirect_url']} (expired)", fg="red"
)
else:
click.echo(f"{build_open_url(url['hash'])} - {url['redirect_url']}")
14 changes: 12 additions & 2 deletions florgon_cc_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,28 @@
"""
import click

from florgon_cc_cli.commands import url, login, logout, host
from florgon_cc_cli.commands import url, login, logout, host, config


@click.group()
def main():
@click.option(
"-D",
"--debug",
is_flag=True,
default=False,
help="Enable debug features like printing api responses.",
)
@click.pass_context
def main(ctx: click.Context, debug: bool):
ctx.obj = {"DEBUG": debug}
"""Florgon CC CLI - url shortener and paste manager."""


main.add_command(url)
main.add_command(login)
main.add_command(logout)
main.add_command(host)
main.add_command(config)

if __name__ == "__main__":
main()
10 changes: 10 additions & 0 deletions florgon_cc_cli/models/error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
Error model from CC API response.
"""
from typing import TypedDict


class Error(TypedDict):
message: str
code: int
status: int
36 changes: 36 additions & 0 deletions florgon_cc_cli/models/url.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""
Url TypedDict model from CC API responses.
"""
from typing import TypedDict, Optional


class UrlLink(TypedDict):
"""
Single url link.
"""

href: str


class UrlLinks(TypedDict):
"""
Url links in url field `_links`.
"""

qr: UrlLink
stats: Optional[UrlLink]


class Url(TypedDict):
"""
Url model from API.
"""

id: int
redirect_url: str
hash: str
expires_at: float
is_expired: bool
stats_is_public: bool
is_deleted: bool
_links: UrlLinks
2 changes: 2 additions & 0 deletions florgon_cc_cli/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
click
pick
22 changes: 17 additions & 5 deletions florgon_cc_cli/services/api.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""
Services for working with Florgon CC Api.
"""
from typing import Any, Dict, Optional
from typing import Any, Dict, Optional, NoReturn, Union

import requests
import click

import florgon_cc_cli.config as config
from florgon_cc_cli.services.config import get_value_from_config
Expand All @@ -16,7 +17,7 @@ def execute_api_method(
data: Dict[str, Any] = {},
params: Dict[str, Any] = {},
access_token: Optional[str] = None,
) -> Dict[str, Any]:
) -> Dict[str, Any] | NoReturn:
"""
Executes API method.
:param str http_method: GET, POST, PUT, PATCH, DELETE or OPTIONS
Expand All @@ -25,7 +26,8 @@ def execute_api_method(
:param Dict[str, Any] params: GET data
:param Optional[str] access_token: Florgon OAuth token
:rtype: Dict[str, Any]
:return: JSON response from API
:rtype: NoReturn
:return: JSON response from API or exit application
"""
request_url = f"{get_api_host()}/{api_method}"
response = requests.request(
Expand All @@ -35,7 +37,18 @@ def execute_api_method(
params=params,
headers={"Authorization": access_token} if access_token else {},
)
return response.json()
ctx = click.get_current_context()
if ctx.obj["DEBUG"]:
click.secho(
f"API response from {request_url} with HTTP code {response.status_code}:", fg="yellow"
)
click.echo(response.text)

try:
return response.json()
except requests.exceptions.JSONDecodeError:
click.secho("Unable to decode API response as JSON!", fg="red", err=True)
ctx.exit(1)


def get_api_host() -> str:
Expand All @@ -45,4 +58,3 @@ def get_api_host() -> str:
:return: API host
"""
return get_value_from_config("api_host") or config.CC_API_URL

43 changes: 27 additions & 16 deletions florgon_cc_cli/services/config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""
Services for working with user config.
"""
from typing import Any
from typing import Any, Dict
import toml

from florgon_cc_cli import config
Expand All @@ -14,11 +14,8 @@ def save_value_to_config(key: str, value: Any) -> None:
:param Any value: value to save.
:rtype: None
"""
config.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
config.CONFIG_FILE.touch(exist_ok=True)

with open(config.CONFIG_FILE, "r") as f:
user_config = toml.load(f)
create_config_file()
user_config = deserialize_config()

user_config[key] = value
with open(config.CONFIG_FILE, "w") as f:
Expand All @@ -31,28 +28,42 @@ def get_value_from_config(key: str) -> Any:
:param str key: key for value
:rtype: Any
"""
config.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
config.CONFIG_FILE.touch(exist_ok=True)

with open(config.CONFIG_FILE, "r") as f:
user_config = toml.load(f)
create_config_file()
user_config = deserialize_config()

return user_config.get(key)


def delete_value_from_config(key: str) -> None:
"""
Deletes value from config by key.
:param str key: key for value
:rtype: None
"""
config.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
config.CONFIG_FILE.touch(exist_ok=True)

with open(config.CONFIG_FILE, "r") as f:
user_config = toml.load(f)
create_config_file()
user_config = deserialize_config()

if key in user_config:
user_config.pop(key)

with open(config.CONFIG_FILE, "w") as f:
toml.dump(user_config, f)


def deserialize_config() -> Dict[str, Any]:
"""
Deserializes config and returns dict.
:rtype: Dict[str, Any]
:return: user config
"""
with open(config.CONFIG_FILE, "r") as f:
return toml.load(f)


def create_config_file() -> None:
"""
Creates empty config dir and config file.
:rtype: None
"""
config.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
config.CONFIG_FILE.touch(exist_ok=True)
6 changes: 4 additions & 2 deletions florgon_cc_cli/services/oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ def build_sso_login_url(
Defaults to "https://florgon.com/oauth/blank"
:param str oauth_screen_url: Login page url. Defaults to "https://florgon.com/oauth/authorize"
"""
return (f"{oauth_screen_url}?client_id={client_id}&redirect_uri={redirect_uri}"
f"&scope={scope}&response_type={response_type}")
return (
f"{oauth_screen_url}?client_id={client_id}&redirect_uri={redirect_uri}"
f"&scope={scope}&response_type={response_type}"
)


def extract_token_from_redirect_uri(redirect_uri: str) -> Optional[str]:
Expand Down
Loading

0 comments on commit 6a3504b

Please sign in to comment.