Skip to content

Commit f948117

Browse files
authored
Merge pull request #15 from afonsoc12/develop
develop
2 parents 7abe45c + fd3047d commit f948117

26 files changed

+804
-203
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

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

5+
## v0.2.1 (2023-04-08)
6+
7+
### New
8+
- Autocomplete is now supported when adding transactions
9+
10+
### Changed
11+
- Docker image runs with non-root user
12+
13+
### Fixes
14+
- [#14] Wrong space-parsing when using oneline
15+
516
## v0.1.2 (2022-02-28)
617

718
### New

Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
ARG DOCKER_IMAGE=python:3.9-alpine \
2-
USER=nonroot \
3-
GROUP=nonroot \
2+
USER=coolio \
3+
GROUP=coolio \
44
UID=1234 \
55
GID=4321
66

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

41-
USER nonroot
41+
USER $USER
4242
WORKDIR /home
4343

4444
COPY --from=install-image --chown=$USER:$GROUP /root/.local /home/.local

firefly_cli/__init__.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
11
import sys
22

33
from firefly_cli._version import get_versions
4+
from firefly_cli.api import FireflyAPI
45
from firefly_cli.cli import FireflyPrompt
56
from firefly_cli.parser import Parser
7+
from firefly_cli.transaction import Transaction
68

79
__version__ = get_versions()["version"]
10+
del get_versions
811

912

1013
def _real_main():
1114

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

15-
try:
16-
if args.version:
17-
FireflyPrompt().onecmd("version")
18-
elif args.help:
19-
FireflyPrompt().onecmd("help")
20-
else:
21-
FireflyPrompt().onecmd(" ".join(ffargs))
22-
except Exception:
23-
raise
18+
if args.version:
19+
FireflyPrompt().onecmd("version")
20+
elif args.help:
21+
FireflyPrompt().onecmd("help")
22+
else:
23+
FireflyPrompt().onecmd(Parser.safe_string(ffargs))
2424
else:
2525
FireflyPrompt().cmdloop()
2626

@@ -30,7 +30,6 @@ def main():
3030
_real_main()
3131
except KeyboardInterrupt:
3232
print("\nInterrupted by user")
33-
except:
33+
except Exception:
3434
# Silence raising exceptions
3535
raise
36-
# pass

firefly_cli/api.py

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,7 @@ def __init__(self, hostname, auth_token, check_connection=True):
2626
else hostname[:-1]
2727
) # Remove trailing backslash
2828
self.api_url = self.hostname + "/api/v1/" if hostname else self.hostname
29-
self.api_test = self._test_api() if check_connection else False
30-
31-
def _test_api(self):
32-
"""Tests API connection."""
33-
try:
34-
_ = self.get_about_user()
35-
return True
36-
except:
37-
return False
29+
self.api_test = self.test_api_connection() if check_connection else False
3830

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

5244
return response
5345

54-
def _get(self, endpoint, params={}, cache=False):
46+
def _get(self, endpoint, params={}, cache=False, request_raw=False, timeout=2):
5547
"""Handles general GET requests."""
5648

5749
with self.rc.cache_disabled() if not cache else nullcontext():
5850
response = self.rc.get(
5951
"{}{}".format(self.api_url, endpoint),
6052
params=params,
6153
headers=self.headers,
54+
timeout=timeout,
6255
)
6356

64-
return response.json()
57+
return response.json() if not request_raw else response
6558

66-
def get_budgets(self):
67-
"""Returns budgets of the user."""
59+
def test_api_connection(self):
60+
"""Tests API connection."""
61+
try:
62+
_ = self.get_about_user()
63+
return True
64+
except:
65+
return False
6866

69-
return self._get("budgets")
67+
def get_about_user(self):
68+
"""Returns user information."""
69+
70+
return self._get("about/user")
7071

