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 9ff1e03..9a1d8a1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,13 +2,14 @@ __pycache__ .pytest_cache *.pyc -/src/fx_converter_lab.egg-info +/src/argus.egg-info + # IDE settings -.vscode/ +.vscode -# Temporary files (exercise files) -temp/ +# ruff cache +.ruff_cache # Virtual environment .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 5a520e8..2914fa1 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,10 @@ ARGUS is currently focused on building a clean local foundation: --- +## 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. @@ -82,7 +86,7 @@ Each roadmap phase is treated as a separate development sprint. The roadmap desc ```text docs/ src/ - fx_converter_lab/ + argus/ analytics/ charts/ metrics/ @@ -207,8 +211,8 @@ Recommended for development: Clone the repository: ```bash -git clone https://github.com/BytecodeBrewer/fx-converter-lab.git -cd fx-converter-lab +git clone https://github.com/BytecodeBrewer/argus.git +cd argus ``` Create a virtual environment: @@ -237,6 +241,12 @@ Install the project in editable mode: 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. @@ -280,7 +290,7 @@ The `.env` file must stay local and should never be committed. Start the current Tkinter GUI: ```bash -python -m fx_converter_lab.main +python -m argus.main ``` This starts the local ARGUS prototype with calculator, currency conversion and basic analytics views. diff --git a/pyproject.toml b/pyproject.toml index 3acf581..665f963 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "argus" -version = "0.9.0" +version = "0.1.0" requires-python = ">=3.11" dependencies = [ "requests", @@ -13,6 +13,7 @@ dependencies = [ [project.optional-dependencies] dev = [ "pytest", + "ruff", ] [build-system] @@ -27,4 +28,8 @@ package-dir = {"" = "src"} [tool.pytest.ini_options] testpaths = ["tests"] -pythonpath = ["src"] \ No newline at end of file +pythonpath = ["src"] + +[tool.ruff] +line-length = 88 +target-version = "py311" \ No newline at end of file diff --git a/src/argus/analytics/charts/trend_chart.py b/src/argus/analytics/charts/trend_chart.py index 4fd14f0..57c5193 100644 --- a/src/argus/analytics/charts/trend_chart.py +++ b/src/argus/analytics/charts/trend_chart.py @@ -2,14 +2,15 @@ import pandas as pd from argus.services.timeseries_service import prepare_trend_analysis -def create_trendchart(curr:str,dates:pd.DataFrame): + +def create_trendchart(curr: str, dates: pd.DataFrame): df = pd.DataFrame() - df,min_max_rates = prepare_trend_analysis(curr,dates) + 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) @@ -18,21 +19,27 @@ def create_trendchart(curr:str,dates:pd.DataFrame): 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.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) + 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.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 + # Adjust the layout fig.tight_layout() - return fig \ No newline at end of file + return fig diff --git a/src/argus/analytics/metrics/trend_metrics.py b/src/argus/analytics/metrics/trend_metrics.py index 6b50d43..9493ff3 100644 --- a/src/argus/analytics/metrics/trend_metrics.py +++ b/src/argus/analytics/metrics/trend_metrics.py @@ -1,26 +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() + 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_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 \ No newline at end of file + return min_max diff --git a/src/argus/clients/exchangerate_client.py b/src/argus/clients/exchangerate_client.py index 269f49e..71e9207 100644 --- a/src/argus/clients/exchangerate_client.py +++ b/src/argus/clients/exchangerate_client.py @@ -1,19 +1,20 @@ import requests as req -from argus.config import (EXCHANGE_RATE_BASE_URL ,EXCHANGE_RATE_API_KEY,REQUEST_TIMEOUT_SECONDS) +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 - } - + 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 @@ -30,7 +31,7 @@ def get_rates(curr1, curr2): except KeyError: print("Unerwartete API-Antwortstruktur.") return None - + if payload.get("result") == "success": data["result"] = "success" data["conversion_rate"] = payload.get("conversion_rate") @@ -40,18 +41,25 @@ def get_rates(curr1, curr2): 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': + 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.") + 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") \ No newline at end of file +# data = get_rates("EUR", "USD") diff --git a/src/argus/clients/mock_client.py b/src/argus/clients/mock_client.py index 92b7eb9..31cc6db 100644 --- a/src/argus/clients/mock_client.py +++ b/src/argus/clients/mock_client.py @@ -1,26 +1,47 @@ -import pandas as pd 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 - ], + "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"): + if curr == "USD": index = mock_resp_data["date"].index(date) rate = mock_resp_data["rate"][index] - return rate + return rate else: - return np.nan \ No newline at end of file + return np.nan diff --git a/src/argus/config.py b/src/argus/config.py index 0e56780..81f45f7 100644 --- a/src/argus/config.py +++ b/src/argus/config.py @@ -10,4 +10,4 @@ EXCHANGE_RATE_BASE_URL = "https://v6.exchangerate-api.com/v6" -REQUEST_TIMEOUT_SECONDS = 10 \ No newline at end of file +REQUEST_TIMEOUT_SECONDS = 10 diff --git a/src/argus/domain/validation.py b/src/argus/domain/validation.py index dc1626c..8e08d16 100644 --- a/src/argus/domain/validation.py +++ b/src/argus/domain/validation.py @@ -1,49 +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", + "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", + "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", + "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", + "XAF", + "XCD", + "XDR", + "XOF", + "XPF", "YER", - "ZAR", "ZMW", "ZWL", + "ZAR", + "ZMW", + "ZWL", } -VALID_OPS = {'+', '-', '*', '/', '%', '**'} +VALID_OPS = {"+", "-", "*", "/", "%", "**"} -def normalize_input_string(input:str) -> str: + +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: + +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 \ No newline at end of file + +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 index fd1709f..141fec5 100644 --- a/src/argus/gui/app.py +++ b/src/argus/gui/app.py @@ -6,6 +6,7 @@ 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() @@ -57,11 +58,22 @@ def show_trend() -> None: 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" + "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) @@ -76,7 +88,7 @@ def show_trend() -> None: content.pack(side="top", fill=tk.BOTH, expand=True) if trend_canvas is None: - fig = create_trendchart(mock_curr,mock_dates) + fig = create_trendchart(mock_curr, mock_dates) fig.set_size_inches(7, 4) trend_canvas = FigureCanvasTkAgg(fig, master=content) @@ -132,7 +144,9 @@ def act_convert() -> None: return if amount is None: - result_conv_label.config(text="Please enter a valid amount in the 'Amount' field") + result_conv_label.config( + text="Please enter a valid amount in the 'Amount' field" + ) return response = convert(amount, resp1, resp2) @@ -248,4 +262,4 @@ def app() -> None: show_menu() if __name__ == "__main__": - app() \ No newline at end of file + app() diff --git a/src/argus/main.py b/src/argus/main.py index dd69583..a6fd534 100644 --- a/src/argus/main.py +++ b/src/argus/main.py @@ -1,8 +1,8 @@ from argus.gui.app import app + def main() -> None: app() -main() - \ No newline at end of file +main() diff --git a/src/argus/services/calculator_service.py b/src/argus/services/calculator_service.py index 6715f4b..40c1022 100644 --- a/src/argus/services/calculator_service.py +++ b/src/argus/services/calculator_service.py @@ -1,6 +1,7 @@ from argus.domain.validation import is_valid_op -def check_op(op:str) -> bool: + +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 @@ -8,23 +9,23 @@ def check_op(op:str) -> bool: 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 \ No newline at end of file +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 index 02fd0e8..7d16d76 100644 --- a/src/argus/services/conversion_service.py +++ b/src/argus/services/conversion_service.py @@ -1,26 +1,29 @@ from argus.clients import exchangerate_client as ex_client -from argus.domain.validation import normalize_input_string,is_valid_curr_code +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: +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: + +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) + +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 \ No newline at end of file + return None diff --git a/src/argus/services/timeseries_service.py b/src/argus/services/timeseries_service.py index 206f4b3..6ef34ae 100644 --- a/src/argus/services/timeseries_service.py +++ b/src/argus/services/timeseries_service.py @@ -1,16 +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 +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]: + +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"]) + 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 - - + return df, min_max_rates diff --git a/src/legacy/cli/interface.py b/src/legacy/cli/interface.py index bbe748d..fa9931a 100644 --- a/src/legacy/cli/interface.py +++ b/src/legacy/cli/interface.py @@ -1,5 +1,5 @@ -from argus.services.calculator_service import check_op,calc -from argus.services.conversion_service import convert,check_currency +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 @@ -23,18 +23,21 @@ def display_convert() -> None: continue break while True: - response = convert(amount,resp1,resp2) + 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': + 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: ")) @@ -54,24 +57,26 @@ def display_calc() -> None: print("Please enter again!") continue break - - result = calc(num1,num2,op) + + result = calc(num1, num2, op) print(f"Berechnung: {num1} {op} {num2} = {result}") - - if return_to_menu() == 'y': + + 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 "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) @@ -79,11 +84,11 @@ def dev_interface() -> None: print("Menu: \n(1) Calculator \n(2) Exchnage Rate \n(3) Exit") option = input("Please select an option: ") match option: - case '1': + case "1": display_calc() - case '2': + case "2": display_convert() - case '3': + case "3": break case _: - print("Please enter only '1','2' or '3'!") \ No newline at end of file + print("Please enter only '1','2' or '3'!") diff --git a/src/legacy/debug_main.py b/src/legacy/debug_main.py index 2c3dd50..8d82035 100644 --- a/src/legacy/debug_main.py +++ b/src/legacy/debug_main.py @@ -4,5 +4,6 @@ def start_cli() -> None: dev_interface() + if __name__ == "__main__": - start_cli() \ No newline at end of file + start_cli() diff --git a/tests/test_exchangerate_client.py b/tests/test_exchangerate_client.py index e92bd5f..51e8174 100644 --- a/tests/test_exchangerate_client.py +++ b/tests/test_exchangerate_client.py @@ -1,7 +1,7 @@ -import pytest import requests as req from unittest.mock import Mock -from argus.clients.exchangerate_client import * +from argus.clients.exchangerate_client import get_rates, check_error + def test_check_currency_timeout(monkeypatch): def test_get_resp(url, timeout): @@ -12,6 +12,7 @@ def test_get_resp(url, timeout): 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() @@ -21,6 +22,7 @@ def test_get_resp(url, timeout): 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") @@ -30,6 +32,7 @@ def test_get_resp(url, timeout): 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 @@ -43,6 +46,7 @@ def test_get_resp(url, timeout): 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 @@ -60,13 +64,14 @@ def test_get_resp(url, timeout): 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 + "conversion_rate": 1.2, } def test_get_resp(url, timeout): @@ -75,11 +80,8 @@ def test_get_resp(url, timeout): monkeypatch.setattr("requests.get", test_get_resp) data = get_rates("EUR", "USD") - assert data == { - "result": "success", - "error_type": "", - "conversion_rate": 1.2 - } + assert data == {"result": "success", "error_type": "", "conversion_rate": 1.2} + def test_check_currency_invalid(monkeypatch): test_resp = Mock() @@ -87,7 +89,7 @@ def test_check_currency_invalid(monkeypatch): test_resp.json.return_value = { "result": "error", "error_type": "unsupported-code", - "conversion_rate": None + "conversion_rate": None, } def test_get_resp(url, timeout): @@ -96,7 +98,8 @@ def test_get_resp(url, timeout): monkeypatch.setattr("requests.get", test_get_resp) data = get_rates("EUR", "USD") - assert data == None + assert data is None + def test_check_error(capsys): check_error("unsupported-code") @@ -105,13 +108,21 @@ def test_check_error(capsys): 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" + 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" + 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" - + 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 index e8594be..dbc5675 100644 --- a/tests/test_timeseries_service.py +++ b/tests/test_timeseries_service.py @@ -1,30 +1,30 @@ -import pytest 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"], + "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] + "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] + "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) + 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 \ No newline at end of file + 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 index 7ca85c8..fe175bd 100644 --- a/tests/test_trend_metrics.py +++ b/tests/test_trend_metrics.py @@ -1,59 +1,62 @@ -import pytest 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 +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] + "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] - + "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) + 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] + "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] - + "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) + 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] + "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] + "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 \ No newline at end of file + assert result_dict == min_max diff --git a/tests/test_validation_domain.py b/tests/test_validation_domain.py index c1c17c2..a5bd41f 100644 --- a/tests/test_validation_domain.py +++ b/tests/test_validation_domain.py @@ -1,40 +1,48 @@ -import pytest -import requests as req -from unittest.mock import Mock -from argus.domain.validation import * +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('+') + data = is_valid_op("+") assert data is True + def test_op_is_not_valid(): - data = is_valid_op('LOL') + data = is_valid_op("LOL") assert data is False + def test_curr_is_valid(): - data = is_valid_curr_code('AOA') + data = is_valid_curr_code("AOA") assert data is True + def test_curr_is_not_valid(): - data = is_valid_curr_code('LOL') + data = is_valid_curr_code("LOL") assert data is False + def test_parse_amount_valid(): - data = parse_amount('20.2') + data = parse_amount("20.2") assert data == 20.2 + def test_parse_amount_not_valid(): - data = parse_amount('fuck') + data = parse_amount("fuck") assert data is None -def test_normalizing_string(): - data = normalize_input_string(' lOl ') - assert data == 'LOL' +def test_normalizing_string(): + data = normalize_input_string(" lOl ") + assert data == "LOL"