monopyly 1.5.0__py3-none-any.whl → 1.5.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. monopyly/CHANGELOG.md +23 -0
  2. monopyly/README.md +2 -2
  3. monopyly/__init__.py +22 -27
  4. monopyly/_version.py +14 -2
  5. monopyly/auth/actions.py +12 -0
  6. monopyly/auth/routes.py +31 -22
  7. monopyly/auth/tools.py +1 -2
  8. monopyly/banking/accounts.py +3 -3
  9. monopyly/banking/banks.py +1 -1
  10. monopyly/banking/routes.py +1 -1
  11. monopyly/banking/transactions.py +14 -5
  12. monopyly/common/forms/utils.py +1 -2
  13. monopyly/common/transactions.py +17 -7
  14. monopyly/core/actions.py +0 -7
  15. monopyly/core/routes.py +0 -6
  16. monopyly/credit/accounts.py +1 -1
  17. monopyly/credit/cards.py +7 -3
  18. monopyly/credit/forms.py +1 -1
  19. monopyly/credit/routes.py +46 -25
  20. monopyly/credit/statements.py +1 -1
  21. monopyly/credit/transactions/_transactions.py +18 -5
  22. monopyly/credit/transactions/activity/parser.py +14 -6
  23. monopyly/credit/transactions/activity/reconciliation.py +20 -1
  24. monopyly/database/__init__.py +1 -56
  25. monopyly/database/models.py +181 -273
  26. monopyly/static/css/style.css +191 -11
  27. monopyly/static/js/create-balance-chart.js +1 -1
  28. monopyly/templates/auth/change_password.html +21 -0
  29. monopyly/templates/auth/login.html +3 -1
  30. monopyly/templates/auth/register.html +17 -7
  31. monopyly/templates/core/profile.html +2 -2
  32. monopyly/templates/credit/statement_reconciliation/statement_reconciliation_inquiry.html +1 -1
  33. monopyly/templates/credit/transaction_submission_page.html +64 -71
  34. monopyly/templates/credit/transactions_table/condensed_row_content.html +0 -1
  35. monopyly/templates/layout.html +2 -2
  36. {monopyly-1.5.0.dist-info → monopyly-1.5.2.dist-info}/METADATA +12 -13
  37. {monopyly-1.5.0.dist-info → monopyly-1.5.2.dist-info}/RECORD +41 -45
  38. {monopyly-1.5.0.dist-info → monopyly-1.5.2.dist-info}/WHEEL +1 -1
  39. monopyly-1.5.2.dist-info/entry_points.txt +2 -0
  40. monopyly/cli/apps.py +0 -108
  41. monopyly/cli/launch.py +0 -135
  42. monopyly/config/__init__.py +0 -1
  43. monopyly/config/default_settings.py +0 -56
  44. monopyly/config/settings.py +0 -59
  45. monopyly-1.5.0.dist-info/entry_points.txt +0 -2
  46. {monopyly-1.5.0.dist-info → monopyly-1.5.2.dist-info}/licenses/COPYING +0 -0
  47. {monopyly-1.5.0.dist-info → monopyly-1.5.2.dist-info}/licenses/LICENSE +0 -0
@@ -2,7 +2,7 @@
2
2
  Tools for interacting with the credit transactions in the database.
3
3
  """
4
4
 
5
- from authanor.database.handler import DatabaseHandler, DatabaseViewHandler
5
+ from dry_foundation.database.handler import DatabaseHandler, DatabaseViewHandler
6
6
 
7
7
  from ...common.forms.utils import execute_on_form_validation
8
8
  from ...common.transactions import TransactionHandler, TransactionTagHandler
@@ -38,7 +38,13 @@ class CreditTransactionHandler(
38
38
  @classmethod
39
39
  @DatabaseViewHandler.view_query
40
40
  def get_transactions(
41
- cls, statement_ids=None, card_ids=None, active=None, sort_order="DESC"
41
+ cls,
42
+ statement_ids=None,
43
+ card_ids=None,
44
+ active=None,
45
+ sort_order="DESC",
46
+ offset=None,
47
+ limit=None,
42
48
  ):
43
49
  """
44
50
  Get credit card transactions from the database.