7172
def get_accounts(
7273
self, account_type="asset", cache=False, pagination=False, limit=None
@@ -96,6 +97,11 @@ def get_accounts(
9697

9798
return pages
9899

100+
def get_budgets(self):
101+
"""Returns budgets of the user."""
102+
103+
return self._get("budgets")
104+
99105
def get_autocomplete_accounts(self, limit=20):
100106
"""Returns all user accounts."""
101107
acc_data = self.get_accounts(
@@ -107,15 +113,8 @@ def get_autocomplete_accounts(self, limit=20):
107113

108114
return account_names
109115

110-
def get_about_user(self):
111-
"""Returns user information."""
112-
113-
return self._get("about/user")
114-
115116
def create_transaction(self, transaction: Transaction):
116117
"""Creates a new transaction.
117-
data:
118-
pd.DataFrame
119118
120119
`Amount, Description, Source account, Destination account, Category, Budget`
121120
Example:
@@ -127,18 +126,23 @@ def create_transaction(self, transaction: Transaction):
127126
-> `5, Large Mocha, Cash, , , UCO Bank`
128127
"""
129128

130-
trans_data = transaction.to_dict(remove_none=True, api_safe=True)
129+
payload = self.payload_formatter(transaction)
131130

132-
header = {k: v for k, v in trans_data.items() if k.startswith("header__")}
131+
return self._post(endpoint="transactions", payload=payload)
132+
133+
def payload_formatter(self, transaction):
134+
trans_data = transaction.to_dict(remove_none=True, api_safe=True)
135+
header = {
136+
k.replace("header__", ""): v
137+
for k, v in trans_data.items()
138+
if k.startswith("header__")
139+
}
133140
body = {
134141
"transactions": [
135142
{k: v for k, v in trans_data.items() if not k.startswith("header__")}
136143
]
137144
}
138-
139-
payload = {**header, **body}
140-
141-
return self._post(endpoint="transactions", payload=payload)
145+
return {**header, **body}
142146

143147
@staticmethod
144148
def refresh_api(configs):

firefly_cli/cli.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def do_edit(self, argslist):
113113

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

119119
@cmd2.with_argparser(Parser.accounts())
@@ -149,9 +149,11 @@ def do_add(self, parser):
149149

150150
tab_header, tab_body = trans.get_tabulates()
151151
self.poutput(f"Transaction header:\n{tab_header}\n")
152-
self.poutput(f"Transaction Body:\n{tab_body}\n")
152+
self.poutput(f"Transaction body:\n{tab_body}\n")
153153

154-
if prompt_continue(extra_line=False, extra_msg=" adding the transaction"):
154+
if parser.bypass_prompt or prompt_continue(
155+
extra_line=False, extra_msg=" adding the transaction"
156+
):
155157
try:
156158
response = self.api.create_transaction(trans)
157159

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

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

181183
def default(self):
182-
self.poutput(
183-
'Input not recognised. Please type "help" to list the available commands'
184-
)
184+
# todo default when arg not recognised
185+
pass
185186

186187
do_q = do_exit
187188
help_q = help_exit

firefly_cli/configs.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,29 @@
44

55
from xdg.BaseDirectory import xdg_config_home
66

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

12-
# Create dir if not exists
13-
config_file_path.parent.mkdir(parents=True, exist_ok=True)
8+
def config_file_path():
9+
if os.getenv("FIREFLY_CLI_CONFIG"):
10+
config_file_path = Path(os.environ["FIREFLY_CLI_CONFIG"])
11+
else:
12+
config_file_path = Path(xdg_config_home).joinpath(
13+
"firefly-cli", "firefly-cli.ini"
14+
)
15+
16+
# Create dir if not exists
17+
config_file_path.parent.mkdir(parents=True, exist_ok=True)
18+
19+
return config_file_path
1420

1521

1622
def load_configs():
1723
configs = configparser.ConfigParser()
1824

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

23-
with open(config_file_path, "w") as f:
29+
with open(config_file_path(), "w") as f:
2430
configs["firefly-cli"] = {}
2531
save_configs_to_file(configs)
2632

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

3036
def save_configs_to_file(configs):
3137
try:
32-
with open(config_file_path, "w") as f:
38+
with open(config_file_path(), "w") as f:
3339
configs.write(f)
34-
print("Config file saved at {}".format(str(config_file_path)))
40+
print("Config file saved at {}".format(str(config_file_path())))
3541
except:
3642
print(
3743
"An error has occurred while saving file to {}".format(
38-
str(config_file_path)
44+
str(config_file_path())
3945
)
4046
)

firefly_cli/parser.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@ def description_provider(cmd, limit=10):
2929

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

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

5253
@staticmethod
53-
def accounts():
54-
parser = Cmd2ArgumentParser(
55-
description="Shows account information.",
56-
)
54+
def accounts(prog=None):
55+
parser = Cmd2ArgumentParser(description="Shows account information.", prog=prog)
5756

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

8180
@staticmethod
82-
def add():
81+
def add(prog=None):
8382

8483
parser = Cmd2ArgumentParser(
8584
description="Adds a new transaction to FireflyIII.",
8685
usage="add [comma-separated arguments] [-h] [--optional-arguments]",
86+
prog=None,
8787
)
8888

8989
# Positional arguments
90-
parser.add_argument("transaction", nargs="*", help="Transaction data.")
90+
parser.add_argument(
91+
"transaction",
92+
nargs="*",
93+
help="Transaction data in comma-separated format: Amount, Description , Source account, Destination account, Category, Budget",
94+
)
9195

9296
# Optional arguments (json header)
97+
parser.add_argument(
98+
"-y",
99+
dest="bypass_prompt",
100+
action="store_true",
101+
help="Bypass confirmation prompt.",
102+
)
93103
parser.add_argument(
94104
"--apply-rules",
95105
default=True,
@@ -174,3 +184,8 @@ def add():
174184
)
175185

176186
return parser
187+
188+
@staticmethod
189+
def safe_string(args):
190+
args_str = " ".join(list(map(lambda x: f"'{x}'" if " " in x else x, args)))
191+
return args_str

firefly_cli/transaction.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ def to_dict(
7777

7878
d = self.__dict__
7979

80+
# Remove argparse arguments not related to transaction
81+
if "bypass_prompt" in d:
82+
del d["bypass_prompt"]
83+
8084
if remove_none:
8185
d = {k: v for k, v in d.items() if v is not None}
8286

requirements.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,22 @@ cattrs==1.10.0
44
certifi==2022.6.15
55
charset-normalizer==2.0.12
66
cmd2==2.4.1
7+
coverage==6.4.2
78
idna==3.3
9+
iniconfig==1.1.1
10+
packaging==21.3
11+
pluggy==1.0.0
12+
py==1.11.0
13+
pyparsing==3.0.9
814
pyperclip==1.8.2
15+
pytest==7.1.2
916
pyxdg==0.28
1017
requests==2.28.0
1118
requests-cache==0.9.4
19+
requests-mock==1.9.3
1220
six==1.16.0
1321
tabulate==0.8.9
22+
tomli==2.0.1
1423
url-normalize==1.4.3
1524
urllib3==1.26.9
1625
wcwidth==0.2.5

test/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
import os
2+
from test.utils import ensure_project_root
23

3-
os.environ["XDG_CONFIG_HOME"] = "../.testout"
4+
os.environ["FIREFLY_CLI_CONFIG"] = "test/test_data/firefly-cli-test.ini"
5+
6+
# Ensure tests are being run from root
7+
ensure_project_root()

0 commit comments

Comments
 (0)