Skip to content

Commit

Permalink
Merge pull request #15 from afonsoc12/develop
Browse files Browse the repository at this point in the history
develop
  • Loading branch information
afonsoc12 authored Apr 8, 2023
2 parents 7abe45c + fd3047d commit f948117
Show file tree
Hide file tree
Showing 26 changed files with 804 additions and 203 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

All notable changes to this project will be documented in this file.

## v0.2.1 (2023-04-08)

### New
- Autocomplete is now supported when adding transactions

### Changed
- Docker image runs with non-root user

### Fixes
- [#14] Wrong space-parsing when using oneline

## v0.1.2 (2022-02-28)

### New
Expand Down
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
ARG DOCKER_IMAGE=python:3.9-alpine \
USER=nonroot \
GROUP=nonroot \
USER=coolio \
GROUP=coolio \
UID=1234 \
GID=4321

Expand Down Expand Up @@ -38,7 +38,7 @@ RUN mkdir -p /home $XDG_CONFIG_HOME \
&& chown -R $USER:$GROUP /home $XDG_CONFIG_HOME \
&& rm -rf /tmp/* /var/{cache,log}/* /var/lib/apt/lists/*

USER nonroot
USER $USER
WORKDIR /home

COPY --from=install-image --chown=$USER:$GROUP /root/.local /home/.local
Expand Down
21 changes: 10 additions & 11 deletions firefly_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import sys

from firefly_cli._version import get_versions
from firefly_cli.api import FireflyAPI
from firefly_cli.cli import FireflyPrompt
from firefly_cli.parser import Parser
from firefly_cli.transaction import Transaction

__version__ = get_versions()["version"]
del get_versions


def _real_main():

if len(sys.argv) > 1:
args, ffargs = Parser.entrypoint().parse_known_args()

try:
if args.version:
FireflyPrompt().onecmd("version")
elif args.help:
FireflyPrompt().onecmd("help")
else:
FireflyPrompt().onecmd(" ".join(ffargs))
except Exception:
raise
if args.version:
FireflyPrompt().onecmd("version")
elif args.help:
FireflyPrompt().onecmd("help")
else:
FireflyPrompt().onecmd(Parser.safe_string(ffargs))
else:
FireflyPrompt().cmdloop()

Expand All @@ -30,7 +30,6 @@ def main():
_real_main()
except KeyboardInterrupt:
print("\nInterrupted by user")
except:
except Exception:
# Silence raising exceptions
raise
# pass
58 changes: 31 additions & 27 deletions firefly_cli/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,7 @@ def __init__(self, hostname, auth_token, check_connection=True):
else hostname[:-1]
) # Remove trailing backslash
self.api_url = self.hostname + "/api/v1/" if hostname else self.hostname
self.api_test = self._test_api() if check_connection else False

def _test_api(self):
"""Tests API connection."""
try:
_ = self.get_about_user()
return True
except:
return False
self.api_test = self.test_api_connection() if check_connection else False

def _post(self, endpoint, payload):
"""Handles general POST requests."""
Expand All @@ -51,22 +43,31 @@ def _post(self, endpoint, payload):

return response

def _get(self, endpoint, params={}, cache=False):
def _get(self, endpoint, params={}, cache=False, request_raw=False, timeout=2):
"""Handles general GET requests."""

with self.rc.cache_disabled() if not cache else nullcontext():
response = self.rc.get(
"{}{}".format(self.api_url, endpoint),
params=params,
headers=self.headers,
timeout=timeout,
)

return response.json()
return response.json() if not request_raw else response

def get_budgets(self):
"""Returns budgets of the user."""
def test_api_connection(self):
"""Tests API connection."""
try:
_ = self.get_about_user()
return True
except:
return False

return self._get("budgets")
def get_about_user(self):
"""Returns user information."""

return self._get("about/user")

def get_accounts(
self, account_type="asset", cache=False, pagination=False, limit=None
Expand Down Expand Up @@ -96,6 +97,11 @@ def get_accounts(

return pages

def get_budgets(self):
"""Returns budgets of the user."""

return self._get("budgets")

def get_autocomplete_accounts(self, limit=20):
"""Returns all user accounts."""
acc_data = self.get_accounts(
Expand All @@ -107,15 +113,8 @@ def get_autocomplete_accounts(self, limit=20):

return account_names

def get_about_user(self):
"""Returns user information."""

return self._get("about/user")

def create_transaction(self, transaction: Transaction):
"""Creates a new transaction.
data:
pd.DataFrame
`Amount, Description, Source account, Destination account, Category, Budget`
Example:
Expand All @@ -127,18 +126,23 @@ def create_transaction(self, transaction: Transaction):
-> `5, Large Mocha, Cash, , , UCO Bank`
"""

trans_data = transaction.to_dict(remove_none=True, api_safe=True)
payload = self.payload_formatter(transaction)

header = {k: v for k, v in trans_data.items() if k.startswith("header__")}
return self._post(endpoint="transactions", payload=payload)

def payload_formatter(self, transaction):
trans_data = transaction.to_dict(remove_none=True, api_safe=True)
header = {
k.replace("header__", ""): v
for k, v in trans_data.items()
if k.startswith("header__")
}
body = {
"transactions": [
{k: v for k, v in trans_data.items() if not k.startswith("header__")}
]
}

payload = {**header, **body}

return self._post(endpoint="transactions", payload=payload)
return {**header, **body}

@staticmethod
def refresh_api(configs):
Expand Down
15 changes: 8 additions & 7 deletions firefly_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def do_edit(self, argslist):

def help_edit(self):
self.poutput(
"Edits connection credentials:\n\t> edit url http://<FireflyIII URL>:<Port>\n\t> edit api <API key>"
"Edits connection credentials:\n\t> edit url https://<FireflyIII URL>:<Port>\n\t> edit api <API key>"
)

@cmd2.with_argparser(Parser.accounts())
Expand Down Expand Up @@ -149,9 +149,11 @@ def do_add(self, parser):

tab_header, tab_body = trans.get_tabulates()
self.poutput(f"Transaction header:\n{tab_header}\n")
self.poutput(f"Transaction Body:\n{tab_body}\n")
self.poutput(f"Transaction body:\n{tab_body}\n")

if prompt_continue(extra_line=False, extra_msg=" adding the transaction"):
if parser.bypass_prompt or prompt_continue(
extra_line=False, extra_msg=" adding the transaction"
):
try:
response = self.api.create_transaction(trans)

Expand All @@ -176,12 +178,11 @@ def do_exit(self, _):
return True

def help_exit(self):
return self.poutput("exit the application. Shorthand: x q Ctrl-D.")
return self.poutput("Exits the application. Shorthand: q Ctrl-D.")

def default(self):
self.poutput(
'Input not recognised. Please type "help" to list the available commands'
)
# todo default when arg not recognised
pass

do_q = do_exit
help_q = help_exit
Expand Down
28 changes: 17 additions & 11 deletions firefly_cli/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,29 @@

from xdg.BaseDirectory import xdg_config_home

if os.getenv("FIREFLY_CLI_CONFIG"):
config_file_path = Path(os.environ["FIREFLY_CLI_CONFIG"])
else:
config_file_path = Path(xdg_config_home).joinpath("firefly-cli", "firefly-cli.ini")

# Create dir if not exists
config_file_path.parent.mkdir(parents=True, exist_ok=True)
def config_file_path():
if os.getenv("FIREFLY_CLI_CONFIG"):
config_file_path = Path(os.environ["FIREFLY_CLI_CONFIG"])
else:
config_file_path = Path(xdg_config_home).joinpath(
"firefly-cli", "firefly-cli.ini"
)

# Create dir if not exists
config_file_path.parent.mkdir(parents=True, exist_ok=True)

return config_file_path


def load_configs():
configs = configparser.ConfigParser()

# No config file loaded because it was not available/not existent
if len(configs.read(config_file_path)) < 1:
if len(configs.read(config_file_path())) < 1:
print("File not found, creating the file..")

with open(config_file_path, "w") as f:
with open(config_file_path(), "w") as f:
configs["firefly-cli"] = {}
save_configs_to_file(configs)

Expand All @@ -29,12 +35,12 @@ def load_configs():

def save_configs_to_file(configs):
try:
with open(config_file_path, "w") as f:
with open(config_file_path(), "w") as f:
configs.write(f)
print("Config file saved at {}".format(str(config_file_path)))
print("Config file saved at {}".format(str(config_file_path())))
except:
print(
"An error has occurred while saving file to {}".format(
str(config_file_path)
str(config_file_path())
)
)
29 changes: 22 additions & 7 deletions firefly_cli/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ def description_provider(cmd, limit=10):

class Parser:
@staticmethod
def entrypoint():
def entrypoint(prog=None):
parser = ArgumentParser(
description="A command line interface for conveniently entering expenses in Firefly III.\nRun without arguments to start interactive mode.",
usage="firefly-cli [-h] [-v]",
add_help=False,
prog=None,
)

# Optional arguments (json header)
Expand All @@ -50,10 +51,8 @@ def entrypoint():
return parser

@staticmethod
def accounts():
parser = Cmd2ArgumentParser(
description="Shows account information.",
)
def accounts(prog=None):
parser = Cmd2ArgumentParser(description="Shows account information.", prog=prog)

# Optional arguments (json header)
parser.add_argument("--json", action="store_true")
Expand All @@ -79,17 +78,28 @@ def accounts():
return parser

@staticmethod
def add():
def add(prog=None):

parser = Cmd2ArgumentParser(
description="Adds a new transaction to FireflyIII.",
usage="add [comma-separated arguments] [-h] [--optional-arguments]",
prog=None,
)

# Positional arguments
parser.add_argument("transaction", nargs="*", help="Transaction data.")
parser.add_argument(
"transaction",
nargs="*",
help="Transaction data in comma-separated format: Amount, Description , Source account, Destination account, Category, Budget",
)

# Optional arguments (json header)
parser.add_argument(
"-y",
dest="bypass_prompt",
action="store_true",
help="Bypass confirmation prompt.",
)
parser.add_argument(
"--apply-rules",
default=True,
Expand Down Expand Up @@ -174,3 +184,8 @@ def add():
)

return parser

@staticmethod
def safe_string(args):
args_str = " ".join(list(map(lambda x: f"'{x}'" if " " in x else x, args)))
return args_str
4 changes: 4 additions & 0 deletions firefly_cli/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ def to_dict(

d = self.__dict__

# Remove argparse arguments not related to transaction
if "bypass_prompt" in d:
del d["bypass_prompt"]

if remove_none:
d = {k: v for k, v in d.items() if v is not None}

Expand Down
9 changes: 9 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,22 @@ cattrs==1.10.0
certifi==2022.6.15
charset-normalizer==2.0.12
cmd2==2.4.1
coverage==6.4.2
idna==3.3
iniconfig==1.1.1
packaging==21.3
pluggy==1.0.0
py==1.11.0
pyparsing==3.0.9
pyperclip==1.8.2
pytest==7.1.2
pyxdg==0.28
requests==2.28.0
requests-cache==0.9.4
requests-mock==1.9.3
six==1.16.0
tabulate==0.8.9
tomli==2.0.1
url-normalize==1.4.3
urllib3==1.26.9
wcwidth==0.2.5
6 changes: 5 additions & 1 deletion test/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import os
from test.utils import ensure_project_root

os.environ["XDG_CONFIG_HOME"] = "../.testout"
os.environ["FIREFLY_CLI_CONFIG"] = "test/test_data/firefly-cli-test.ini"

# Ensure tests are being run from root
ensure_project_root()
Loading

0 comments on commit f948117

Please sign in to comment.