@@ -66,6 +72,13 @@ class CreditTransactionHandler(
66
72
  An indicator of whether the transactions should be ordered
67
73
  in ascending (oldest at top) or descending (newest at top)
68
74
  order.
75
+ offset : int, optional
76
+ The number of transactions by which to offset the results
77
+ returned by this query. The default is `None`, in which case
78
+ no offset will be added.
79
+ limit : int, optional
80
+ A limit on the number of transactions retrieved from the
81
+ database.
69
82
 
70
83
  Returns
71
84
  -------
@@ -77,7 +90,7 @@ class CreditTransactionHandler(
77
90
  criteria.add_match_filter(CreditCard, "id", card_ids)
78
91
  criteria.add_match_filter(CreditCard, "active", active)
79
92
  transactions = super()._get_transactions(
80
- criteria=criteria, sort_order=sort_order
93
+ criteria=criteria, sort_order=sort_order, offset=offset, limit=limit
81
94
  )
82
95
  return transactions
83
96
 
@@ -196,7 +209,7 @@ class CreditTagHandler(TransactionTagHandler, model=TransactionTagHandler.model)
196
209
  return tags
197
210
 
198
211
  @classmethod
199
- def _filter_entries(cls, query, criteria):
212
+ def _filter_entries(cls, query, criteria, offset, limit):
200
213
  # Add a join to enable filtering by transaction ID or subtransaction ID
201
214
  join_transaction = CreditTransactionView in criteria.discriminators
202
215
  join_subtransaction = (
@@ -206,7 +219,7 @@ class CreditTagHandler(TransactionTagHandler, model=TransactionTagHandler.model)
206
219
  query = query.join(credit_tag_link_table).join(CreditSubtransaction)
207
220
  if join_transaction:
208
221
  query = query.join(CreditTransactionView)
209
- return super()._filter_entries(query, criteria)
222
+ return super()._filter_entries(query, criteria, offset, limit)
210
223
 
211
224
 
212
225
  @execute_on_form_validation
@@ -4,12 +4,14 @@ import csv
4
4
  from abc import ABC, abstractmethod
5
5
  from pathlib import Path
6
6
 
7
- from flask import current_app
7
+ from flask import abort, current_app
8
8
  from werkzeug.utils import secure_filename
9
9
 
10
10
  from ....common.utils import parse_date
11
11
  from .data import ActivityLoadingError, TransactionActivities, TransactionActivityLoader
12
12
 
13
+ SUPPORTED_BANKS = ("Bank of America", "Chase", "Discover")
14
+
13
15
 
14
16
  def parse_transaction_activity_file(transaction_file):
15
17
  """
@@ -91,7 +93,7 @@ class _TransactionDateColumnIdentifier(_ColumnIdentifier):
91
93
  elif "date" in standardized_title.split():
92
94
  if "trans." in standardized_title:
93
95
  return True
94
- elif standardized_title == "date":
96
+ elif standardized_title == "date" or standardized_title == "posted date":
95
97
  return None
96
98
  return False
97
99
 
@@ -104,8 +106,8 @@ class _TransactionTotalColumnIdentifier(_ColumnIdentifier):
104
106
 
105
107
  class _TransactionDescriptionColumnIdentifier(_ColumnIdentifier):
106
108
  def check(self, standardized_title):
107
- """Check if the title indicates this column contains a description."""
108
- return standardized_title in ("description", "desc.")
109
+ """Check if the title indicates this column contains a description (payee)."""
110
+ return standardized_title in ("description", "desc.", "payee")
109
111
 
110
112
 
111
113
  class _TransactionCategoryColumnIdentifier(_ColumnIdentifier):
@@ -184,9 +186,15 @@ class _TransactionActivityParser:
184
186
  }
185
187
  for column_type in self.column_types:
186
188
  if raw_column_indices[column_type] is None:
187
- raise RuntimeError(
188
- f"The '{column_type}' column could not be identified in the data."
189
+ current_app.logger.debug(
190
+ f"The '{column_type}' column could not be identified in the data. "
191
+ )
192
+ msg = (
193
+ "The data was unable to be parsed, most likely because it did not "
194
+ "match a recognized format. Supported data formats include those "
195
+ f"from the following banks: {', '.join(SUPPORTED_BANKS)}."
189
196
  )
197
+ abort(400, msg)
190
198
  return raw_column_indices
191
199
 
192
200
  def _determine_expenditure_sign(self, raw_data):
@@ -222,7 +222,26 @@ class _Matchmaker(ABC):
222
222
  field : str
223
223
  The string of text to be tokenized.
224
224
  """
225
- return set(wordpunct_tokenize(field.replace("'", "").casefold()))
225
+ replacements = [
226
+ # Remove disruptive punctuation
227
+ ("1-800", "1800"),
228
+ ("-", " "),
229
+ (".", " "),
230
+ (",", " "),
231
+ ("(", " "),
232
+ (")", " "),
233
+ ("'", ""),
234
+ # Standardize characters
235
+ ("&", "and"),
236
+ ("é", "e"),
237
+ ]
238
+ for original, replacement in replacements:
239
+ field = field.replace(original, replacement)
240
+ tokens = set(wordpunct_tokenize(field.replace("'", "").casefold()))
241
+ removals = ["and", "the", "of"]
242
+ for word in removals:
243
+ tokens.discard(word)
244
+ return tokens
226
245
 
227
246
  def _compute_transaction_activity_similarity_score(
228
247
  self, merchant_tokens, notes_tokens, activity_tokens
@@ -2,17 +2,10 @@
2
2
  Expose commonly used database functionality to the rest of the package.
3
3
  """
4
4
 
5
- import sqlite3
6
5
  from pathlib import Path
7
6
 
8
- import click
7
+ from dry_foundation.database import SQLAlchemy as _SQLAlchemy
9
8
  from flask import current_app
10
- from flask.cli import with_appcontext
11
- from fuisce.database import SQLAlchemy as _SQLAlchemy
12
-
13
- from ..core.actions import get_timestamp
14
-
15
- BASE_DB_NAME = f"monopyly.sqlite"
16
9
 
17
10
 
18
11
  class SQLAlchemy(_SQLAlchemy):
@@ -51,51 +44,3 @@ class SQLAlchemy(_SQLAlchemy):
51
44
 
52
45
 
53
46
  SQLAlchemy.create_default_interface(echo_engine=False)
54
-
55
-
56
- @click.command("init-db")
57
- @with_appcontext
58
- def init_db_command():
59
- """Initialize the database from the command line (if it does not exist)."""
60
- db_path = current_app.config["DATABASE"]
61
- if not db_path.is_file():
62
- current_app.db.initialize(current_app)
63
- click.echo(f"Initialized the database ('{db_path}')")
64
- preload_path = current_app.config.get("PRELOAD_DATA_PATH")
65
- if preload_path:
66
- click.echo(f"Prepopulated the database using '{preload_path}'")
67
- else:
68
- click.echo(f"Database exists, using '{db_path}'")
69
-
70
-
71
- @click.command("back-up-db")
72
- @with_appcontext
73
- def back_up_db_command():
74
- """Back up the database from the command line."""
75
- timestamp = get_timestamp()
76
- # Connect to the database
77
- db = sqlite3.connect(current_app.config["DATABASE"])
78
- # Create and connect to the backup database
79
- backup_db_dir_path = Path(current_app.instance_path) / "db_backups"
80
- backup_db_dir_path.mkdir(exist_ok=True, parents=True)
81
- backup_db_path = backup_db_dir_path / f"backup_{timestamp}.sqlite"
82
- backup_db = sqlite3.connect(backup_db_path)
83
- # Back up the database and print status
84
- back_up_db(db, backup_db)
85
- click.echo(f"Backup complete ({timestamp})")
86
-
87
-
88
- def back_up_db(db, backup_db):
89
- """Create a backup of the database."""
90
- # Backup the database
91
- with backup_db:
92
- db.backup(backup_db)
93
- # Close the connections
94
- backup_db.close()
95
- db.close()
96
-
97
-
98
- def register_db_cli_commands(app):
99
- """Register database CLI commands with the app."""
100
- app.cli.add_command(init_db_command)
101
- app.cli.add_command(back_up_db_command)