diff --git a/.githooks/commit-msg b/.githooks/commit-msg new file mode 100755 index 0000000..3d6aa49 --- /dev/null +++ b/.githooks/commit-msg @@ -0,0 +1,21 @@ +#!/bin/sh + +commit_msg_file="$1" +commit_msg="$(head -n 1 "$commit_msg_file")" + +pattern='^(feat|fix|docs|style|refactor|test|chore|ci|build|perf)\(#[0-9]+\): .+' + +if echo "$commit_msg" | grep -Eq "$pattern"; then + exit 0 +fi + +echo "Invalid commit message:" +echo " $commit_msg" +echo "" +echo "Expected format:" +echo " feat(#1): add trend metrics" +echo "" +echo "Allowed types:" +echo " feat, fix, docs, style, refactor, test, chore, ci, build, perf" + +exit 1 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dc01479 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,26 @@ +## Problem + +Describe what is broken. + +## Expected behavior + +Describe what should happen instead. + +## Steps to reproduce + +1. +2. +3. + +## Environment + +- OS: +- Python version: +- Branch: +- Command used: + +## Error output + +```text + +``` diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md new file mode 100644 index 0000000..f9b8b9a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -0,0 +1,22 @@ +## Goal + +Describe what should be achieved. + +## Why + +Explain why this task is useful for the project. + +## Scope + +- +- +- + +## Acceptance criteria + +- [ ] +- [ ] +- [ ] + +> [!NOTE] +> Priority: diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f2e002d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 + +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..e0b7352 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,27 @@ +## What changed? + +- +- +- + +## Related issue + +Closes # + +## Tests + +- [ ] I ran `pytest` +- [ ] Existing tests pass +- [ ] I added or updated tests where necessary +- [ ] Not needed because this only affects documentation or repository setup + +## Checklist + +- [ ] The change is focused +- [ ] No secrets or API keys are included +- [ ] Documentation was updated if needed +- [ ] CI is expected to pass + +## Notes + +- \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7cb6c89 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +# This workflow ensures that the code is tested and meets quality standards on every push and pull request +name: Tests + +on: + push: + pull_request: + branches: + - main + - develop + +jobs: + tests: + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install project + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + + - name: Run tests + run: pytest + + - name: Check code quality + run: ruff check . + + - name: Check code formatting + run: ruff format --check . + \ No newline at end of file diff --git a/.github/workflows/commit-message.yml b/.github/workflows/commit-message.yml new file mode 100644 index 0000000..7005242 --- /dev/null +++ b/.github/workflows/commit-message.yml @@ -0,0 +1,51 @@ +# This workflow is needed to validate commit messages in pull requests, if the githook was bypassed +name: Commit Message + +on: + pull_request: + +permissions: + contents: read + pull-requests: read + +jobs: + validate-commit-messages: + name: Validate commit messages + runs-on: ubuntu-latest + + steps: + - name: Check commit message + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Fetch base branch + run: git fetch origin "${{ github.base_ref }}" + - name: Validate commit message + shell: bash + run: | + pattern='^(feat|fix|docs|style|refactor|test|chore|ci|build|perf)\(#[0-9]+\): .{1,50}$' + invalid=0 + + while IFS= read -r commit_message; do + echo "Checking: $commit_message" + + if echo "$commit_message" | grep -Eq '^(Merge|Revert)'; then + echo "Skipping technical commit: $commit_message" + continue + fi + + if ! echo "$commit_message" | grep -Eq "$pattern"; then + echo "::error::Invalid commit message: $commit_message" + invalid=1 + fi + done < <(git log --format=%s "origin/${{ github.base_ref }}..HEAD") + + if [ "$invalid" -ne 0 ]; then + echo "" + echo "Expected format:" + echo " feat(#1): add feature" + echo "" + echo "Allowed types:" + echo " feat, fix, docs, style, refactor, test, chore, ci, build, perf" + exit 1 + fi \ No newline at end of file diff --git a/.gitignore b/.gitignore index d995796..9a1d8a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,15 @@ # Python cache files __pycache__ +.pytest_cache *.pyc +/src/argus.egg-info + # IDE settings -.vscode/ +.vscode -# Temporary files (exercise files) -.temp/ +# ruff cache +.ruff_cache # Virtual environment -.env/ -venv/ \ No newline at end of file +.env \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index c9ebf2d..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "python-envs.defaultEnvManager": "ms-python.python:system" -} \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..44fd766 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,100 @@ +# Code of Conduct + +## My Pledge + +ARGUS is a technical learning, portfolio and open-source project focused on market analytics, data workflows and future AI-assisted monitoring. + +I want this project to be a respectful and constructive space for learning, collaboration and technical discussion. + +Everyone who participates in this project is expected to communicate with professionalism, patience and respect. + +## Expected Behavior + +Examples of behavior that helps this project: + +- using welcoming and respectful language +- giving constructive feedback in issues, pull requests and reviews +- explaining technical suggestions clearly +- being patient with questions, mistakes and learning processes +- keeping discussions focused on the project and its goals +- respecting different levels of experience +- reporting bugs or problems with useful context +- proposing changes without attacking people + +## Unacceptable Behavior + +The following behavior is not acceptable: + +- insults, harassment or personal attacks +- discriminatory language or behavior +- aggressive, hostile or mocking comments +- sexualized language or unwanted attention +- trolling, spam or repeated off-topic comments +- publishing private information without permission +- deliberately disruptive behavior in issues, pull requests or discussions +- pressuring maintainers to accept changes immediately + +## Scope + +This Code of Conduct applies to all project spaces, including: + +- GitHub issues +- pull requests +- code reviews +- GitHub discussions +- the project wiki +- documentation contributions +- any future community spaces connected to ARGUS + +It also applies when someone represents the project in public project-related communication. + +## Maintainer Responsibilities + +Project maintainers are responsible for keeping the project space respectful and productive. + +Maintainers may take appropriate action in response to unacceptable behavior, including: + +- asking someone to change their behavior +- editing or removing comments +- closing issues or pull requests +- limiting participation +- blocking users from the project if necessary + +Maintainers should apply these rules fairly and focus on protecting a constructive technical environment. + +## Enforcement Principles + +Enforcement should be proportional to the situation. + +Possible responses include: + +1. **Clarification** + A maintainer explains why a behavior is inappropriate and asks for a change. + +2. **Warning** + A maintainer gives a clear warning if the behavior continues or is serious. + +3. **Temporary restriction** + A user may be temporarily limited from participating in discussions or pull requests. + +4. **Permanent restriction** + A user may be blocked from the project in cases of harassment, repeated abuse or serious misconduct. + +## Project Culture + +ARGUS is built around careful technical growth. + +The project values: + +- clear thinking +- reliable code +- honest documentation +- constructive review +- long-term learning +- respectful collaboration + +Strong technical opinions are welcome. Personal attacks are not. + +## Attribution + +This Code of Conduct is inspired by common open-source community guidelines and adapted for the ARGUS project. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0b53865 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,342 @@ +# Contributing to ARGUS + +Thank you for your interest in contributing to ARGUS. + +ARGUS is a Python-based market analytics project focused on clean data workflows, reliable code, useful metrics and future AI-assisted monitoring. + +This project is still growing, so contributions should help the project become more stable, understandable and useful step by step. + +> [!IMPORTANT] +> ARGUS values reliability, clear communication and long-term skill building. +> Contributions should improve the project without creating unnecessary complexity. + +--- + +## Project Mindset + +ARGUS is not only about adding features quickly. + +The project is built around: + +- clean Python code +- understandable architecture +- reliable tests +- useful documentation +- careful data handling +- practical analytics +- continuous learning + +Good contributions should make the project easier to use, test, maintain or extend. + +--- + +## What You Can Contribute + +Helpful contributions include: + +- bug fixes +- tests +- documentation improvements +- small refactorings +- validation improvements +- analytics metrics +- chart improvements +- data-source clients +- CI/CD improvements +- issue clarification +- architecture notes +- examples and usage instructions + +> [!NOTE] +> Large features should usually start with an issue or short discussion before implementation. + +--- + +## Before You Start + +Before working on a contribution: + +1. Check existing issues and pull requests. +2. Make sure your idea fits the current roadmap. +3. Keep the scope small and focused. +4. Ask if the direction is unclear. + +Avoid opening large pull requests that mix unrelated changes. + +Good examples: + +- one PR for a new metric +- one PR for a bug fix +- one PR for documentation cleanup +- one PR for CI improvements + +Bad examples: + +- one PR that changes the UI, rewrites services, updates docs and adds a new data client at the same time + +--- + +## Development Setup + +Clone the repository: + +```bash +git clone https://github.com/BytecodeBrewer/argus.git +cd argus +``` + +Create a virtual environment: + +```bash +python -m venv .venv +``` + +Activate it. + +On Windows PowerShell: + +```powershell +.venv\Scripts\Activate.ps1 +``` + +On macOS/Linux: + +```bash +source .venv/bin/activate +``` + +Install the project with development dependencies: + +```bash +pip install -e ".[dev]" +``` + +--- + +## Branch Workflow + +Create a new branch for your work: + +```bash +git checkout -b +``` + +Example: + +```bash +git checkout -b 12-add-volatility-metric +``` + +Use focused branch names that describe the work. + +--- + +## Commit Expectations + +Commits should be small, understandable and related to the current task. + +Good commit messages: + +```text +Add rolling volatility metric +Fix currency validation edge case +Update README setup instructions +Add tests for trend metrics +``` + +Avoid unclear messages: + +```text +fix +stuff +changes +update +final +``` + +> [!TIP] +> A good commit tells future readers what changed and why it belongs to the task. + +--- + +## Testing + +Before opening a pull request, run the test suite: + +```bash +pytest +``` + +A pull request should not be opened as ready for review if tests are failing without explanation. + +If a test fails and you do not know why, mention it clearly in the pull request. + +> [!IMPORTANT] +> CI checks must pass before a pull request can be merged. + +--- + +## Pull Request Expectations + +A good pull request should include: + +- a clear title +- a short explanation of what changed +- a link to the related issue if available +- notes about tests +- screenshots for UI changes if useful +- a short explanation of any trade-offs + +Pull requests should be focused and reviewable. + +Before opening a pull request, run: + +```bash +pytest +ruff check . +ruff format --check . +``` + +--- + +## Reliability Expectations + +Contributors are expected to work reliably. + +This means: + +- do not submit random or unfinished code without context +- do not ignore failing tests +- do not introduce secrets, API keys or local machine paths +- do not rewrite unrelated parts of the project without discussion +- communicate if you are unsure +- keep changes understandable for future contributors +- respect the existing architecture unless there is a clear reason to change it + +Reliability does not mean knowing everything already. + +It means being honest, careful and consistent. + +--- + +## Learning Mindset + +ARGUS welcomes contributors who want to improve their technical skills. + +You do not need to be an expert to contribute. + +Helpful behavior includes: + +- asking clear questions +- explaining your reasoning +- being open to review feedback +- improving your code after feedback +- learning from tests, errors and architecture discussions +- documenting what you learned when it helps others + +> [!NOTE] +> This project values skill growth. +> A thoughtful small contribution is better than a large unclear one. + +--- + +## Code Style + +Keep code simple and readable. + +General guidelines: + +- prefer clear names over clever shortcuts +- keep functions focused +- separate data loading, transformation and presentation logic +- avoid unnecessary global state +- avoid hidden side effects +- add tests for important behavior +- document assumptions when they matter + +For analytics code: + +- keep metric functions reusable +- avoid coupling metrics directly to UI code +- avoid coupling metrics directly to one specific API client +- prefer clear pandas transformations + +--- + +## Secrets and API Keys + +Never commit secrets. + +Do not commit: + +- API keys +- tokens +- passwords +- `.env` files +- local config files with private data + +Use a local `.env` file for secrets. + +```env +api_key=your_api_key_here +``` + +> [!WARNING] +> If you accidentally commit a secret, revoke it immediately and inform the maintainer. + +--- + +## Documentation + +Documentation changes are welcome. + +Useful documentation includes: + +- setup instructions +- roadmap notes +- architecture explanations +- metric definitions +- data-source assumptions +- troubleshooting notes + +Repository-level files such as `README.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md` and `LICENSE` belong in the repository root. + +Technical notes, research and deeper explanations belong in `docs/`. + +--- + +## Communication + +Please communicate respectfully and constructively. + +When giving feedback: + +- focus on the code or idea, not the person +- explain the reason behind suggestions +- be specific +- stay open to alternatives + +When receiving feedback: + +- assume good intent +- ask questions if something is unclear +- improve the contribution step by step + +All contributors are expected to follow the project’s Code of Conduct. + +--- + +## Maintainer Notes + +The maintainer may ask for changes before merging a pull request. + +A contribution may be declined if it: + +- does not fit the current roadmap +- adds too much complexity too early +- breaks existing functionality +- lacks necessary tests +- duplicates existing work +- does not follow the project’s quality expectations + +This helps keep ARGUS stable, learnable and maintainable. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4b3f66a --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 BytecodeBrewer + + 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. \ No newline at end of file diff --git a/README.md b/README.md index 3195f27..2914fa1 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,349 @@ -# FX Converter Lab +# ARGUS -A growing Python project exploring currency conversion, exchange rate APIs, and financial data workflows. +
+ Banner +
+ +--- + +ARGUS is a Python-based market analytics project evolving from a small FX converter into a broader data-oriented platform for exchange rates, market data, analytics, dashboards, and future AI-assisted monitoring workflows. + +> [!NOTE] +> This project started as **FX Converter Lab** and is being renamed to **ARGUS** as the scope grows beyond simple currency conversion. + +ARGUS is currently focused on building a clean local foundation: + +- currency conversion using live exchange-rate data +- calculator and conversion logic +- input validation and error handling +- Tkinter GUI prototype +- legacy CLI/debug interface +- first pandas/matplotlib-based analytics prototype +- tests and documentation + +> [!IMPORTANT] +> ARGUS is not a finished trading tool or financial advisor. +> It is a portfolio and learning project for building reliable data, analytics, visualization and future automation workflows. + +--- + +## License + +This project is licensed under the [Apache License 2.0](LICENSE). + +## Project Direction + +ARGUS is designed to grow step by step from a local Python application into a market analytics and monitoring system. + +The long-term direction includes: + +- market data ingestion +- historical FX and market data analysis +- reusable analytics and metric layers +- dashboards and visualizations +- local and cloud-based storage +- data quality checks +- reporting workflows +- future AI-assisted research and agentic monitoring + +> [!TIP] +> The goal is to keep each development step usable and testable instead of building a large system all at once. + +--- + +## Roadmap + +The full project roadmap is maintained separately in [`docs/roadmap.md`](docs/roadmap.md). + +Each roadmap phase is treated as a separate development sprint. The roadmap describes how ARGUS is planned to grow from a local Python application into a broader market analytics, data engineering and future AI-assisted monitoring system. + +> [!TIP] +> The README gives a compact project overview. +> Detailed planning, sprint scope and long-term architecture notes live in the documentation files. --- ## Current Features -- Basic calculator (CLI-based) -- Currency exchange rates via REST API -- Currency conversion logic +- Calculator +- Currency conversion using live exchange rates - Input validation and error handling -- Support for multiple arithmetic operations +- Tkinter GUI prototype +- Legacy CLI/debug interface +- Basic pandas-based trend metrics +- Matplotlib-based trend visualization +- Mock time-series data for early analytics development +- Basic test suite + +> [!CAUTION] +> Historical market data support is still limited. +> The current live exchange-rate client is useful for simple conversion, but future analytics work will require additional data sources such as Frankfurter or yfinance. --- ## Project Structure -- `src/` – main application logic -- `temp/` – experiments and early learning code (not part of final system) +```text +docs/ +src/ + argus/ + analytics/ + charts/ + metrics/ + clients/ + domain/ + gui/ + services/ + config.py + main.py + legacy/ + cli/ +tests/ +pyproject.toml +README.md +``` --- -## Tech Stack +## Current Tech Stack + +### Language + +- Python 3.11+ + +### Core libraries -- Python -- REST API (ExchangeRate API) - requests +- python-dotenv +- pandas +- NumPy +- matplotlib +- Tkinter +- pytest + +### Current data source + +- ExchangeRate API for live currency conversion --- -## Roadmap +## Planned / Future Tech Stack + +ARGUS is expected to grow into a broader data and analytics system. + +Planned or likely future technologies include: + +### Data sources + +- Frankfurter API for historical FX data +- yfinance for broader market data +- possible additional market-data APIs later + +### Data processing + +- pandas +- NumPy +- possibly Polars later for larger datasets + +### Storage + +- PostgreSQL +- DuckDB +- Parquet +- optional cloud storage + +### Visualization and UI + +- matplotlib +- Plotly +- NiceGUI -### Phase 1 (current) -- CLI calculator -- API integration -- basic currency conversion +### DevOps and deployment -### Phase 2 -- improve conversion logic -- better structure (modules, separation) -- error handling & validation +- GitHub Actions +- Docker +- Docker Compose +- cloud deployment later -### Phase 3 -- visualization (matplotlib / plotly) -- historical exchange rates -- data analysis features +### Cloud and data engineering -### Phase 4 (vision) -- web interface (JS / frontend) -- integration with Excel / Power BI -- cloud-based data processing +- Azure, GCP or AWS depending on project direction +- scheduled ingestion +- data quality checks +- reporting pipelines + +### AI and agentic workflows + +- LLM-assisted summaries +- RAG over stored reports or notes +- agentic data checks +- anomaly monitoring +- human-in-the-loop signal review + +> [!CAUTION] +> AI and agentic features are future-stage ideas. +> They should only be added after the data, storage, service and reporting layers are stable. --- -## Goal +## Requirements + +Before running ARGUS locally, make sure you have: + +- Python 3.11 or newer +- Git +- pip +- an ExchangeRate API key for live currency conversion + +Recommended for development: + +- VS Code +- a virtual environment +- pytest + +> [!NOTE] +> Runtime dependencies are managed through `pyproject.toml`. + +--- + +## Setup + +Clone the repository: + +```bash +git clone https://github.com/BytecodeBrewer/argus.git +cd argus +``` -This project is not just a calculator. +Create a virtual environment: -It is a learning environment to understand: -- API integration -- data processing -- system design -- building useful tools step by step +```bash +python -m venv .venv +``` + +Activate the virtual environment. + +On Windows PowerShell: + +```powershell +.venv\Scripts\Activate.ps1 +``` + +On macOS/Linux: + +```bash +source .venv/bin/activate +``` + +Install the project in editable mode: + +```bash +pip install -e . +``` + +For development and tests, install the development dependencies: + +```bash +pip install -e ".[dev]" +``` + +> [!TIP] +> Editable install keeps the project linked to your local source files. +> This means code changes are picked up without reinstalling the project after every edit. + +--- + +## API Key Setup + +ARGUS currently uses the ExchangeRate API for live currency conversion. + +### 1. Create an API key + +Create a free account at ExchangeRate API and generate your personal API key. + +### 2. Create a `.env` file + +Create a file named `.env` in the project root: + +```text +.env +``` + +Add your API key: + +```env +api_key=your_api_key_here +``` + +### 3. Keep secrets private + +The `.env` file must stay local and should never be committed. + +> [!WARNING] +> Never commit API keys, tokens or secrets to the repository. +> Make sure `.env` is listed in `.gitignore`. + +--- + +## Running ARGUS + +Start the current Tkinter GUI: + +```bash +python -m argus.main +``` + +This starts the local ARGUS prototype with calculator, currency conversion and basic analytics views. + +### Legacy CLI / Debug Interface + +The legacy CLI is still available for quick local checks and debugging: + +```bash +python src/legacy/debug_main.py +``` + +> [!NOTE] +> The Tkinter GUI is the current main local interface. +> The CLI is kept as a legacy/debug interface and is not the long-term product interface. + +--- + +## Running Tests + +Run the test suite: + +```bash +pytest +``` + +> [!TIP] +> Run tests after changing clients, services, validation logic or analytics functions. + +--- + +## Documentation + +More detailed project documentation lives in [`docs/`](docs/). + +Current documentation: + +- [`docs/roadmap.md`](docs/roadmap.md) — sprint-based project roadmap +- [`docs/gui.md`](docs/gui.md) — notes about the current Tkinter GUI prototype +- metric and UI research notes for future analytics and interface decisions + +--- ## Status -Currently under active development. \ No newline at end of file + +ARGUS is under active development. + +The project is currently transitioning from a small FX converter into a broader market analytics platform. + +Current focus: + +- finish Sprint 1 foundation +- prepare first public release +- improve README and project documentation +- keep the application runnable and testable +- prepare the next analytics and data-source expansion diff --git a/docs/client_api.md b/docs/client_api.md new file mode 100644 index 0000000..465efdf --- /dev/null +++ b/docs/client_api.md @@ -0,0 +1,33 @@ +# ExchangeRate-Client + +Kurzbeschreibung + +Der ExchangeRate-Client ist in `src/clients/exchangerate_client.py` implementiert und stellt die Funktion `get_rates(curr1, curr2)` zur Verfügung. Die Funktion ruft die API von exchangerate-api.com ab und verwendet den API-Key aus der `.env`-Datei (Umgebungsvariable `api_key`). + +Rückgabe + +- Bei Erfolg: Ein Dictionary mit mindestens `{ "result": "success", "conversion_rate": }`. +- Bei Fehlern: `None` (zusätzlich werden deutsche Fehlermeldungen auf STDOUT ausgegeben). + +Fehler- und Timeout-Verhalten + +- Timeout: 5 Sekunden; die Implementierung fängt `Timeout`, `ConnectionError`, `RequestException`, `ValueError` und `KeyError` ab und gibt bei Bedarf Hinweise auf der Konsole aus. +- API-spezifische Fehler (`unsupported-code`, `malformed-request`, `invalid-key`, `inactive-account`, `quota-reached`) werden von `check_error(err_type)` verarbeitet und als aussagekräftige deutsche Meldungen ausgegeben. + +Integration + +In `src/main.py` wird der Client wie folgt eingebunden: `from clients import exchangerate_client as api` und über `api.get_rates()` in den Funktionen `get_conv_rate()` / `convert()` verwendet, um Wechselkurse für Benutzereingaben zu ermitteln. + +Beispiel + +```py +# Beispielaufruf +data = get_rates("EUR", "USD") +if data is not None and data.get("result") == "success": + rate = data["conversion_rate"] + # weiterverarbeiten +else: + # Fehlerbehandlung (siehe Konsolenausgaben) +``` + +Hinweis: Der Client liefert ein vereinfachtes, rohes Dictionary zurück; die Aufbereitung für Anzeige oder weitere Logik erfolgt in der aufrufenden Anwendung. diff --git a/docs/gui.md b/docs/gui.md new file mode 100644 index 0000000..b119132 --- /dev/null +++ b/docs/gui.md @@ -0,0 +1,39 @@ +# GUI — Developer Overview + +## Key functions + +- `app()` — launches Tkinter main loop (entry in `src/argus/main.py`) +- `show_menu()` — show main menu and clear inputs/results +- `show_calc()` / `show_conv()` — switch views +- `act_calculate()` — validate inputs and call calculator service +- `act_convert()` — validate inputs and call conversion service + +## Structure + +- **Main window:** `root` (Tk) +- **Frames:** + + - `menu_frame` — mode selection (Calculator, Converter) + - `app_frame` — container for `sidebar` and `content` + - `sidebar` — navigation (Calculator, Converter, Back to menu) + - `content` — contains `calc_frame` and `conv_frame` + +- **Views:** + - `calc_frame` — Number1, Number2, Operation entries; `Calculate` button; result label + - `conv_frame` — Amount, Currency1, Currency2 entries; `Convert` button; result label + +## Flow + +1. User selects a mode on the menu → `show_calc()` or `show_conv()` displays the view. +2. User enters values and clicks the action button. +3. Handler (`act_calculate()` / `act_convert()`) validates input, then calls the service layer. +4. Conversion service may call `clients/exchangerate_client.py` to fetch rates; the service returns a value or `None`. +5. UI displays the result or a short error message; navigation returns to menu as needed. + +## Interaction + +- GUI delegates business logic to `src/argus/services/*` and uses the client in `src/argus/clients/` for rates. + +## See also + +- `src/argus/gui/app.py` — implementation (UI & handlers) diff --git a/docs/pictures/banner.png b/docs/pictures/banner.png new file mode 100644 index 0000000..1c71136 Binary files /dev/null and b/docs/pictures/banner.png differ diff --git a/docs/research-gui-for-py-projects.md b/docs/research-gui-for-py-projects.md new file mode 100644 index 0000000..c5a7741 --- /dev/null +++ b/docs/research-gui-for-py-projects.md @@ -0,0 +1,130 @@ +# GUI / Frontend Research for FX Converter Lab + +## Goal + +Evaluate possible GUI and frontend technologies for the future direction of the project. + +The project is evolving from a simple currency converter into a larger financial data and analytics platform with: + +* API integrations +* data processing +* ETL pipelines +* dashboards +* machine learning experiments +* Power BI connectors +* future RAG and data lake concepts + +The selected UI technology should support this long-term direction without creating unnecessary complexity. + +The research is not only about choosing a GUI framework. +It is also about choosing an application direction that can grow from a local learning project into a small financial data and analytics platform. + +--- + +## GUI / Frontend Comparison + +### Data & Analytics Focus + +| Technology | Strengths | Weaknesses | Fit for Project | +| ---------- | --------- | ---------- | --------------- | +| Streamlit | Extremely fast prototyping, very popular in Data Science, easy ML and data app demos | Less flexible for larger application structures, architecture can become limiting | Good | +| Dash | Strong analytics and dashboard ecosystem, widely used for analytical web apps and finance/BI-style dashboards | More complex than Streamlit, dashboard-focused, less suitable as the whole application platform | Very Good | +| Panel | Designed for data exploration and analytical workflows, integrates well with the PyData ecosystem, supports dashboards and complex applications | Smaller community and ecosystem than Streamlit/Dash | Very Good | + +### General Application Focus + +| Technology | Strengths | Weaknesses | Fit for Project | +| ---------- | --------- | ---------- | --------------- | +| NiceGUI | Python-first, browser-based UI, FastAPI-based, supports dashboards, APIs, routing and multi-page applications | Smaller ecosystem than major frontend frameworks | Excellent | +| Reflex | Modern architecture, scalable web apps | More frontend complexity and abstraction | Good | +| Anvil | Fast development, many built-in features | Less transparent architecture, more platform-oriented | Medium | +| PySide6 | Powerful desktop applications, professional UI capabilities | Higher learning curve, desktop-focused architecture, less aligned with web/dashboard/cloud direction | Good technically, Medium strategically | +| Tkinter | Simple, built into Python, useful for learning GUI basics | Outdated for larger analytics platforms and web deployment | Learning phase only | + +--- + +## Tkinter Evaluation + +Tkinter is suitable for: + +* learning GUI fundamentals +* event handling +* desktop application basics +* small internal tools +* simple data visualization with Pandas and Matplotlib + +Tkinter can support: + +* CSV imports +* financial calculations +* charts +* small analytics tools + +However, the project vision includes: + +* dashboards +* web access +* cloud deployment +* ETL pipelines +* machine learning services +* Power BI integrations + +For these goals, Tkinter becomes increasingly limiting. + +Conclusion: + +Tkinter is useful as a learning step and prototype phase, but should not become the long-term platform architecture. + +Tkinter should be finished for the current GUI prototype ticket, but future UI development should move toward NiceGUI. + +--- + +## PySide6 Evaluation + +PySide6 is a strong option for professional desktop applications. + +It is technically more powerful than Tkinter and suitable for serious desktop software with: + +* advanced widgets +* complex layouts +* professional desktop UI patterns +* larger local applications + +However, PySide6 introduces a different type of complexity: + +* Qt concepts +* signals and slots +* widget hierarchies +* layout management +* desktop-specific UI architecture + +This is not necessarily too difficult, but it would cost additional learning time. + +For this project, the main direction is not desktop software engineering. +The project is moving toward data engineering, analytics, dashboards, APIs, ETL, ML and future cloud deployment. + +Conclusion: + +PySide6 is technically strong, but not the best strategic next step for this project right now. + +NiceGUI fits the project direction better because it connects UI work with web apps, dashboards, APIs and future deployment. + +--- + +## Recommended Project Structure + +```text +src/ +│ +├─ clients/ +├─ services/ +├─ pipelines/ +├─ analytics/ +├─ ml/ +├─ storage/ +├─ api/ +│ +└─ ui/ + │ + ├─ tkinter_legacy/ + └─ nicegui/ diff --git a/docs/research-useful-market-metrics.md b/docs/research-useful-market-metrics.md new file mode 100644 index 0000000..237c22b --- /dev/null +++ b/docs/research-useful-market-metrics.md @@ -0,0 +1,338 @@ +# Useful metrics for the fx market + +## Intro To Research + +Identify simple but meaningful metrics for analyzing exchange-rate data. + +It should be a beginner-friendly analytics that: + +* provide useful insights +* are easy to implement in Python +* work well with charts +* as less as possible number of diffrent charts - as much as necessary for user-friendly visibility +* create a foundation for future ETL, ML and signal-generation features + +--- + +## Overview of decided metrics for this project + +### This sprint + +* Daily Percentage Change +* Min/Max +* Rolling Average + +### Next Sprint + +* Volatility +* Strongest / Weakest Day +* Cumulative Return + +--- + +## Core Trend Metrics (Sprint 1) + +These metrics provide immediate value, are easy to understand, and work well for visualizations. + +They should be implemented first. + +--- + +### Daily Percentage Change + +* **What it is**: Measures how much the exchange rate changed compared to the previous day. +* **Why it is useful**: + * Normalizes movements across different currency pairs + * Shows whether a currency strengthened or weakened + * Serves as the foundation for several future metrics +* **Chart Idea**: Bar chart below the main exchange-rate chart. + +```text +Positive movement → above zero +Negative movement → below zero +``` + +* **Priority**: Sprint 1 + +--- + +### Rolling Average (Moving Average) + +* **What it is**: A moving average smooths daily fluctuations and highlights the underlying trend. It is commonly used in time-series analysis to reduce noise and reveal longer-term patterns. +* **Why it is useful**: + * Makes trends easier to identify + * Reduces short-term noise + * Introduces basic time-series analysis concepts +* **Chart Idea**: Display together with the exchange-rate line. + +```text +Exchange Rate ++ +7-Day Rolling Average +``` + +* **Priority**: Sprint 1 + +--- + +### Min / Max Rate + +* **What it is**: Identifies the lowest and highest exchange rate within the selected period. +* **Why it is useful**: + * Very intuitive for users + * Common metric in financial applications + * Useful for understanding the range of movements +* **Chart Idea**: Show markers directly on the main chart. + +```text +● Highest Rate +● Lowest Rate +``` + +* **Priority**: Sprint 1 + +--- + +## Recommended Sprint 1 Visualization + +All Sprint 1 metrics can be displayed within a single dashboard. + +### Main Chart + +```text +Exchange Rate Line ++ +Rolling Average Line ++ +Min / Max Markers +``` + +### Secondary Chart + +```text +Daily Percentage Change +``` + +--- + +## Movement, Performance & Risk Metrics (Sprint 2) + +These metrics provide additional context about exchange-rate behavior. + +They should extend the existing dashboard without adding too many separate charts. + +--- + +### Cumulative Return + +* **What it is**: Measures the total percentage change between the first and last value in the selected period. +* **Why it is useful**: + * Shows overall performance + * Easy for users to interpret + * Useful for comparing periods + * Works well as a higher-level performance metric +* **Chart Idea**: + * Cumulative Return + * Strongest Day Marker + * Weakest Day Marker +* **Priority**: Sprint 2 + +--- + +### Strongest / Weakest Day + +* **What it is**: Identifies the largest positive and largest negative daily percentage movement in the selected period. +* **Why it is useful**: + * Highlights important events + * Creates interesting insights for users + * Easy to calculate from daily percentage change + * Works best as markers instead of a separate chart +* **Chart Idea**: + * Performance Chart + * Strongest Day Marker + * Weakest Day Marker +* **Priority**: Sprint 2 + +--- + +### Volatility (Standard Deviation) + +* **What it is**: Measures how strongly exchange rates fluctuate over time. Volatility is commonly approximated using the standard deviation of returns and is one of the most widely used measures of financial risk. +* **Why it is useful**: + * Introduces risk analysis + * Provides a bridge toward forecasting and ML + * Commonly used in financial analytics + * Adds a different perspective than trend or performance metrics +* **Chart Idea**: + * Date + * Rolling Volatility +* **Priority**: Sprint 2 + +--- + +## Recommended Sprint 2 Visualization + +Sprint 2 should extend the dashboard with one performance chart and one risk chart. + +The goal is to keep the number of charts low while still making the metrics visually useful. + +### Performance Chart + +Contains: + +* Cumulative Return +* Strongest Day Marker +* Weakest Day Marker + +Why? + +* Cumulative Return shows the overall movement during the selected period. +* Strongest and weakest days are events, not standalone time series. +* Showing them as markers avoids adding another unnecessary chart. + +--- + +### Risk Chart + +Contains: + +* Rolling Volatility + +Why? + +* Volatility describes fluctuation intensity. +* It answers a different question than trend or performance. +* Keeping it separate improves readability. + +--- + +### Resulting Sprint 2 Dashboard + +#### Performance Analytics + +* Cumulative Return +* Strongest Day Marker +* Weakest Day Marker + +#### Risk Analytics + +* Rolling Volatility + +--- + +### Resulting Dashboard Structure + +#### Trend Analytics + +```text +Exchange Rate ++ +Rolling Average ++ +Min / Max +``` + +```text +Daily Percentage Change +``` + +This structure keeps the dashboard compact while still allowing future expansion. + +## Explaination for the decision + +### First Implementation + +The first implementation should include: + +1. Daily Percentage Change +2. Rolling Average +3. Min / Max Rate + +Reasons: + +* Easy to calculate with Pandas +* Easy to visualize with Plotly +* Provide immediate analytical value +* Introduce core time-series concepts +* Create a strong foundation for future metrics + +### Future Direction + +Potential future additions: + +* Volatility +* Currency rankings +* Multi-currency comparisons +* Signal generation +* Forecasting experiments +* ML features + +These features should be added only after the core analytical dashboard is completed. + +## Analytics Architecture + +The analytics layer should remain independent from the UI layer. + +Future metrics should be implemented inside a dedicated analytics package rather than directly inside GUI code. + +### Suggested Structure + +```text +docs/ + +src/ + fx_converter_lab/ + cli/ + clients/ + gui/ + services/ + analytics/ + metrics/ + charts/ + domain/ + config.py + main.py + +tests/ +``` + +### Responsibilities + +* **analytics/metrics/** + + *Contains metric calculations:* + + * daily percentage change + * rolling average + * min / max + * cumulative return + * volatility + * strongest / weakest day + +* **analytics/charts/** + + *Contains chart preparation logic:* + + * trend charts + * performance charts + * risk charts + +* **gui/** + + *Responsible only for just displaying data.* + +--- + +### Long-Term Direction + +```text +clients +↓ +services +↓ +analytics + ├─ metrics + └─ charts +↓ +gui +``` diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 0000000..4df584c --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,118 @@ +# Roadmap + +Each roadmap phase is treated as a separate development sprint. +The roadmap is intentionally iterative: each sprint should leave the project in a usable and testable state. + +## Sprint 1 — Product Foundation & First Public Release + +**Status:** In progress + +Build a small but usable Python application with a clear structure, tests, documentation and first release readiness. + +Scope: + +- Modular Python package structure +- REST API client for live exchange rates +- Calculator and currency conversion logic +- Input validation and error handling +- Tkinter GUI prototype +- Legacy CLI/debug interface +- First pandas/matplotlib analytics prototype with mock time-series data +- Basic test suite +- README update +- First release instructions +- API key setup instructions +- Collaboration documentation through `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md` and `LICENSE` + +Outcome: +ARGUS can be run locally, tested, understood by other developers and used as a small desktop analytics prototype. + +### Sprint 2 — Market Analytics & Data Source Expansion + +**Status:** Planned + +Move from simple FX conversion toward broader market analytics. + +Scope: + +- Add stronger market metrics: + - cumulative return + - strongest / weakest day + - rolling volatility + - performance analytics + - risk analytics +- Extend the current dashboard without adding unnecessary chart noise +- Add or evaluate new data clients: + - Frankfurter for historical FX data + - yfinance for broader market data +- Replace or reduce dependency on the current ExchangeRate API where needed +- Improve pandas-based analysis workflows +- Add tests for metric calculations and data transformations +- Document metric definitions, assumptions and chart behavior + +Outcome: +ARGUS becomes a basic market analytics tool, not only a converter. + +### Sprint 3 — Storage, Web-Ready UI & Data Architecture + +**Status:** Planned + +Prepare ARGUS for persistent data workflows and a stronger product interface. + +Scope: + +- Add local storage layer: + - PostgreSQL, DuckDB, SQLite or Parquet depending on use case +- Store historical market data +- Separate ingestion, transformation, analytics and presentation layers more clearly +- Start NiceGUI as the main web-ready UI direction +- Keep Tkinter as legacy/prototype unless still useful +- Keep CLI as internal/debug interface only +- Add clearer architecture documentation +- Prepare the project for larger data workflows and external contributors + +Outcome: +ARGUS has a clearer data architecture and starts moving from local prototype toward a scalable analytics application. + +### Sprint 4 — Cloud, Pipelines & Portfolio-Grade Data Engineering + +**Status:** Future + +Turn ARGUS into a stronger end-to-end data engineering project. + +Scope: + +- Docker / Docker Compose +- Scheduled data ingestion +- Cloud storage or cloud database +- CI/CD improvements +- Data quality checks +- Basic pipeline orchestration +- Reporting layer +- Architecture diagram +- Deployment documentation + +Target workflow: + +```text +API → Ingestion → Storage → Transformation → Analysis → Visualization → CI/CD +``` + +### Sprint 5 — AI-Assisted Research & Agentic Monitoring + +**Status:** Future vision + +Add AI support only after the data, storage, service and reporting layers are stable. + +Scope: + +- LLM-assisted report summaries +- Explanation of unusual movements +- RAG over stored market notes, reports or documentation +- Agentic checks for data quality, anomalies and recurring market scans +- Human-in-the-loop signal review +- Automated monitoring workflows + +Outcome: + +ARGUS starts behaving like its name: a system that continuously watches market data, evaluates it and helps generate useful signals. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..665f963 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,35 @@ +[project] +name = "argus" +version = "0.1.0" +requires-python = ">=3.11" +dependencies = [ + "requests", + "python-dotenv", + "pandas", + "numpy", + "matplotlib", +] + +[project.optional-dependencies] +dev = [ + "pytest", + "ruff", +] + +[build-system] +requires = ["setuptools>=61"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["src"] + +[tool.ruff] +line-length = 88 +target-version = "py311" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 663bd1f..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -requests \ No newline at end of file diff --git a/src/api_test.py b/src/api_test.py deleted file mode 100644 index 799917e..0000000 --- a/src/api_test.py +++ /dev/null @@ -1,8 +0,0 @@ -import requests as req - -url = "https://v6.exchangerate-api.com/v6/8ef8d96608f75de7f8788b7a/latest/USD" - -resp = req.get(url) -data = resp.json() - -# print(data["conversion_rates"]["EUR"]) \ No newline at end of file diff --git a/src/argus/__init__.py b/src/argus/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/argus/analytics/__init__.py b/src/argus/analytics/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/argus/analytics/charts/__init__.py b/src/argus/analytics/charts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/argus/analytics/charts/trend_chart.py b/src/argus/analytics/charts/trend_chart.py new file mode 100644 index 0000000..57c5193 --- /dev/null +++ b/src/argus/analytics/charts/trend_chart.py @@ -0,0 +1,45 @@ +import matplotlib.pyplot as plt +import pandas as pd +from argus.services.timeseries_service import prepare_trend_analysis + + +def create_trendchart(curr: str, dates: pd.DataFrame): + df = pd.DataFrame() + df, min_max_rates = prepare_trend_analysis(curr, dates) + min_date = min_max_rates["min_date"][0] + min_rate = min_max_rates["min_rate"][0] + max_date = min_max_rates["max_date"][0] + max_rate = min_max_rates["max_rate"][0] + + # Rate and Rolling Average needs seperat x-Achse von Daily Percentage Chnage erhalten + fig, ax1 = plt.subplots(figsize=(5, 3.5), dpi=100) + + # Subplot 1 + ax1.plot(df["date"], df["rate"], color="black", label="Exchange Rate") + ax1.plot(df["date"], df["roll_avg"], color="blue", label="Rolling Average") + + # Scatter and Annote Min/Max Rate + ax1.scatter(min_date, min_rate, color="red") + ax1.scatter(max_date, max_rate, color="green") + ax1.annotate("Min", (min_date, min_rate)) + ax1.annotate("Max", (max_date, max_rate)) + + # Rotate date values for better visibillity + ax1.tick_params(axis="x", rotation=45) + + # Subplot 2 + ax2 = ax1.twinx() + bar_colors = ["green" if x >= 0 else "red" for x in df["daily_pct_change"]] + ax2.bar( + df["date"], + df["daily_pct_change"], + color=bar_colors, + alpha=0.4, + label="Daily Change", + ) + ax2.legend(loc="upper left") + ax2.set_ylabel("Percentage Scale") + + # Adjust the layout + fig.tight_layout() + return fig diff --git a/src/argus/analytics/metrics/__init__.py b/src/argus/analytics/metrics/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/argus/analytics/metrics/trend_metrics.py b/src/argus/analytics/metrics/trend_metrics.py new file mode 100644 index 0000000..9493ff3 --- /dev/null +++ b/src/argus/analytics/metrics/trend_metrics.py @@ -0,0 +1,24 @@ +import pandas as pd + + +def add_daily_percentage_change(df: pd.DataFrame) -> pd.DataFrame: + result = df.copy() + result["daily_pct_change"] = result["rate"].pct_change() * 100 + return result + + +def add_rolling_average(df: pd.DataFrame) -> pd.DataFrame: + result = df.copy() + result["roll_avg"] = result["rate"].rolling(window=3, min_periods=1).mean() + return result + + +def get_min_max_rates(df: pd.DataFrame) -> dict: + min_max = {"min_date": [], "min_rate": [], "max_date": [], "max_rate": []} + min_id = df["rate"].idxmin() + max_id = df["rate"].idxmax() + min_max["min_date"].append(df.loc[min_id, "date"]) + min_max["min_rate"].append(df.loc[min_id, "rate"]) + min_max["max_date"].append(df.loc[max_id, "date"]) + min_max["max_rate"].append(df.loc[max_id, "rate"]) + return min_max diff --git a/src/argus/clients/__init__.py b/src/argus/clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/argus/clients/exchangerate_client.py b/src/argus/clients/exchangerate_client.py new file mode 100644 index 0000000..71e9207 --- /dev/null +++ b/src/argus/clients/exchangerate_client.py @@ -0,0 +1,65 @@ +import requests as req +from argus.config import ( + EXCHANGE_RATE_BASE_URL, + EXCHANGE_RATE_API_KEY, + REQUEST_TIMEOUT_SECONDS, +) + + +def get_rates(curr1, curr2): + url = f"{EXCHANGE_RATE_BASE_URL}/{EXCHANGE_RATE_API_KEY}/pair/{curr1}/{curr2}" + data = {"result": "", "error_type": "", "conversion_rate": None} + + try: + resp = req.get(url, timeout=REQUEST_TIMEOUT_SECONDS) + resp.raise_for_status() + payload = resp.json() + + except req.exceptions.Timeout: + print("API hat zu lange gebraucht.") + return None + except req.exceptions.ConnectionError: + print("Keine Verbindung zur API.") + return None + except req.exceptions.RequestException as error: + print(f"Request fehlgeschlagen: {error}") + # Request fehlgeschlagen: 403 Client Error: Forbidden for url: https://v6.exchangerate-api.com/v6/None/pair/EUR/USD -> sollte nicht gezeigt werden!!! + return None + except ValueError: + print("Fehler beim Verarbeiten der API-Antwort.") + return None + except KeyError: + print("Unerwartete API-Antwortstruktur.") + return None + + if payload.get("result") == "success": + data["result"] = "success" + data["conversion_rate"] = payload.get("conversion_rate") + return data + else: + data["result"] = "error" + data["error_type"] = payload.get("error_type") + check_error(data["error_type"]) + return None + + +def check_error(err_type): + match err_type: + case "unsupported-code" | "malformed-request": + print("Ungültige Anfrage! Bitter versuchen Sie es später erneut.") + case "invalid-key": + print( + "Ungültiger API-Key! Checken Sie Ihren API-Key und versuchen Sie es erneut." + ) + case "inactive-account": + print( + "Inaktives Konto! Bitte auf exchangerate-api.com gehen und Konto aktivieren." + ) + case "quota-reached": + print( + "Anfrage-Limit erreicht! Bitte später erneut versuchen oder auf exchangerate-api.com upgraden." + ) + + +# Testen, ob die API funktioniert +# data = get_rates("EUR", "USD") diff --git a/src/argus/clients/mock_client.py b/src/argus/clients/mock_client.py new file mode 100644 index 0000000..31cc6db --- /dev/null +++ b/src/argus/clients/mock_client.py @@ -0,0 +1,47 @@ +import numpy as np + +mock_resp_data = { + "date": [ + "2026-06-01", + "2026-06-02", + "2026-06-03", + "2026-06-04", + "2026-06-05", + "2026-06-06", + "2026-06-07", + "2026-06-08", + "2026-06-09", + "2026-06-10", + "2026-06-11", + "2026-06-12", + "2026-06-13", + "2026-06-14", + "2026-06-15", + ], + "rate": [ + 1.08, + 1.10, + 1.14, + 1.12, + 1.10, + 1.07, + 1.08, + 1.08, + 1.08, + 1.10, + 1.12, + 1.12, + 1.15, + 1.12, + 1.13, + ], +} + + +def get_mock_timeseries(curr: str, date: str) -> int | float: + if curr == "USD": + index = mock_resp_data["date"].index(date) + rate = mock_resp_data["rate"][index] + return rate + else: + return np.nan diff --git a/src/argus/config.py b/src/argus/config.py new file mode 100644 index 0000000..81f45f7 --- /dev/null +++ b/src/argus/config.py @@ -0,0 +1,13 @@ +from pathlib import Path +import os +from dotenv import load_dotenv + +PROJECT_ROOT = Path(__file__).resolve().parents[2] + +load_dotenv(PROJECT_ROOT / ".env") + +EXCHANGE_RATE_API_KEY = os.getenv("EXCHANGE_RATE_API_KEY") + +EXCHANGE_RATE_BASE_URL = "https://v6.exchangerate-api.com/v6" + +REQUEST_TIMEOUT_SECONDS = 10 diff --git a/src/argus/domain/__init__.py b/src/argus/domain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/argus/domain/validation.py b/src/argus/domain/validation.py new file mode 100644 index 0000000..8e08d16 --- /dev/null +++ b/src/argus/domain/validation.py @@ -0,0 +1,185 @@ +VALID_CURRENCY_CODES = { + "AED", + "AFN", + "ALL", + "AMD", + "ANG", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BRL", + "BSD", + "BTN", + "BWP", + "BYN", + "BZD", + "CAD", + "CDF", + "CHF", + "CLF", + "CLP", + "CNH", + "CNY", + "COP", + "CRC", + "CUP", + "CVE", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EGP", + "ERN", + "ETB", + "EUR", + "FJD", + "FKP", + "FOK", + "GBP", + "GEL", + "GGP", + "GHS", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HRK", + "HTG", + "HUF", + "IDR", + "ILS", + "IMP", + "INR", + "IQD", + "ISK", + "JEP", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KID", + "KMF", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "LYD", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MRU", + "MUR", + "MVR", + "MWK", + "MXN", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SDG", + "SEK", + "SGD", + "SHP", + "SLE", + "SOS", + "SRD", + "SSP", + "STN", + "SYP", + "SZL", + "THB", + "TJS", + "TMT", + "TND", + "TOP", + "TRY", + "TTD", + "TVD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "UYU", + "UZS", + "VES", + "VND", + "VUV", + "WST", + "XAF", + "XCD", + "XDR", + "XOF", + "XPF", + "YER", + "ZAR", + "ZMW", + "ZWL", +} + +VALID_OPS = {"+", "-", "*", "/", "%", "**"} + + +def normalize_input_string(input: str) -> str: + return input.strip().upper() + + +def parse_amount(value: str) -> float | None: + try: + return float(value) + except ValueError: + return None + + +def is_valid_curr_code(code: str) -> bool: + return code in VALID_CURRENCY_CODES + + +def is_valid_op(op: str) -> bool: + return op in VALID_OPS diff --git a/src/argus/gui/app.py b/src/argus/gui/app.py new file mode 100644 index 0000000..141fec5 --- /dev/null +++ b/src/argus/gui/app.py @@ -0,0 +1,265 @@ +import tkinter as tk +import pandas as pd +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +from argus.analytics.charts.trend_chart import create_trendchart +from argus.services.calculator_service import calc, check_op +from argus.services.conversion_service import convert, check_currency +from argus.domain.validation import parse_amount + + +def on_close() -> None: + if trend_chart_widget is not None: + trend_chart_widget.destroy() + + root.quit() + root.destroy() + + +def hide_trend_chart() -> None: + if trend_chart_widget is not None: + trend_chart_widget.pack_forget() + + +def show_menu() -> None: + app_frame.pack_forget() + calc_frame.pack_forget() + conv_frame.pack_forget() + hide_trend_chart() + + menu_frame.pack(side="right", fill=tk.BOTH, expand=True) + + +def show_calc() -> None: + conv_frame.pack_forget() + hide_trend_chart() + menu_frame.pack_forget() + + app_frame.pack(fill=tk.BOTH, expand=True) + sidebar.pack(side="top", fill="x") + content.pack(side="top", fill=tk.BOTH, expand=True) + + calc_frame.pack(fill=tk.BOTH, expand=True) + + +def show_conv() -> None: + calc_frame.pack_forget() + hide_trend_chart() + menu_frame.pack_forget() + + app_frame.pack(fill=tk.BOTH, expand=True) + sidebar.pack(side="top", fill="x") + content.pack(side="top", fill=tk.BOTH, expand=True) + + conv_frame.pack(fill=tk.BOTH, expand=True) + + +def show_trend() -> None: + global trend_canvas + global trend_chart_widget + + mock_dates = { + "date": [ + "2026-06-01", + "2026-06-02", + "2026-06-03", + "2026-06-04", + "2026-06-05", + "2026-06-06", + "2026-06-07", + "2026-06-08", + "2026-06-09", + "2026-06-10", + "2026-06-11", + "2026-06-12", + "2026-06-13", + "2026-06-14", + "2026-06-15", + ] + } + mock_dates = pd.DataFrame(mock_dates) + mock_curr = "USD" + + calc_frame.pack_forget() + conv_frame.pack_forget() + menu_frame.pack_forget() + + app_frame.pack(fill=tk.BOTH, expand=True) + sidebar.pack(side="top", fill="x") + content.pack(side="top", fill=tk.BOTH, expand=True) + + if trend_canvas is None: + fig = create_trendchart(mock_curr, mock_dates) + fig.set_size_inches(7, 4) + + trend_canvas = FigureCanvasTkAgg(fig, master=content) + trend_chart_widget = trend_canvas.get_tk_widget() + + if trend_chart_widget is None: + return + trend_canvas.draw() + trend_chart_widget.pack(fill=tk.BOTH, expand=True) + + +def act_calculate() -> None: + resp1 = num1.get() + resp1 = parse_amount(resp1) + + resp2 = num2.get() + resp2 = parse_amount(resp2) + + op = op_e.get() + + if resp1 is None: + result_calc_label.config(text="Please enter a valid number for 'Number 1'.") + return + + if resp2 is None: + result_calc_label.config(text="Please enter a valid number for 'Number 2'.") + return + + if check_op(op) is False: + result_calc_label.config(text="Please enter a valid operation for 'Operation'.") + return + + result = calc(resp1, resp2, op) + + if result is None: + result_calc_label.config(text="Division by zero is not possible.") + return + + result_calc_label.config(text=f"{resp1} {op} {resp2} = {result}") + + +def act_convert() -> None: + resp1 = check_currency(curr1.get()) + resp2 = check_currency(curr2.get()) + amount = parse_amount(amount_e.get()) + + if resp1 is None: + result_conv_label.config(text="Please enter a valid currency for 'Currency 1'") + return + + if resp2 is None: + result_conv_label.config(text="Please enter a valid currency for 'Currency 2'") + return + + if amount is None: + result_conv_label.config( + text="Please enter a valid amount in the 'Amount' field" + ) + return + + response = convert(amount, resp1, resp2) + + if response is None: + result_conv_label.config(text="Currency conversion error") + return + + result = response + result_conv_label.config( + text=f"The exchange rate from {resp1} to {resp2} for {amount} {resp1} is {result} {resp2}" + ) + + +def app() -> None: + root.mainloop() + + +# Window +root = tk.Tk() +root.title("FX-Converter Lab") +root.geometry("800x600") # Width x Length +root.protocol("WM_DELETE_WINDOW", on_close) + +# Frames +menu_frame = tk.Frame(root) +app_frame = tk.Frame(root) +sidebar = tk.Frame(app_frame) +content = tk.Frame(app_frame) +conv_frame = tk.Frame(content) +calc_frame = tk.Frame(content) + +# Trend chart is loaded lazily +trend_canvas = None +trend_chart_widget = None + +# Labels +menu_label = tk.Label(menu_frame, text="Menu", font=("Arial", 20)) +calc_label = tk.Label(calc_frame, text="Calculator", font=("Arial", 20)) +conv_label = tk.Label(conv_frame, text="Converter", font=("Arial", 20)) + +num1_label = tk.Label(calc_frame, text="Number 1:") +num2_label = tk.Label(calc_frame, text="Number 2:") +op_label = tk.Label(calc_frame, text="Operation:") + +amount_label = tk.Label(conv_frame, text="Amount:") +curr1_label = tk.Label(conv_frame, text="Currency 1:") +curr2_label = tk.Label(conv_frame, text="Currency 2:") + +result_calc_label = tk.Label(calc_frame) +result_conv_label = tk.Label(conv_frame) + +menu_label.pack(pady=20) + +calc_label.grid(pady=20, column=1, row=0) +conv_label.grid(pady=20, column=1, row=0) + +num1_label.grid(pady=5, column=0, row=1) +num2_label.grid(pady=5, column=0, row=2) +op_label.grid(pady=5, column=0, row=3) + +amount_label.grid(pady=5, column=0, row=1) +curr1_label.grid(pady=5, column=0, row=2) +curr2_label.grid(pady=5, column=0, row=3) + +result_calc_label.grid(column=1, row=5) +result_conv_label.grid(column=1, row=5) + +# Entries +num1 = tk.Entry(calc_frame) +num2 = tk.Entry(calc_frame) +op_e = tk.Entry(calc_frame) + +amount_e = tk.Entry(conv_frame) +curr1 = tk.Entry(conv_frame) +curr2 = tk.Entry(conv_frame) + +num1.grid(pady=5, column=1, row=1) +num2.grid(pady=5, column=1, row=2) +op_e.grid(pady=5, column=1, row=3) + +amount_e.grid(pady=5, column=1, row=1) +curr1.grid(pady=5, column=1, row=2) +curr2.grid(pady=5, column=1, row=3) + +num1.focus() + +# Buttons +from_menu_calc = tk.Button(menu_frame, text="Calculator", command=show_calc) +from_menu_conv = tk.Button(menu_frame, text="Converter", command=show_conv) +from_menu_trend_chart = tk.Button(menu_frame, text="Trend Chart", command=show_trend) + +from_sidebar_calc = tk.Button(sidebar, text="Calculator", command=show_calc) +from_sidebar_conv = tk.Button(sidebar, text="Converter", command=show_conv) +from_sidebar_trend_chart = tk.Button(sidebar, text="Trend Chart", command=show_trend) +return_menu = tk.Button(sidebar, text="Back to menu", command=show_menu) + +click_calculate = tk.Button(calc_frame, text="Calculate", command=act_calculate) +click_converter = tk.Button(conv_frame, text="Convert", command=act_convert) + +from_menu_calc.pack(fill="x", padx=50, pady=15) +from_menu_conv.pack(fill="x", padx=50, pady=15) +from_menu_trend_chart.pack(fill="x", padx=50, pady=15) + +from_sidebar_calc.pack(side="left") +from_sidebar_conv.pack(side="left") +from_sidebar_trend_chart.pack(side="left") +return_menu.pack(side="left") + +click_calculate.grid(column=1, row=4) +click_converter.grid(column=1, row=4) + +show_menu() + +if __name__ == "__main__": + app() diff --git a/src/argus/main.py b/src/argus/main.py new file mode 100644 index 0000000..a6fd534 --- /dev/null +++ b/src/argus/main.py @@ -0,0 +1,8 @@ +from argus.gui.app import app + + +def main() -> None: + app() + + +main() diff --git a/src/argus/services/__init__.py b/src/argus/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/argus/services/calculator_service.py b/src/argus/services/calculator_service.py new file mode 100644 index 0000000..40c1022 --- /dev/null +++ b/src/argus/services/calculator_service.py @@ -0,0 +1,31 @@ +from argus.domain.validation import is_valid_op + + +def check_op(op: str) -> bool: + # Tipp: Liste verwenden, wenn mehr als 2 Optionen für etwas besteht + if is_valid_op(op): + return True + else: + return False + + +def calc(num1: float, num2: float, op: str) -> float | None: + # Tipp: Auf Ifelse verzichten, wenn davon mehr als 3 Stück entstehen + match op: + case "+": + return num1 + num2 + case "-": + return num1 - num2 + case "*": + return num1 * num2 + case "/": + try: + return num1 / num2 + except ZeroDivisionError: + return None + case "%": + return num1 % num2 + case "**": + return num1**num2 + case _: + return None diff --git a/src/argus/services/conversion_service.py b/src/argus/services/conversion_service.py new file mode 100644 index 0000000..7d16d76 --- /dev/null +++ b/src/argus/services/conversion_service.py @@ -0,0 +1,29 @@ +from argus.clients import exchangerate_client as ex_client +from argus.domain.validation import normalize_input_string, is_valid_curr_code + + +# This function has to be moved to dmoain +def check_currency(question: str) -> str | None: + resp = normalize_input_string(question) + + if is_valid_curr_code(resp): + return resp + + return None + + +def get_conv_rate(resp1: str, resp2: str) -> float | None: + data = ex_client.get_rates(resp1, resp2) + + if data is None: + return None + + return float(data["conversion_rate"]) + + +def convert(amount: float, resp1: str, resp2: str) -> float | None: + data = get_conv_rate(resp1, resp2) + if data is not None: + return amount * data + else: + return None diff --git a/src/argus/services/timeseries_service.py b/src/argus/services/timeseries_service.py new file mode 100644 index 0000000..6ef34ae --- /dev/null +++ b/src/argus/services/timeseries_service.py @@ -0,0 +1,21 @@ +import pandas as pd +from argus.clients.mock_client import get_mock_timeseries +from argus.analytics.metrics.trend_metrics import ( + add_rolling_average, + add_daily_percentage_change, + get_min_max_rates, +) + + +def prepare_trend_analysis( + mock_curr: str, df: pd.DataFrame +) -> tuple[pd.DataFrame, dict]: + df["rate"] = 0.0 + # For each date one API call to get the rate + for i in range(len(df)): + date = str(df.loc[i, "date"]) + df.loc[i, "rate"] = get_mock_timeseries(mock_curr, date) + df = add_daily_percentage_change(df) + df = add_rolling_average(df) + min_max_rates = get_min_max_rates(df) + return df, min_max_rates diff --git a/src/calc.py b/src/calc.py deleted file mode 100644 index 7216c0a..0000000 --- a/src/calc.py +++ /dev/null @@ -1,117 +0,0 @@ -import api_test as api - -def getRates(): - data = api.data["conversion_rates"] - return data - -def getNum(): - # Tipp: Bool-Werte müssen nicht in Klammern stehen - while True: - try: - num = float(input("Gib die Zahl ein:")) - return num - except ValueError: - print("Bitte eine Zahl eingeben!!") - -def getOperator(): - # Tipp: Liste verwenden, wenn mehr als 2 Optionen für etwas besteht - valid_ops = ['+', '-', '*', '/', '%', '**'] - while True: - op = input("Welche Rechenoperation wollen Sie anwenden? (+,-,*,/,%,**) ") - if op in valid_ops: - return op - else: - print("Bitte erneut eingeben!") - -def getCurr(): - data = getRates() - while True: - resp = input("Welche Währung wollen Sie?") - if resp in data: - return resp - else: - print("Bitt eine gültige Währung eingeben") - -def convert(): - data = getRates() - - -def calc(num1,num2,op): - # Tipp: Auf Ifelse verzichten, wenn davon mehr als 3 Stück entstehen - match op: - case '+': - return num1 + num2 - case '-': - return num1 - num2 - case '*': - return num1 * num2 - case '/': - try: - return num1 / num2 - except ZeroDivisionError: - print("Divison durch null nicht möglich!") - return None - case '%': - return num1 % num2 - case '**': - return num1 ** num2 - case _: - return None - -def displayConvert(): - while True: - # Tipp: Man muss nicht die Variable weiterreichen, die zum Speichern des Return-Values da ist - amount = getNum() - from_curr = getCurr() - to_curr = getCurr() - result = convert(amount,from_curr,to_curr) - print(f"Wechelkurs von {from_curr} zu {to_curr} mit {amount} {from_curr} ergibt {result}") - while True: - repeat = input("Wollen Sie eine neue Berechnung ausführen? (y/n) ") - match repeat: - case 'y': - break - case 'n': - return - case _: - print("Bitte 'y' oder 'n' eingeben!") - -def displayCalc(): - num1 = 0 - num2 = 0 - while True: - # Tipp: Man muss nicht die Variable weiterreichen, die zum Speichern des Return-Values da ist - num1 = getNum() - num2 = getNum() - op = getOperator() - result = calc(num1,num2,op) - print(f"Berechnung: {num1} {op} {num2} = {result}") - while True: - repeat = input("Wollen Sie eine neue Berechnung ausführen? (y/n) ") - match repeat: - case 'y': - break - case 'n': - return - case _: - print("Bitte 'y' oder 'n' eingeben!") - -def main(): - initConv = "Willkommen zum Calculator mit Wechselkurberechnung!" - print(initConv) - while True: - print("Menü: \n(1) Calculator \n(2) Exchnage Rate \n(3) Exit") - option = input("Wählen Sie bitte eine Option aus") - match option: - case '1': - displayCalc() - case '2': - displayConvert() - case '3': - break - case _: - print("Bitte nur '1','2' oder '3' eingeben!") - -main() - - \ No newline at end of file diff --git a/src/legacy/cli/__init__.py b/src/legacy/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/legacy/cli/interface.py b/src/legacy/cli/interface.py new file mode 100644 index 0000000..fa9931a --- /dev/null +++ b/src/legacy/cli/interface.py @@ -0,0 +1,94 @@ +from argus.services.calculator_service import check_op, calc +from argus.services.conversion_service import convert, check_currency +from argus.domain.validation import parse_amount + + +def display_convert() -> None: + while True: + amount = parse_amount(input("Amount: ")) + if amount is None: + print("Please enter a valid amount in the 'Amount' field.") + continue + break + while True: + resp1 = check_currency(input("First currency: ")) + if resp1 is None: + print("Unvalid currency! Please enter again.") + continue + break + while True: + resp2 = check_currency(input("Second currency: ")) + if resp2 is None: + print("Unvalid currency! Please enter again..") + continue + break + while True: + response = convert(amount, resp1, resp2) + if response is None: + print("Error with the API request! Please try again later.") + continue + break + + result = response + print( + f"The exchange rate from {resp1} to {resp2} for {amount} {resp1} is {result} {resp2}" + ) + + if return_to_menu() == "y": + return + + +def display_calc() -> None: + while True: + num1 = parse_amount(input("First number: ")) + if num1 is None: + print("Please enter again!") + continue + break + while True: + num2 = parse_amount(input("Second number: ")) + if num2 is None: + print("Please enter again!") + continue + break + while True: + op = input("Which operation do you wanna apply? (+,-,*,/,%,**) ") + if check_op(op) is False: + print("Please enter again!") + continue + break + + result = calc(num1, num2, op) + print(f"Berechnung: {num1} {op} {num2} = {result}") + + if return_to_menu() == "y": + return + + +def return_to_menu() -> str: + while True: + repeat = input("Would you like to return to the menu? (y/n) ") + match repeat: + case "y": + return "y" + case "n": + return "n" + case _: + print("Please enter 'y' or 'n'!") + + +def dev_interface() -> None: + initConv = "Welcome to the Calculator with Currency Conversion!" + print(initConv) + while True: + print("Menu: \n(1) Calculator \n(2) Exchnage Rate \n(3) Exit") + option = input("Please select an option: ") + match option: + case "1": + display_calc() + case "2": + display_convert() + case "3": + break + case _: + print("Please enter only '1','2' or '3'!") diff --git a/src/legacy/debug_main.py b/src/legacy/debug_main.py new file mode 100644 index 0000000..8d82035 --- /dev/null +++ b/src/legacy/debug_main.py @@ -0,0 +1,9 @@ +from legacy.cli.interface import dev_interface + + +def start_cli() -> None: + dev_interface() + + +if __name__ == "__main__": + start_cli() diff --git a/temp/basic_calc.py b/temp/basic_calc.py deleted file mode 100644 index ed215de..0000000 --- a/temp/basic_calc.py +++ /dev/null @@ -1,12 +0,0 @@ - - -x = 5 -y = 5 - - -print("Addition: ",x + y) -print("Subtaktion: ",x - y) -print("Multiplikation: ",x * y) -print("Division: ",x / y) -print("Potenz: ",x ** y) -print("Modulo: ",x % y) \ No newline at end of file diff --git a/temp/basic_input.py b/temp/basic_input.py deleted file mode 100644 index 115867b..0000000 --- a/temp/basic_input.py +++ /dev/null @@ -1,5 +0,0 @@ -name = input("Wie heißt du? ") # Inpu() gibt nur String zurück -> Eingaben zum Rechnen gehen nicht einfach so -print("Hallo",name) - -zahl = int(input("Gib eine Zahl ein, die mit 5 addiert wird: ")) -print("Ergebnis",zahl + 5) \ No newline at end of file diff --git a/temp/exercise_1.py b/temp/exercise_1.py deleted file mode 100644 index 8dcad5e..0000000 --- a/temp/exercise_1.py +++ /dev/null @@ -1,6 +0,0 @@ -age = 22 -favNum = 8 - -print("Mein Alter:",age) -print("Mein Lieblingszahl:",favNum) -print("Summe:",age + favNum) \ No newline at end of file diff --git a/tests/test_exchangerate_client.py b/tests/test_exchangerate_client.py new file mode 100644 index 0000000..51e8174 --- /dev/null +++ b/tests/test_exchangerate_client.py @@ -0,0 +1,128 @@ +import requests as req +from unittest.mock import Mock +from argus.clients.exchangerate_client import get_rates, check_error + + +def test_check_currency_timeout(monkeypatch): + def test_get_resp(url, timeout): + raise req.exceptions.Timeout() + + monkeypatch.setattr("requests.get", test_get_resp) + + data = get_rates("EUR", "USD") + assert data is None + + +def test_check_currency_connection_error(monkeypatch): + def test_get_resp(url, timeout): + raise req.exceptions.ConnectionError() + + monkeypatch.setattr("requests.get", test_get_resp) + + data = get_rates("EUR", "USD") + assert data is None + + +def test_check_currency_request_exception(monkeypatch): + def test_get_resp(url, timeout): + raise req.exceptions.RequestException("Testfehler") + + monkeypatch.setattr("requests.get", test_get_resp) + + data = get_rates("EUR", "USD") + assert data is None + + +def test_check_currency_value_error(monkeypatch): + test_resp = Mock() + test_resp.raise_for_status.return_value = None + test_resp.json.side_effect = ValueError("Ungültige JSON-Antwort") + + def test_get_resp(url, timeout): + return test_resp + + monkeypatch.setattr("requests.get", test_get_resp) + + data = get_rates("EUR", "USD") + assert data is None + + +def test_check_currency_key_error(monkeypatch): + test_resp = Mock() + test_resp.raise_for_status.return_value = None + test_resp.json.return_value = { + "result": "", + "error_type": "", + # "conversion_rate" fehlt absichtlich + } + + def test_get_resp(url, timeout): + return test_resp + + monkeypatch.setattr("requests.get", test_get_resp) + + data = get_rates("EUR", "USD") + assert data is None + + +def test_check_currency_valid(monkeypatch): + test_resp = Mock() + test_resp.raise_for_status.return_value = None + test_resp.json.return_value = { + "result": "success", + "error_type": "", + "conversion_rate": 1.2, + } + + def test_get_resp(url, timeout): + return test_resp + + monkeypatch.setattr("requests.get", test_get_resp) + + data = get_rates("EUR", "USD") + assert data == {"result": "success", "error_type": "", "conversion_rate": 1.2} + + +def test_check_currency_invalid(monkeypatch): + test_resp = Mock() + test_resp.raise_for_status.return_value = None + test_resp.json.return_value = { + "result": "error", + "error_type": "unsupported-code", + "conversion_rate": None, + } + + def test_get_resp(url, timeout): + return test_resp + + monkeypatch.setattr("requests.get", test_get_resp) + + data = get_rates("EUR", "USD") + assert data is None + + +def test_check_error(capsys): + check_error("unsupported-code") + captured = capsys.readouterr() + assert captured.out == "Ungültige Anfrage! Bitter versuchen Sie es später erneut.\n" + + check_error("invalid-key") + captured = capsys.readouterr() + assert ( + captured.out + == "Ungültiger API-Key! Checken Sie Ihren API-Key und versuchen Sie es erneut.\n" + ) + + check_error("inactive-account") + captured = capsys.readouterr() + assert ( + captured.out + == "Inaktives Konto! Bitte auf exchangerate-api.com gehen und Konto aktivieren.\n" + ) + + check_error("quota-reached") + captured = capsys.readouterr() + assert ( + captured.out + == "Anfrage-Limit erreicht! Bitte später erneut versuchen oder auf exchangerate-api.com upgraden.\n" + ) diff --git a/tests/test_timeseries_service.py b/tests/test_timeseries_service.py new file mode 100644 index 0000000..dbc5675 --- /dev/null +++ b/tests/test_timeseries_service.py @@ -0,0 +1,30 @@ +import pandas as pd +import pandas.testing as pdt +import numpy as np +from argus.services.timeseries_service import prepare_trend_analysis + + +def test_is_pct_change_added(): + test_curr = "USD" + test_dates = { + "date": ["2026-06-01", "2026-06-02", "2026-06-03"], + } + test_dates = pd.DataFrame(test_dates) + + expect_result = { + "date": ["2026-06-01", "2026-06-02", "2026-06-03"], + "rate": [1.08, 1.1, 1.14], + "daily_pct_change": [np.nan, 1.85185185185186, 3.6363636363636154], + "roll_avg": [1.08, 1.09, 1.1066666666666667], + } + expect_dict = { + "min_date": ["2026-06-01"], + "min_rate": [1.08], + "max_date": ["2026-06-03"], + "max_rate": [1.14], + } + result_df, result_dict = prepare_trend_analysis(test_curr, test_dates) + expect_df = pd.DataFrame(expect_result) + + pdt.assert_frame_equal(result_df, expect_df) + assert result_dict == expect_dict diff --git a/tests/test_trend_metrics.py b/tests/test_trend_metrics.py new file mode 100644 index 0000000..fe175bd --- /dev/null +++ b/tests/test_trend_metrics.py @@ -0,0 +1,62 @@ +import pandas as pd +import pandas.testing as pdt +import numpy as np +from argus.analytics.metrics.trend_metrics import ( + add_daily_percentage_change, + add_rolling_average, + get_min_max_rates, +) + + +def test_is_pct_change_added(): + test_timesseries = { + "date": ["2026-05-01", "2026-05-02", "2026-05-03"], + "rate": [1.00, 1.10, 1.21], + } + + expect_result = { + "date": ["2026-05-01", "2026-05-02", "2026-05-03"], + "rate": [1.00, 1.10, 1.21], + "daily_pct_change": [np.nan, 10.0, 10.0], + } + test_df = pd.DataFrame(test_timesseries) + result_df = add_daily_percentage_change(test_df) + expect_df = pd.DataFrame(expect_result) + + pdt.assert_frame_equal(result_df, expect_df) + + +def test_is_roll_avg_added(): + test_timesseries = { + "date": ["2026-05-01", "2026-05-02", "2026-05-03"], + "rate": [1.00, 1.10, 1.21], + } + + expect_result = { + "date": ["2026-05-01", "2026-05-02", "2026-05-03"], + "rate": [1.00, 1.10, 1.21], + "roll_avg": [1.00, 1.05, 1.1033333333333333333333333333333], + } + test_df = pd.DataFrame(test_timesseries) + result_df = add_rolling_average(test_df) + expect_df = pd.DataFrame(expect_result) + + pdt.assert_frame_equal(result_df, expect_df) + + +def test_get_min_max_(): + test_timesseries = { + "date": ["2026-05-01", "2026-05-02", "2026-05-03"], + "rate": [1.00, 1.10, 1.21], + } + + min_max = { + "min_date": ["2026-05-01"], + "min_rate": [1.00], + "max_date": ["2026-05-03"], + "max_rate": [1.21], + } + test_df = pd.DataFrame(test_timesseries) + result_dict = get_min_max_rates(test_df) + + assert result_dict == min_max diff --git a/tests/test_validation_domain.py b/tests/test_validation_domain.py new file mode 100644 index 0000000..a5bd41f --- /dev/null +++ b/tests/test_validation_domain.py @@ -0,0 +1,48 @@ +from argus.domain.validation import ( + is_valid_op, + is_valid_curr_code, + parse_amount, + normalize_input_string, +) + + +def test_op_is_valid(): + data = is_valid_op("+") + + assert data is True + + +def test_op_is_not_valid(): + data = is_valid_op("LOL") + + assert data is False + + +def test_curr_is_valid(): + data = is_valid_curr_code("AOA") + + assert data is True + + +def test_curr_is_not_valid(): + data = is_valid_curr_code("LOL") + + assert data is False + + +def test_parse_amount_valid(): + data = parse_amount("20.2") + + assert data == 20.2 + + +def test_parse_amount_not_valid(): + data = parse_amount("fuck") + + assert data is None + + +def test_normalizing_string(): + data = normalize_input_string(" lOl ") + + assert data == "LOL"