├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── feature_request.md
│ └── questions-or-inquiries.md
└── workflows
│ ├── codeql.yml
│ ├── markdown.yml
│ ├── matrix-publisher.yml
│ └── version.yml
├── .gitignore
├── .pre-commit-config.yaml
├── LICENSE
├── MANIFEST.in
├── README.md
├── dev_scripts
├── api_v2.py
├── dependencies.py
├── display.py
└── version.py
├── docker-compose.yml
├── docs
├── .nojekyll
├── CNAME
├── README.html
├── README.md
├── _sources
│ ├── README.md.txt
│ └── index.rst.txt
├── _static
│ ├── _sphinx_javascript_frameworks_compat.js
│ ├── basic.css
│ ├── classic.css
│ ├── doctools.js
│ ├── documentation_options.js
│ ├── file.png
│ ├── jquery-3.6.0.js
│ ├── jquery.js
│ ├── language_data.js
│ ├── minus.png
│ ├── plus.png
│ ├── pygments.css
│ ├── searchtools.js
│ ├── sidebar.js
│ ├── static.css
│ ├── underscore-1.13.1.js
│ └── underscore.js
├── genindex.html
├── index.html
├── objects.inv
├── py-modindex.html
├── search.html
└── searchindex.js
├── docs_gen
├── Makefile
├── conf.py
├── index.rst
├── make.bat
└── static.css
├── jarvis
├── __init__.py
├── _preexec
│ └── keywords_handler.py
├── api
│ ├── entrypoint.py
│ ├── logger.py
│ ├── main.py
│ ├── models
│ │ ├── authenticator.py
│ │ ├── modals.py
│ │ └── settings.py
│ ├── routers
│ │ ├── basics.py
│ │ ├── favicon.ico
│ │ ├── fileio.py
│ │ ├── investment.py
│ │ ├── offline.py
│ │ ├── proxy_service.py
│ │ ├── routes.py
│ │ ├── secure_send.py
│ │ ├── speech_synthesis.py
│ │ ├── stats.py
│ │ ├── stock_analysis.py
│ │ ├── stock_monitor.py
│ │ ├── surveillance.py
│ │ └── telegram.py
│ ├── server.py
│ ├── squire
│ │ ├── scheduler.py
│ │ ├── stockanalysis_squire.py
│ │ ├── stockmonitor_squire.py
│ │ ├── surveillance_squire.py
│ │ └── timeout_otp.py
│ └── triggers
│ │ ├── stock_monitor.py
│ │ └── stock_report.py
├── executors
│ ├── alarm.py
│ ├── automation.py
│ ├── background_task.py
│ ├── car.py
│ ├── comm_squire.py
│ ├── commander.py
│ ├── communicator.py
│ ├── conditions.py
│ ├── connection.py
│ ├── controls.py
│ ├── crontab.py
│ ├── custom_conditions.py
│ ├── date_time.py
│ ├── display_functions.py
│ ├── face.py
│ ├── files.py
│ ├── functions.py
│ ├── github.py
│ ├── guard.py
│ ├── internet.py
│ ├── ios_functions.py
│ ├── lights.py
│ ├── lights_squire.py
│ ├── listener_controls.py
│ ├── location.py
│ ├── method.py
│ ├── offline.py
│ ├── others.py
│ ├── port_handler.py
│ ├── process_map.py
│ ├── processor.py
│ ├── remind.py
│ ├── resource_tracker.py
│ ├── restrictions.py
│ ├── robinhood.py
│ ├── simulator.py
│ ├── static_responses.py
│ ├── system.py
│ ├── telegram.py
│ ├── thermostat.py
│ ├── todo_list.py
│ ├── tv.py
│ ├── tv_controls.py
│ ├── unconditional.py
│ ├── volume.py
│ ├── vpn_server.py
│ ├── weather.py
│ ├── weather_monitor.py
│ └── word_match.py
├── indicators
│ ├── acknowledgement.mp3
│ ├── alarm.mp3
│ ├── coin.mp3
│ ├── end.mp3
│ └── start.mp3
├── lib
│ ├── install_darwin.sh
│ ├── install_linux.sh
│ ├── install_windows.sh
│ ├── installer.py
│ ├── version_locked_requirements.txt
│ ├── version_pinned_requirements.txt
│ └── version_upgrade_requirements.txt
├── main.py
├── modules
│ ├── audio
│ │ ├── listener.py
│ │ ├── speaker.py
│ │ ├── tts_stt.py
│ │ └── voices.py
│ ├── auth_bearer.py
│ ├── builtin_overrides.py
│ ├── cache
│ │ └── cache.py
│ ├── camera
│ │ └── camera.py
│ ├── conditions
│ │ ├── conversation.py
│ │ └── keywords.py
│ ├── crontab
│ │ └── expression.py
│ ├── database
│ │ └── database.py
│ ├── dictionary
│ │ └── dictionary.py
│ ├── exceptions.py
│ ├── facenet
│ │ └── face.py
│ ├── lights
│ │ ├── preset_values.py
│ │ └── smart_lights.py
│ ├── logger.py
│ ├── meetings
│ │ ├── events.py
│ │ ├── ics.py
│ │ └── ics_meetings.py
│ ├── microphone
│ │ ├── graph_mic.py
│ │ └── recognizer.py
│ ├── models
│ │ ├── classes.py
│ │ ├── enums.py
│ │ ├── models.py
│ │ ├── squire.py
│ │ └── validators.py
│ ├── peripherals.py
│ ├── retry
│ │ └── retry.py
│ ├── speaker
│ │ └── speak.py
│ ├── telegram
│ │ ├── audio_handler.py
│ │ ├── bot.py
│ │ ├── file_handler.py
│ │ ├── settings.py
│ │ └── webhook.py
│ ├── temperature
│ │ └── temperature.py
│ ├── templates
│ │ ├── Modelfile
│ │ ├── car_report.html
│ │ ├── email.html
│ │ ├── email_OTP.html
│ │ ├── email_stock_alert.html
│ │ ├── email_threat_audio.html
│ │ ├── email_threat_image.html
│ │ ├── email_threat_image_audio.html
│ │ ├── robinhood.html
│ │ ├── surveillance.html
│ │ ├── templates.py
│ │ └── win_wifi_config.xml
│ ├── timeout
│ │ └── timeout.py
│ ├── transformer
│ │ └── gpt.py
│ ├── tv
│ │ ├── lg.py
│ │ └── roku.py
│ ├── utils
│ │ ├── shared.py
│ │ ├── support.py
│ │ └── util.py
│ └── wakeonlan
│ │ └── wakeonlan.py
└── scripts
│ ├── applauncher.scpt
│ ├── calendar.scpt
│ └── outlook.scpt
├── pre_commit.sh
├── pyproject.toml
├── release_notes.rst
├── tests
├── __init__.py
├── api_test.py
├── constant_test.py
├── listener_test.py
├── main_test.py
├── speaker_test.py
└── speech_synthesis_test.py
└── update-toml.py
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help me improve
4 | title: Bug report
5 | labels: bug
6 | assignees: thevickypedia
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Type of error
16 | 2. When does it occur
17 | 3. Steps followed as per [README.md][readme]?
18 |
19 | **Expected behavior**
20 | A clear and concise description of what you expected to happen.
21 |
22 | **Screenshots**
23 | If applicable, add screenshots to help explain your problem.
24 |
25 | **Hosted device (please complete the following information):**
26 | - OS: [e.g. macOS]
27 | - Version [e.g. 10.13]
28 | - Module [e.g. [modules/lights/smart_lights.py][eg_lights]]
29 | - Line number [e.g. 424]
30 |
31 | **Additional context**
32 | Add any other context about the problem here.
33 |
34 | [readme]: https://github.com/thevickypedia/Jarvis/blob/master/README.md
35 | [eg_lights]: https://github.com/thevickypedia/Jarvis/blob/master/jarvis/modules/lights/smart_lights.py
36 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: Feature request
5 | labels: enhancement
6 | assignees: thevickypedia
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/questions-or-inquiries.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Questions or inquiries
3 | about: Ask a question regarding Jarvis
4 | title: Questions or inquiries
5 | labels: help wanted, question
6 | assignees: thevickypedia
7 |
8 | ---
9 |
10 | Did you find the [README][readme] useful?
11 | Did you check out the [wiki][wiki] page?
12 |
13 | ## Questions?
14 | 1.
15 | 2.
16 | 3.
17 |
18 | [readme]: https://github.com/thevickypedia/Jarvis/blob/master/README.md
19 | [wiki]: https://github.com/thevickypedia/Jarvis/wiki
20 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | name: codeql-scan
2 |
3 | permissions:
4 | actions: read
5 | contents: read
6 | security-events: write
7 |
8 | on:
9 | workflow_dispatch:
10 | push:
11 | branches:
12 | - "master"
13 | paths:
14 | - jarvis/**
15 | - 'pyproject.toml'
16 |
17 | jobs:
18 | analyze:
19 | runs-on: ubuntu-latest
20 | strategy:
21 | fail-fast: false
22 | matrix:
23 | language: [ 'python' ]
24 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
25 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
26 |
27 | steps:
28 | - name: Checkout repository
29 | uses: actions/checkout@v4
30 |
31 | - name: Setup python
32 | uses: actions/setup-python@v5
33 | with:
34 | python-version: '3.11'
35 |
36 | # Initializes the CodeQL tools for scanning.
37 | - name: Initialize CodeQL
38 | uses: github/codeql-action/init@v3
39 | with:
40 | queries: +security-extended
41 | languages: ${{ matrix.language }}
42 | # If you wish to specify custom queries, you can do so here or in a config file.
43 | # By default, queries listed here will override any specified in a config file.
44 | # Prefix the list here with "+" to use these queries and those in the config file.
45 |
46 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
47 | # queries: security-extended,security-and-quality
48 |
49 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
50 | # If this step fails, then you should remove it and run the build manually (see below)
51 | - name: Autobuild
52 | uses: github/codeql-action/autobuild@v3
53 |
54 | # ℹ️ Command-line programs to run using the OS shell.
55 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
56 |
57 | # If the Autobuild fails above, remove it and uncomment the following three lines.
58 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
59 |
60 | # - run: |
61 | # echo "Run, Build Application using script"
62 | # ./location_of_script_within_repo/buildscript.sh
63 |
64 | - name: Perform CodeQL Analysis
65 | uses: github/codeql-action/analyze@v3
66 |
--------------------------------------------------------------------------------
/.github/workflows/markdown.yml:
--------------------------------------------------------------------------------
1 | name: none-shall-pass
2 |
3 | permissions:
4 | contents: read
5 |
6 | on:
7 | workflow_dispatch:
8 | push:
9 | branches:
10 | - master
11 | paths:
12 | - '**/*.md'
13 |
14 | jobs:
15 | none-shall-pass:
16 | runs-on: thevickypedia-lite
17 | steps:
18 | - uses: actions/checkout@v4
19 | - uses: thevickypedia/none-shall-pass@v5
20 |
--------------------------------------------------------------------------------
/.github/workflows/matrix-publisher.yml:
--------------------------------------------------------------------------------
1 | name: matrix-publisher
2 |
3 | on:
4 | release:
5 | types:
6 | - published
7 | push:
8 | branches:
9 | - master
10 | paths:
11 | - jarvis/**
12 | - pyproject.toml
13 | workflow_dispatch:
14 | inputs:
15 | dry_run:
16 | type: choice
17 | description: Dry run mode
18 | required: true
19 | options:
20 | - "true"
21 | - "false"
22 |
23 | jobs:
24 | pypi-publisher:
25 | runs-on: thevickypedia-lite
26 | strategy:
27 | fail-fast: false
28 | matrix:
29 | project_name:
30 | - jarvis-ironman
31 | - jarvis-bot
32 | - jarvis-nlp
33 | - natural-language-ui
34 |
35 | steps:
36 | - name: Checkout
37 | uses: actions/checkout@v4
38 |
39 | - name: Set dry-run
40 | run: |
41 | if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
42 | echo "::notice title=DryRun::Setting dry run to ${{ inputs.dry_run }} for '${{ github.event_name }}' event"
43 | echo "dry_run=${{ inputs.dry_run }}" >> $GITHUB_ENV
44 | elif [[ "${{ github.event_name }}" == "push" ]]; then
45 | echo "::notice title=DryRun::Setting dry run to true for '${{ github.event_name }}' event"
46 | echo "dry_run=true" >> $GITHUB_ENV
47 | else
48 | echo "::notice title=DryRun::Setting dry run to false for '${{ github.event_name }}' event"
49 | echo "dry_run=false" >> $GITHUB_ENV
50 | fi
51 | shell: bash
52 |
53 | - name: "Update metadata: ${{ matrix.project_name }}"
54 | run: |
55 | python -m pip install toml
56 | python update-toml.py
57 | shell: bash
58 | env:
59 | FILE_PATH: "pyproject.toml"
60 | PROJECT_NAME: "${{ matrix.project_name }}"
61 |
62 | - uses: thevickypedia/pypi-publisher@v4
63 | with:
64 | dry-run: ${{ env.dry_run }}
65 | token: ${{ secrets.PYPI_TOKEN }}
66 | checkout: 'false'
67 |
--------------------------------------------------------------------------------
/.github/workflows/version.yml:
--------------------------------------------------------------------------------
1 | name: package-resolver
2 |
3 | permissions:
4 | contents: read
5 |
6 | on:
7 | workflow_dispatch:
8 | push:
9 | branches:
10 | - master
11 | paths:
12 | - "**/*.txt"
13 | - "dev_scripts/*"
14 | - ".github/workflows/version.yml"
15 |
16 | jobs:
17 | dependencies:
18 | runs-on: thevickypedia-lite
19 | steps:
20 | - uses: actions/checkout@v4
21 | - run: python dev_scripts/dependencies.py
22 | shell: bash
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # python generated
2 | .idea
3 | .DS_Store
4 | __pycache__
5 | .pytest_cache
6 | *venv*/
7 |
8 | # module generated
9 | did.bin
10 |
11 | # WIP: Dockerfile and it's requirements file
12 | Dockerfile
13 | docker_requirements.txt
14 |
15 | # File IO operations
16 | fileio
17 |
18 | # Log files
19 | logs
20 |
21 | # Recordings
22 | recordings
23 |
24 | # training directory with images of faces for recognition
25 | train
26 |
27 | # Storage for images of intruders
28 | threat
29 |
30 | # .keep files for Alarms and Reminders
31 | alarm
32 | reminder
33 |
34 | # pip build
35 | build/
36 | jarvis_ironman.egg-info/
37 |
38 | # sphinx docs builder within docs_gen
39 | docs_gen/_build
40 | docs_gen/_static
41 |
42 | # credentials stored locally
43 | .env
44 | *.json
45 | *.yaml
46 |
47 | # volume exec file for Windows
48 | SetVol.exe
49 |
50 | # audio converter for Telegram API (Windows)
51 | ffmpeg
52 |
53 | # future work
54 | hidden
55 | temp*.*
56 |
57 | # pypi package files
58 | dist
59 | jarvis_ironman.egg-info
60 |
61 | # Create package
62 | package.py
63 | package.zip
64 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | fail_fast: true
2 | exclude: ^(notebooks/|scripts/|.github/|docs/|dev_scripts/)
3 | repos:
4 | - repo: https://github.com/pre-commit/pre-commit-hooks
5 | rev: v5.0.0
6 | hooks:
7 | - id: check-added-large-files
8 | - id: check-ast
9 | - id: check-builtin-literals
10 | - id: check-case-conflict
11 | - id: check-docstring-first
12 | - id: check-executables-have-shebangs
13 | - id: check-json
14 | - id: check-merge-conflict
15 | - id: check-shebang-scripts-are-executable
16 | - id: check-toml
17 | - id: check-vcs-permalinks
18 | - id: check-xml
19 | - id: check-yaml
20 | - id: debug-statements
21 | - id: detect-aws-credentials
22 | - id: detect-private-key
23 | - id: end-of-file-fixer
24 | - id: file-contents-sorter
25 | - id: fix-byte-order-marker
26 | - id: forbid-submodules
27 | - id: mixed-line-ending
28 | - id: name-tests-test
29 | - id: requirements-txt-fixer
30 | - id: sort-simple-yaml
31 | - id: trailing-whitespace
32 |
33 | - repo: https://github.com/psf/black
34 | # Latest version is just ugly and does the opposite of a linter
35 | rev: 22.3.0
36 | hooks:
37 | - id: black
38 | exclude: docs_gen/
39 |
40 | - repo: https://github.com/PyCQA/flake8
41 | rev: 7.1.1
42 | hooks:
43 | - id: flake8
44 | additional_dependencies:
45 | - flake8-docstrings
46 | - flake8-sfs
47 | args: [ --max-line-length=120, --extend-ignore=SFS3 D107 SFS301 D100 D104 D401 SFS101 SFS201 D412 ]
48 |
49 | - repo: https://github.com/PyCQA/isort
50 | rev: 5.13.2
51 | hooks:
52 | - id: isort
53 | args: [ --profile, black ]
54 |
55 | - repo: local
56 | hooks:
57 | - id: pytest_docs
58 | name: run pytest and generate runbook
59 | entry: /bin/bash pre_commit.sh
60 | language: system
61 | pass_filenames: false
62 | always_run: true
63 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Vignesh Rao
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | # MANIFEST.in is mandatory due to a wide range of filetypes
2 | global-exclude .env
3 | global-exclude *.yaml
4 | global-exclude .DS_Store
5 | prune **/.idea
6 | prune */__pycache__
7 | include LICENSE
8 | include README.md
9 | include jarvis/lib/*
10 | include jarvis/scripts/*
11 | include jarvis/indicators/*
12 | recursive-include jarvis/api *
13 | recursive-include jarvis/modules *
14 |
--------------------------------------------------------------------------------
/dev_scripts/display.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 |
4 | class Format:
5 | """Initiates Format object to define variables that print the message in a certain format.
6 |
7 | >>> Format
8 |
9 | """
10 |
11 | BOLD = "\033[1m"
12 | UNDERLINE = "\033[4m"
13 | ITALIC = "\x1B[3m"
14 |
15 |
16 | class Colors:
17 | """Initiates Colors object to define variables that print the message in a certain color.
18 |
19 | >>> Colors
20 |
21 | """
22 |
23 | VIOLET = "\033[95m"
24 | BLUE = "\033[94m"
25 | CYAN = "\033[96m"
26 | GREEN = "\033[92m"
27 | YELLOW = "\033[93m"
28 | RED = "\033[91m"
29 | END = "\033[0m"
30 | LIGHT_GREEN = "\033[32m"
31 | LIGHT_YELLOW = "\033[2;33m"
32 | LIGHT_RED = "\033[31m"
33 |
34 |
35 | class Echo:
36 | """Initiates Echo objects to set text to certain format and color based on the level of the message.
37 |
38 | >>> Echo
39 |
40 | """
41 |
42 | def __init__(self):
43 | self._colors = Colors
44 | self._format = Format
45 |
46 | def debug(self, msg: Any) -> None:
47 | """Method for debug statement.
48 |
49 | Args:
50 | msg: Message to be printed.
51 | """
52 | print(f"{self._colors.LIGHT_GREEN}{msg}{self._colors.END}")
53 |
54 | def info(self, msg: Any) -> None:
55 | """Method for info statement.
56 |
57 | Args:
58 | msg: Message to be printed.
59 | """
60 | print(f"{self._colors.GREEN}{msg}{self._colors.END}")
61 |
62 | def warning(self, msg: Any) -> None:
63 | """Method for warning statement.
64 |
65 | Args:
66 | msg: Message to be printed.
67 | """
68 | print(f"{self._colors.YELLOW}{msg}{self._colors.END}")
69 |
70 | def error(self, msg: Any) -> None:
71 | """Method for error statement.
72 |
73 | Args:
74 | msg: Message to be printed.
75 | """
76 | print(f"{self._colors.RED}{msg}{self._colors.END}")
77 |
78 | def critical(self, msg: Any) -> None:
79 | """Method for critical statement.
80 |
81 | Args:
82 | msg: Message to be printed.
83 | """
84 | print(f"{self._colors.RED}{self._format.BOLD}{msg}{self._colors.END}")
85 |
86 | def fatal(self, msg: Any) -> None:
87 | """Method for fatal statement.
88 |
89 | Args:
90 | msg: Message to be printed.
91 | """
92 | print(f"{self._colors.RED}{self._format.BOLD}{msg}{self._colors.END}")
93 |
94 |
95 | echo = Echo()
96 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | speech-synthesis:
3 | image: thevickypedia/speech-synthesis:latest
4 | container_name: speech-synthesis
5 | restart: always
6 | ports:
7 | - "5002:5002"
8 | environment:
9 | HOME: "${HOME}"
10 | working_dir: "${PWD}"
11 | volumes:
12 | - "${HOME}:${HOME}"
13 | - /usr/share/ca-certificates:/usr/share/ca-certificates
14 | - /etc/ssl/certs:/etc/ssl/certs
15 | user: "${UID}:${GID}"
16 | stdin_open: true
17 | tty: true
18 |
--------------------------------------------------------------------------------
/docs/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevickypedia/Jarvis/1298228d8ed6471f12b7848f0620abea15dbc0a1/docs/.nojekyll
--------------------------------------------------------------------------------
/docs/CNAME:
--------------------------------------------------------------------------------
1 | jarvis-docs.vigneshrao.com
--------------------------------------------------------------------------------
/docs/_static/documentation_options.js:
--------------------------------------------------------------------------------
1 | var DOCUMENTATION_OPTIONS = {
2 | URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
3 | VERSION: '',
4 | LANGUAGE: 'en',
5 | COLLAPSE_INDEX: false,
6 | BUILDER: 'html',
7 | FILE_SUFFIX: '.html',
8 | LINK_SUFFIX: '.html',
9 | HAS_SOURCE: true,
10 | SOURCELINK_SUFFIX: '.txt',
11 | NAVIGATION_WITH_KEYS: false,
12 | SHOW_SEARCH_SUMMARY: true,
13 | ENABLE_SEARCH_SHORTCUTS: true,
14 | };
--------------------------------------------------------------------------------
/docs/_static/file.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevickypedia/Jarvis/1298228d8ed6471f12b7848f0620abea15dbc0a1/docs/_static/file.png
--------------------------------------------------------------------------------
/docs/_static/minus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevickypedia/Jarvis/1298228d8ed6471f12b7848f0620abea15dbc0a1/docs/_static/minus.png
--------------------------------------------------------------------------------
/docs/_static/plus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevickypedia/Jarvis/1298228d8ed6471f12b7848f0620abea15dbc0a1/docs/_static/plus.png
--------------------------------------------------------------------------------
/docs/_static/sidebar.js:
--------------------------------------------------------------------------------
1 | /*
2 | * sidebar.js
3 | * ~~~~~~~~~~
4 | *
5 | * This script makes the Sphinx sidebar collapsible.
6 | *
7 | * .sphinxsidebar contains .sphinxsidebarwrapper. This script adds
8 | * in .sphixsidebar, after .sphinxsidebarwrapper, the #sidebarbutton
9 | * used to collapse and expand the sidebar.
10 | *
11 | * When the sidebar is collapsed the .sphinxsidebarwrapper is hidden
12 | * and the width of the sidebar and the margin-left of the document
13 | * are decreased. When the sidebar is expanded the opposite happens.
14 | * This script saves a per-browser/per-session cookie used to
15 | * remember the position of the sidebar among the pages.
16 | * Once the browser is closed the cookie is deleted and the position
17 | * reset to the default (expanded).
18 | *
19 | * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.
20 | * :license: BSD, see LICENSE for details.
21 | *
22 | */
23 |
24 | const initialiseSidebar = () => {
25 |
26 |
27 |
28 |
29 | // global elements used by the functions.
30 | const bodyWrapper = document.getElementsByClassName("bodywrapper")[0]
31 | const sidebar = document.getElementsByClassName("sphinxsidebar")[0]
32 | const sidebarWrapper = document.getElementsByClassName('sphinxsidebarwrapper')[0]
33 | const sidebarButton = document.getElementById("sidebarbutton")
34 | const sidebarArrow = sidebarButton.querySelector('span')
35 |
36 | // for some reason, the document has no sidebar; do not run into errors
37 | if (typeof sidebar === "undefined") return;
38 |
39 | const flipArrow = element => element.innerText = (element.innerText === "»") ? "«" : "»"
40 |
41 | const collapse_sidebar = () => {
42 | bodyWrapper.style.marginLeft = ".8em";
43 | sidebar.style.width = ".8em"
44 | sidebarWrapper.style.display = "none"
45 | flipArrow(sidebarArrow)
46 | sidebarButton.title = _('Expand sidebar')
47 | window.localStorage.setItem("sidebar", "collapsed")
48 | }
49 |
50 | const expand_sidebar = () => {
51 | bodyWrapper.style.marginLeft = ""
52 | sidebar.style.removeProperty("width")
53 | sidebarWrapper.style.display = ""
54 | flipArrow(sidebarArrow)
55 | sidebarButton.title = _('Collapse sidebar')
56 | window.localStorage.setItem("sidebar", "expanded")
57 | }
58 |
59 | sidebarButton.addEventListener("click", () => {
60 | (sidebarWrapper.style.display === "none") ? expand_sidebar() : collapse_sidebar()
61 | })
62 |
63 | if (!window.localStorage.getItem("sidebar")) return
64 | const value = window.localStorage.getItem("sidebar")
65 | if (value === "collapsed") collapse_sidebar();
66 | else if (value === "expanded") expand_sidebar();
67 | }
68 |
69 | if (document.readyState !== "loading") initialiseSidebar()
70 | else document.addEventListener("DOMContentLoaded", initialiseSidebar)
--------------------------------------------------------------------------------
/docs/_static/static.css:
--------------------------------------------------------------------------------
1 | .sphinxsidebarwrapper {
2 | overflow-y: scroll;
3 | }
4 |
--------------------------------------------------------------------------------
/docs/objects.inv:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevickypedia/Jarvis/1298228d8ed6471f12b7848f0620abea15dbc0a1/docs/objects.inv
--------------------------------------------------------------------------------
/docs/search.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Search — Jarvis documentation
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
Search
48 |
49 |
50 |
51 |
52 | Please activate JavaScript to enable the search
53 | functionality.
54 |
55 |
56 |
57 |
58 |
59 |
60 | Searching for multiple words only shows matches that contain
61 | all words.
62 |
63 |
64 |
65 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
86 |
87 |
88 |
101 |
105 |
106 |
--------------------------------------------------------------------------------
/docs_gen/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/docs_gen/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # This file only contains a selection of the most common options. For a full
4 | # list see the documentation:
5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
6 |
7 | # -- Path setup --------------------------------------------------------------
8 |
9 | # If extensions (or modules to document with autodoc) are in another directory,
10 | # add these directories to sys.path here. If the directory is relative to the
11 | # documentation root, use os.path.abspath to make it absolute, like shown here.
12 |
13 | import os
14 | import pathlib
15 | import sys
16 |
17 | # Since pytest and docs run parallely, change the current dir and insert it to sys.path at index 0
18 | os.chdir(pathlib.Path(__file__).parent.parent)
19 | sys.path.insert(0, os.getcwd())
20 |
21 | # -- Project information -----------------------------------------------------
22 |
23 | project = 'Jarvis'
24 | copyright = '2021, Vignesh Rao'
25 | author = 'Vignesh Rao'
26 |
27 | # -- General configuration ---------------------------------------------------
28 |
29 | # Add any Sphinx extension module names here, as strings. They can be
30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
31 | # ones.
32 | extensions = [
33 | 'sphinx.ext.napoleon', # certain styles of doc strings
34 | 'sphinx.ext.autodoc', # generates from doc strings
35 | 'recommonmark', # supports markdown integration
36 | ]
37 |
38 | # https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html#configuration
39 | napoleon_google_docstring = True
40 | napoleon_use_param = False
41 |
42 | # Add any paths that contain templates here, relative to this directory.
43 | templates_path = ['_templates']
44 |
45 | # List of patterns, relative to source directory, that match files and
46 | # directories to ignore when looking for source files.
47 | # This pattern also affects html_static_path and html_extra_path.
48 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
49 |
50 | # -- Options for HTML output -------------------------------------------------
51 |
52 | # The theme to use for HTML and HTML Help pages. See the documentation for
53 | # a list of builtin themes.
54 | # https://www.sphinx-doc.org/en/master/usage/theming.html#builtin-themes
55 | html_theme = 'classic'
56 | html_theme_options = {
57 | "body_max_width": "80%"
58 | }
59 |
60 | # Add any paths that contain custom static files (such as style sheets) here,
61 | # relative to this directory. They are copied after the builtin static files,
62 | # so a file named "default.css" will overwrite the builtin "default.css".
63 | html_static_path = ['_static']
64 |
65 | # Add docstrings from __init__ method
66 | # Reference: https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#confval-autoclass_content
67 | # autoclass_content = 'both'
68 |
69 | # Include private methods/functions
70 | # Reference: https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#confval-autodoc_default_options
71 | autodoc_default_options = {"members": True, "undoc-members": True, "private-members": True}
72 |
73 | # Add support to mark down files in sphinx documentation
74 | # Reference: https://www.sphinx-doc.org/en/1.5.3/markdown.html
75 | source_suffix = {
76 | '.rst': 'restructuredtext',
77 | '.txt': 'markdown',
78 | '.md': 'markdown',
79 | }
80 |
81 | # Retain the function/member order
82 | # Reference: https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#confval-autodoc_member_order
83 | autodoc_member_order = 'bysource'
84 |
85 | # Make left pane scroll
86 | html_css_files = ["static.css"]
87 |
--------------------------------------------------------------------------------
/docs_gen/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 |
13 | if "%1" == "" goto help
14 |
15 | %SPHINXBUILD% >NUL 2>NUL
16 | if errorlevel 9009 (
17 | echo.
18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
19 | echo.installed, then set the SPHINXBUILD environment variable to point
20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
21 | echo.may add the Sphinx directory to PATH.
22 | echo.
23 | echo.If you don't have Sphinx installed, grab it from
24 | echo.http://sphinx-doc.org/
25 | exit /b 1
26 | )
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/docs_gen/static.css:
--------------------------------------------------------------------------------
1 | .sphinxsidebarwrapper {
2 | overflow-y: scroll;
3 | }
4 |
--------------------------------------------------------------------------------
/jarvis/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | from multiprocessing import current_process
4 | from typing import Callable
5 |
6 | version = "6.0.0"
7 |
8 |
9 | def __preflight_check__() -> Callable:
10 | """Startup validator that imports Jarvis' main module to validate all dependencies' installation status.
11 |
12 | Returns:
13 | Callable:
14 | Returns the ``start`` function.
15 | """
16 | try:
17 | import pynotification # noqa: F401
18 |
19 | from ._preexec import keywords_handler # noqa: F401
20 | from .main import start # noqa: F401
21 | except ImportError:
22 | try:
23 | # noinspection PyUnboundLocalVariable
24 | pynotification.pynotifier(
25 | title="First time user?",
26 | dialog=True,
27 | message="Please run\n\njarvis install",
28 | )
29 | except NameError:
30 | pass
31 | raise UserWarning("Missing dependencies!\n\nPlease run\n\tjarvis install")
32 | return start
33 |
34 |
35 | def start() -> None:
36 | """Start function to invoke Jarvis programmatically."""
37 | init = __preflight_check__()
38 | init()
39 |
40 |
41 | def commandline() -> None:
42 | """Starter function to invoke Jarvis using commandline."""
43 | # This is to validate that only 'jarvis' command triggers this function and not invoked by other functions
44 | assert sys.argv[0].endswith("jarvis"), "Invalid commandline trigger!!"
45 | options = {
46 | "install": "Installs the main dependencies.",
47 | "dev-install": "Installs the dev dependencies.",
48 | "start | run": "Initiates Jarvis.",
49 | "uninstall": "Uninstall the main dependencies",
50 | "dev-uninstall": "Uninstall the dev dependencies",
51 | "--version | -v": "Prints the version.",
52 | "--help | -h": "Prints the help section.",
53 | }
54 | # weird way to increase spacing to keep all values monotonic
55 | _longest_key = len(max(options.keys()))
56 | _pretext = "\n\t* "
57 | choices = _pretext + _pretext.join(
58 | f"{k} {'·' * (_longest_key - len(k) + 8)}→ {v}".expandtabs()
59 | for k, v in options.items()
60 | )
61 | try:
62 | arg = sys.argv[1].lower()
63 | except (IndexError, AttributeError):
64 | print(
65 | f"Cannot proceed without arbitrary commands. Please choose from {choices}"
66 | )
67 | exit(1)
68 | from jarvis.lib import installer
69 |
70 | match arg:
71 | case "install":
72 | installer.main_install()
73 | case "dev-install":
74 | installer.dev_install()
75 | case "uninstall":
76 | installer.main_uninstall()
77 | case "dev-uninstall":
78 | installer.dev_uninstall()
79 | case "start" | "run":
80 | os.environ["debug"] = str(os.environ.get("JARVIS_VERBOSITY", "-1") == "1")
81 | init = __preflight_check__()
82 | init()
83 | case "version" | "-v" | "--version":
84 | print(f"Jarvis {version}")
85 | case "help" | "-h" | "--help":
86 | print(
87 | f"Usage: jarvis [arbitrary-command]\nOptions (and corresponding behavior):{choices}"
88 | )
89 | case _:
90 | print(f"Unknown Option: {arg}\nArbitrary commands must be one of {choices}")
91 |
92 |
93 | # MainProcess has specific conditions to land at 'start' or 'commandline'
94 | # __preflight_check__ still needs to be loaded for all other child processes
95 | if current_process().name == "MainProcess":
96 | current_process().name = os.environ.get("PROCESS_NAME", "JARVIS")
97 | else:
98 | __preflight_check__()
99 |
--------------------------------------------------------------------------------
/jarvis/_preexec/keywords_handler.py:
--------------------------------------------------------------------------------
1 | import os
2 | import warnings
3 | from collections import OrderedDict
4 |
5 | import yaml
6 |
7 | from jarvis.modules.builtin_overrides import ordered_dump, ordered_load
8 | from jarvis.modules.conditions import conversation, keywords
9 | from jarvis.modules.models import enums, models
10 | from jarvis.modules.utils import support
11 |
12 |
13 | def load_ignores(data: dict) -> None:
14 | """Loads ``ignore_after`` and ``ignore_add`` list to avoid iterations on the same phrase."""
15 | # Keywords for which the ' after ' split should not happen.
16 | keywords.ignore_after = data["meetings"] + data["avoid"]
17 | # Keywords for which the ' and ' split should not happen.
18 | keywords.ignore_and = (
19 | data["send_notification"] + data["reminder"] + data["distance"] + data["avoid"]
20 | )
21 |
22 |
23 | def rewrite_keywords() -> None:
24 | """Loads keywords.yaml file if available, else loads the base keywords module as an object."""
25 | keywords_src = OrderedDict(
26 | **keywords.keyword_mapping(), **conversation.conversation_mapping()
27 | )
28 | # WATCH OUT: for changes in keyword/function name
29 | if models.env.event_app:
30 | keywords_src["events"] = [
31 | models.env.event_app.lower(),
32 | support.ENGINE.plural(models.env.event_app),
33 | ]
34 | else:
35 | keywords_src["events"] = [
36 | enums.EventApp.CALENDAR.value,
37 | enums.EventApp.OUTLOOK.value,
38 | ]
39 | if os.path.isfile(models.fileio.keywords):
40 | with open(models.fileio.keywords) as dst_file:
41 | try:
42 | data = ordered_load(stream=dst_file, Loader=yaml.FullLoader) or {}
43 | except yaml.YAMLError as error:
44 | warnings.warn(message=str(error))
45 | data = None
46 |
47 | if not data: # Either an error occurred when reading or a manual deletion
48 | if data is {}:
49 | warnings.warn(
50 | f"\nSomething went wrong. {models.fileio.keywords!r} appears to be empty."
51 | f"\nRe-sourcing {models.fileio.keywords!r} from base."
52 | )
53 | # compare as sorted, since this will allow changing the order of keywords in the yaml file
54 | elif (
55 | sorted(list(data.keys())) == sorted(list(keywords_src.keys()))
56 | and data.values()
57 | and all(data.values())
58 | ):
59 | keywords.keywords = data
60 | load_ignores(data)
61 | return
62 | else: # Mismatch in keys
63 | warnings.warn(
64 | "\nData mismatch between base keywords and custom keyword mapping."
65 | "\nPlease note: This mapping file is only to change the value for keywords, not the key(s) itself."
66 | f"\nRe-sourcing {models.fileio.keywords!r} from base."
67 | )
68 |
69 | with open(models.fileio.keywords, "w") as dst_file:
70 | ordered_dump(keywords_src, stream=dst_file, indent=4)
71 | keywords.keywords = keywords_src
72 | load_ignores(keywords_src)
73 |
74 |
75 | rewrite_keywords()
76 |
--------------------------------------------------------------------------------
/jarvis/api/entrypoint.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pathlib
3 | import shutil
4 | from datetime import datetime
5 |
6 | from fastapi.middleware.cors import CORSMiddleware
7 |
8 | from jarvis.api.logger import logger
9 | from jarvis.executors import crontab, resource_tracker
10 | from jarvis.modules.models import models
11 |
12 |
13 | def get_cors_params() -> dict:
14 | """Allow CORS: Cross-Origin Resource Sharing to allow restricted resources on the API."""
15 | logger.info("Setting CORS policy.")
16 | origins = [
17 | "http://localhost.com",
18 | "https://localhost.com",
19 | ]
20 | for website in models.env.website:
21 | origins.extend([f"http://{website.host}", f"https://{website.host}"])
22 |
23 | return dict(
24 | middleware_class=CORSMiddleware,
25 | allow_origins=origins,
26 | allow_credentials=True,
27 | allow_methods=["GET", "POST"],
28 | allow_headers=[
29 | "host",
30 | "user-agent", # Default headers
31 | "authorization",
32 | "apikey", # Offline auth and stock monitor apikey headers
33 | "email-otp",
34 | "email_otp", # One time passcode sent via email
35 | "access-token",
36 | "access_token", # Access token sent via email
37 | ],
38 | )
39 |
40 |
41 | def startup() -> None:
42 | """Runs startup scripts (``.py``, ``.sh``, ``.zsh``) stored in ``fileio/startup`` directory."""
43 | if not os.path.isdir(models.fileio.startup_dir):
44 | return
45 | for startup_script in os.listdir(models.fileio.startup_dir):
46 | startup_script = pathlib.Path(startup_script)
47 | logger.info("Executing startup script: '%s'", startup_script)
48 | if startup_script.suffix in (
49 | ".py",
50 | ".sh",
51 | ".zsh",
52 | ) and not startup_script.stem.startswith("_"):
53 | starter = None
54 | if startup_script.suffix == ".py":
55 | starter = shutil.which(cmd="python")
56 | if startup_script.suffix == ".sh":
57 | starter = shutil.which(cmd="bash")
58 | if startup_script.suffix == ".zsh":
59 | starter = shutil.which(cmd="zsh")
60 | if not starter:
61 | continue
62 | script = (
63 | starter + " " + os.path.join(models.fileio.startup_dir, startup_script)
64 | )
65 | logger.debug("Running %s", script)
66 | log_file = datetime.now().strftime(
67 | os.path.join("logs", "startup_script_%d-%m-%Y.log")
68 | )
69 | resource_tracker.semaphores(
70 | crontab.executor, (script, log_file, "startup_script")
71 | )
72 | else:
73 | logger.warning(
74 | "Unsupported file format for startup script: %s", startup_script
75 | )
76 |
--------------------------------------------------------------------------------
/jarvis/api/logger.py:
--------------------------------------------------------------------------------
1 | # noinspection PyUnresolvedReferences
2 | """Looger configuration specific for Jarvis API.
3 |
4 | >>> Logger
5 |
6 | See Also:
7 | - Configures custom logging for uvicorn.
8 | - Disables uvicorn access logs from printing on the screen.
9 | - Modifies application logs to match uvicorn default log format.
10 | - Creates a multiprocessing log wrapper, and adds a filter to include custom process name in the logger format.
11 | """
12 |
13 | import logging
14 | import os
15 | import pathlib
16 | from logging.config import dictConfig
17 |
18 | from jarvis.modules.logger import APIConfig, multiprocessing_logger
19 |
20 | api_config = APIConfig()
21 | multiprocessing_logger(filename=api_config.DEFAULT_LOG_FILENAME)
22 |
23 | # Creates log files
24 | if not os.path.isfile(api_config.ACCESS_LOG_FILENAME):
25 | pathlib.Path(api_config.ACCESS_LOG_FILENAME).touch()
26 |
27 | if not os.path.isfile(api_config.DEFAULT_LOG_FILENAME):
28 | pathlib.Path(api_config.DEFAULT_LOG_FILENAME).touch()
29 |
30 | # Configure logging
31 | dictConfig(config=api_config.LOG_CONFIG)
32 | logging.getLogger(
33 | "uvicorn.access"
34 | ).propagate = False # Disables access logger in default logger to log independently
35 | logger = logging.getLogger("uvicorn.default")
36 |
--------------------------------------------------------------------------------
/jarvis/api/main.py:
--------------------------------------------------------------------------------
1 | from contextlib import asynccontextmanager
2 | from threading import Thread
3 |
4 | from fastapi import FastAPI
5 |
6 | from jarvis import version
7 | from jarvis.api import entrypoint
8 | from jarvis.api.logger import logger
9 | from jarvis.api.routers import routes
10 | from jarvis.api.squire import stockanalysis_squire
11 | from jarvis.modules.models import models
12 |
13 |
14 | @asynccontextmanager
15 | async def lifespan(_: FastAPI):
16 | """Simple startup function to add anything that has to be triggered when Jarvis API starts up."""
17 | logger.info(
18 | "Hosting at http://%s:%s", models.env.offline_host, models.env.offline_port
19 | )
20 | if models.env.author_mode:
21 | Thread(target=stockanalysis_squire.nasdaq).start()
22 | entrypoint.startup()
23 | yield
24 |
25 |
26 | # Initiate API
27 | app = FastAPI(
28 | title="Jarvis API",
29 | description="#### Gateway to communicate with Jarvis, and an entry point for the UI.\n\n"
30 | "**Contact:** [https://vigneshrao.com/contact](https://vigneshrao.com/contact)",
31 | version=version,
32 | lifespan=lifespan,
33 | )
34 |
35 | # Include all the routers
36 | # WATCH OUT: for changes in function name
37 | if models.settings.pname == "jarvis_api": # Avoid looping when called by subprocesses
38 | # Cannot add middleware after an application has started
39 | app.add_middleware(**entrypoint.get_cors_params())
40 | app.routes.extend(routes.get_all_routes())
41 |
--------------------------------------------------------------------------------
/jarvis/api/models/authenticator.py:
--------------------------------------------------------------------------------
1 | import secrets
2 | from http import HTTPStatus
3 |
4 | from fastapi import Depends
5 | from fastapi.security import HTTPBasicCredentials, HTTPBearer
6 |
7 | from jarvis.modules.exceptions import APIResponse
8 | from jarvis.modules.models import models
9 |
10 | SECURITY = HTTPBearer()
11 |
12 |
13 | async def offline_has_access(token: HTTPBasicCredentials = Depends(SECURITY)) -> None:
14 | """Validates the token if mentioned as a dependency.
15 |
16 | Args:
17 | token: Takes the authorization header token as an argument.
18 |
19 | Raises:
20 | APIResponse:
21 | - 401: If authorization is invalid.
22 | """
23 | auth = token.dict().get("credentials", "")
24 | if auth.startswith("\\"):
25 | auth = bytes(auth, "utf-8").decode(encoding="unicode_escape")
26 | if secrets.compare_digest(auth, models.env.offline_pass):
27 | return
28 | raise APIResponse(
29 | status_code=HTTPStatus.UNAUTHORIZED.real, detail=HTTPStatus.UNAUTHORIZED.phrase
30 | )
31 |
32 |
33 | async def robinhood_has_access(token: HTTPBasicCredentials = Depends(SECURITY)) -> None:
34 | """Validates the token if mentioned as a dependency.
35 |
36 | Args:
37 | token: Takes the authorization header token as an argument.
38 |
39 | Raises:
40 | APIResponse:
41 | - 401: If authorization is invalid.
42 | """
43 | auth = token.dict().get("credentials")
44 | if auth.startswith("\\"):
45 | auth = bytes(auth, "utf-8").decode(encoding="unicode_escape")
46 | if secrets.compare_digest(auth, models.env.robinhood_endpoint_auth):
47 | return
48 | raise APIResponse(
49 | status_code=HTTPStatus.UNAUTHORIZED.real, detail=HTTPStatus.UNAUTHORIZED.phrase
50 | )
51 |
52 |
53 | async def surveillance_has_access(
54 | token: HTTPBasicCredentials = Depends(SECURITY),
55 | ) -> None:
56 | """Validates the token if mentioned as a dependency.
57 |
58 | Args:
59 | token: Takes the authorization header token as an argument.
60 |
61 | Raises:
62 | APIResponse:
63 | - 401: If authorization is invalid.
64 | """
65 | auth = token.dict().get("credentials")
66 | if auth.startswith("\\"):
67 | auth = bytes(auth, "utf-8").decode(encoding="unicode_escape")
68 | if secrets.compare_digest(auth, models.env.surveillance_endpoint_auth):
69 | return
70 | raise APIResponse(
71 | status_code=HTTPStatus.UNAUTHORIZED.real, detail=HTTPStatus.UNAUTHORIZED.phrase
72 | )
73 |
74 |
75 | OFFLINE_PROTECTOR = [Depends(dependency=offline_has_access)]
76 | ROBINHOOD_PROTECTOR = [Depends(dependency=robinhood_has_access)]
77 | SURVEILLANCE_PROTECTOR = [Depends(dependency=surveillance_has_access)]
78 |
--------------------------------------------------------------------------------
/jarvis/api/models/modals.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Optional
2 |
3 | from pydantic import BaseModel, EmailStr
4 |
5 | from jarvis.modules.models import models
6 |
7 |
8 | class OfflineCommunicatorModal(BaseModel):
9 | """BaseModel that handles input data for ``OfflineCommunicatorModal``.
10 |
11 | >>> OfflineCommunicatorModal
12 |
13 | """
14 |
15 | command: str
16 | native_audio: Optional[bool] = False
17 | speech_timeout: Optional[int | float] = 0
18 |
19 |
20 | class StockMonitorModal(BaseModel):
21 | """BaseModel that handles input data for ``StockMonitorModal``.
22 |
23 | >>> StockMonitorModal
24 |
25 | """
26 |
27 | email: EmailStr | None = None
28 | token: Any | None = None
29 | request: Any | None = None
30 | plaintext: bool = False
31 |
32 |
33 | class CameraIndexModal(BaseModel):
34 | """BaseModel that handles input data for ``CameraIndexModal``.
35 |
36 | >>> CameraIndexModal
37 |
38 | """
39 |
40 | index: Optional[Any] = None
41 |
42 |
43 | class SpeechSynthesisModal(BaseModel):
44 | """BaseModel that handles input data for ``SpeechSynthesisModal``.
45 |
46 | >>> SpeechSynthesisModal
47 |
48 | """
49 |
50 | text: str
51 | timeout: Optional[int | float] = None
52 | quality: Optional[str] = models.env.speech_synthesis_quality
53 | voice: Optional[str] = models.env.speech_synthesis_voice
54 |
--------------------------------------------------------------------------------
/jarvis/api/models/settings.py:
--------------------------------------------------------------------------------
1 | from multiprocessing import Process, Queue
2 | from typing import Dict, Hashable, List, Optional, Tuple
3 |
4 | from fastapi import WebSocket
5 | from pydantic import BaseConfig, BaseModel, EmailStr
6 |
7 |
8 | class Robinhood(BaseModel):
9 | """Initiates ``Robinhood`` object to handle members across modules.
10 |
11 | >>> Robinhood
12 |
13 | """
14 |
15 | token: Hashable = None
16 |
17 |
18 | robinhood = Robinhood()
19 |
20 |
21 | class StockMonitorHelper(BaseModel):
22 | """Initiates ``StockMonitorHelper`` object to handle members across modules.
23 |
24 | >>> StockMonitorHelper
25 |
26 | """
27 |
28 | otp_sent: Dict[EmailStr, Hashable] = {}
29 | otp_recd: Dict[EmailStr, Optional[Hashable]] = {}
30 |
31 |
32 | stock_monitor_helper = StockMonitorHelper()
33 |
34 |
35 | class Surveillance(BaseConfig):
36 | """Initiates ``Surveillance`` object to handle members across modules.
37 |
38 | >>> Surveillance
39 |
40 | """
41 |
42 | token: Hashable = None
43 | camera_index: str = None
44 | client_id: int = None
45 | available_cameras: List[str] = []
46 | processes: Dict[int, Process] = {}
47 | queue_manager: Dict[int, Queue] = {}
48 | session_manager: Dict[int, float] = {}
49 | frame: Tuple[int, int, int] = ()
50 |
51 |
52 | surveillance = Surveillance()
53 |
54 |
55 | class StockMonitor(BaseModel):
56 | """Initiates ``StockMonitor`` object to handle members across modules.
57 |
58 | >>> StockMonitor
59 |
60 | """
61 |
62 | user_info: Tuple[str, str, str, str, str, str] = (
63 | "ticker",
64 | "email",
65 | "max",
66 | "min",
67 | "correction",
68 | "repeat",
69 | )
70 | values: str = "(" + ",".join("?" for _ in user_info) + ")"
71 | alerts: Tuple[str, str, str, str, str, str, str] = (
72 | "time",
73 | "ticker",
74 | "email",
75 | "max",
76 | "min",
77 | "correction",
78 | "repeat",
79 | )
80 | alert_values: str = "(" + ",".join("?" for _ in alerts) + ")"
81 |
82 |
83 | stock_monitor = StockMonitor()
84 |
85 |
86 | class Trader(BaseModel):
87 | """Initiates ``Trader`` object to handle members across modules.
88 |
89 | >>> Trader
90 |
91 | """
92 |
93 | stock_list: Dict[str, str] = {}
94 | result: Dict[str, List[str]] = {"BUY": [], "SELL": [], "HOLD": []}
95 |
96 |
97 | trader = Trader()
98 |
99 |
100 | class ConnectionManager:
101 | """Initiates ``ConnectionManager`` object to handle multiple connections using ``WebSockets``.
102 |
103 | >>> ConnectionManager
104 |
105 | References:
106 | https://fastapi.tiangolo.com/advanced/websockets/#handling-disconnections-and-multiple-clients
107 | """
108 |
109 | def __init__(self):
110 | """Loads up an active connection queue."""
111 | self.active_connections: List[WebSocket] = []
112 |
113 | async def connect(self, websocket: WebSocket) -> None:
114 | """Accepts the websocket connection.
115 |
116 | Args:
117 | websocket: Websocket.
118 | """
119 | await websocket.accept()
120 | self.active_connections.append(websocket)
121 |
122 | def disconnect(self, websocket: WebSocket) -> None:
123 | """Remove socket from active connections.
124 |
125 | Args:
126 | websocket: Websocket.
127 | """
128 | self.active_connections.remove(websocket)
129 |
--------------------------------------------------------------------------------
/jarvis/api/routers/basics.py:
--------------------------------------------------------------------------------
1 | import os
2 | from http import HTTPStatus
3 |
4 | from fastapi.responses import FileResponse
5 |
6 | from jarvis.api.logger import logger
7 | from jarvis.modules.conditions import keywords as keywords_mod
8 | from jarvis.modules.exceptions import APIResponse
9 |
10 |
11 | async def redirect_index():
12 | """Redirect to docs in read-only mode.
13 |
14 | Returns:
15 |
16 | str:
17 | Redirects the root endpoint / url to read-only doc location.
18 | """
19 | return "/redoc"
20 |
21 |
22 | async def health():
23 | """Health Check for OfflineCommunicator.
24 |
25 | Raises:
26 |
27 | - 200: For a successful health check.
28 | """
29 | raise APIResponse(status_code=HTTPStatus.OK, detail=HTTPStatus.OK.phrase)
30 |
31 |
32 | async def get_favicon():
33 | """Gets the favicon.ico and adds to the API endpoint.
34 |
35 | Returns:
36 |
37 | FileResponse:
38 | Returns the favicon.ico file as FileResponse to support the front-end.
39 | """
40 | # This logger import should not be changed to make sure the path is detected using it
41 | filepath = os.path.join(os.path.dirname(__file__), "favicon.ico")
42 | if os.path.exists(filepath):
43 | return FileResponse(
44 | filename=os.path.basename(filepath),
45 | path=filepath,
46 | status_code=HTTPStatus.OK.real,
47 | )
48 | logger.warning(
49 | "'favicon.ico' is missing or the path is messed up. Fix this to avoid errors in the UI"
50 | )
51 |
52 |
53 | async def keywords():
54 | """Converts the keywords and conversations into a dictionary of key-value pairs.
55 |
56 | Returns:
57 |
58 | Dict[str, List[str]]:
59 | Key-value pairs of the keywords file.
60 | """
61 | return {k: v for k, v in keywords_mod.keywords.items() if isinstance(v, list)}
62 |
--------------------------------------------------------------------------------
/jarvis/api/routers/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevickypedia/Jarvis/1298228d8ed6471f12b7848f0620abea15dbc0a1/jarvis/api/routers/favicon.ico
--------------------------------------------------------------------------------
/jarvis/api/routers/fileio.py:
--------------------------------------------------------------------------------
1 | import os
2 | from datetime import datetime
3 | from http import HTTPStatus
4 |
5 | from fastapi import UploadFile
6 | from fastapi.responses import FileResponse
7 |
8 | from jarvis.api.logger import logger
9 | from jarvis.modules.exceptions import APIResponse
10 | from jarvis.modules.models import models
11 |
12 |
13 | async def list_files():
14 | """Get all YAML files from fileio and all log files from logs directory.
15 |
16 | Returns:
17 |
18 | Dict[str, List[str]]:
19 | Dictionary of files that can be downloaded or uploaded.
20 | """
21 | return {
22 | **{
23 | "logs": [
24 | file_
25 | for __path, __directory, __file in os.walk("logs")
26 | for file_ in __file
27 | ]
28 | },
29 | **{
30 | "fileio": [f for f in os.listdir(models.fileio.root) if f.endswith(".yaml")]
31 | },
32 | **{
33 | "uploads": [
34 | f for f in os.listdir(models.fileio.uploads) if not f.startswith(".")
35 | ]
36 | },
37 | }
38 |
39 |
40 | async def get_file(filename: str):
41 | """Download a particular YAML file from fileio or log file from logs directory.
42 |
43 | Args:
44 |
45 | filename: Name of the file that has to be downloaded.
46 |
47 | Returns:
48 |
49 | FileResponse:
50 | Returns the FileResponse object of the file.
51 | """
52 | allowed_files = await list_files()
53 | if filename not in allowed_files["fileio"] + allowed_files["logs"]:
54 | raise APIResponse(
55 | status_code=HTTPStatus.NOT_ACCEPTABLE.real,
56 | detail=f"{filename!r} is either unavailable or not allowed.\n"
57 | f"Downloadable files:{allowed_files}",
58 | )
59 | if filename.endswith(".log"):
60 | if path := [
61 | __path
62 | for __path, __directory, __file in os.walk("logs")
63 | if filename in __file
64 | ]:
65 | target_file = os.path.join(path[0], filename)
66 | else:
67 | logger.critical("ATTENTION::'%s' wasn't found.", filename)
68 | raise APIResponse(
69 | status_code=HTTPStatus.NOT_FOUND.real,
70 | detail=HTTPStatus.NOT_FOUND.phrase,
71 | )
72 | else:
73 | target_file = os.path.join(models.fileio.root, filename)
74 | logger.info("Requested file: '%s' for download.", filename)
75 | return FileResponse(
76 | status_code=HTTPStatus.OK.real,
77 | path=target_file,
78 | media_type="text/yaml",
79 | filename=filename,
80 | )
81 |
82 |
83 | async def put_file(file: UploadFile):
84 | """Upload a particular YAML file to the fileio directory.
85 |
86 | Args:
87 |
88 | file: Takes the UploadFile object as an argument.
89 | """
90 | logger.info("Requested file: '%s' for upload.", file.filename)
91 | content = await file.read()
92 | allowed_files = await list_files()
93 | if file.filename not in allowed_files["fileio"]:
94 | with open(
95 | os.path.join(
96 | models.fileio.uploads,
97 | f"{datetime.now().strftime('%d_%B_%Y-%I_%M_%p')}-{file.filename}",
98 | ),
99 | "wb",
100 | ) as f_stream:
101 | f_stream.write(content)
102 | raise APIResponse(
103 | status_code=HTTPStatus.ACCEPTED.real,
104 | detail=f"{file.filename!r} is not allowed for an update.\n"
105 | "Hence storing as a standalone file.",
106 | )
107 | with open(os.path.join(models.fileio.root, file.filename), "wb") as f_stream:
108 | f_stream.write(content)
109 | raise APIResponse(
110 | status_code=HTTPStatus.OK.real,
111 | detail=f"{file.filename!r} was uploaded to {models.fileio.root}.",
112 | )
113 |
--------------------------------------------------------------------------------
/jarvis/api/routers/proxy_service.py:
--------------------------------------------------------------------------------
1 | import re
2 | from http import HTTPStatus
3 |
4 | import requests
5 | from fastapi import Request, Response
6 | from pydantic import HttpUrl
7 |
8 | from jarvis.api.logger import logger
9 | from jarvis.modules.exceptions import APIResponse, EgressErrors
10 |
11 |
12 | def is_valid_media_type(media_type: str) -> bool:
13 | """Regular expression to match valid media types.
14 |
15 | Args:
16 | media_type: Takes the media type to be validated.
17 |
18 | Returns:
19 | bool:
20 | Returns a boolean value to indicate validity.
21 | """
22 | media_type_pattern = r"^[a-zA-Z]+/[a-zA-Z0-9\-\.\+]+$"
23 | return bool(re.match(media_type_pattern, media_type))
24 |
25 |
26 | async def proxy_service_api(request: Request, origin: HttpUrl, output: str):
27 | """API endpoint to act as a proxy for GET calls.
28 |
29 | See Also:
30 | This is primarily to solve the CORS restrictions on web browsers.
31 |
32 | Examples:
33 | .. code-block:: bash
34 |
35 | curl -X 'GET' 'http://0.0.0.0:{PORT}/proxy?origin=http://example.com&output=application/xml'
36 |
37 | Args:
38 | request: FastAPI request module.
39 | origin: Origin URL as query string.
40 | output: Output media type.
41 | """
42 | logger.info(
43 | "Connection received from %s via %s using %s",
44 | request.client.host,
45 | request.headers.get("host"),
46 | request.headers.get("user-agent"),
47 | )
48 | try:
49 | response = requests.get(url=origin, allow_redirects=True, verify=False)
50 | if response.ok:
51 | if is_valid_media_type(output):
52 | return Response(content=response.text, media_type=output)
53 | raise APIResponse(
54 | status_code=HTTPStatus.BAD_REQUEST.real,
55 | detail=HTTPStatus.BAD_REQUEST.phrase,
56 | )
57 | raise APIResponse(status_code=response.status_code, detail=response.text)
58 | except EgressErrors as error:
59 | logger.error(error)
60 | raise APIResponse(
61 | status_code=HTTPStatus.SERVICE_UNAVAILABLE.real,
62 | detail=HTTPStatus.SERVICE_UNAVAILABLE.phrase,
63 | )
64 |
--------------------------------------------------------------------------------
/jarvis/api/routers/secure_send.py:
--------------------------------------------------------------------------------
1 | import os
2 | from http import HTTPStatus
3 | from typing import Optional
4 |
5 | from fastapi import Header, Request
6 |
7 | from jarvis.api.logger import logger
8 | from jarvis.executors import files
9 | from jarvis.modules.exceptions import APIResponse
10 | from jarvis.modules.models import models
11 |
12 |
13 | async def secure_send_api(request: Request, access_token: Optional[str] = Header(None)):
14 | """API endpoint to share/retrieve secrets.
15 |
16 | Args:
17 | request: FastAPI request module.
18 | access_token: Access token for the secret to be retrieved.
19 |
20 | Raises:
21 |
22 | - 200: For a successful authentication (secret will be returned)
23 | - 400: For a bad request if headers are passed with underscore
24 | - 401: For a failed authentication (if the access token doesn't match)
25 | - 404: If the ``secure_send`` mapping file is unavailable
26 | """
27 | logger.info(
28 | "Connection received from %s via %s using %s",
29 | request.client.host,
30 | request.headers.get("host"),
31 | request.headers.get("user-agent"),
32 | )
33 | key = access_token or request.headers.get("access-token")
34 | if not key:
35 | logger.warning("'access-token' not received in headers")
36 | raise APIResponse(
37 | status_code=HTTPStatus.UNAUTHORIZED.real,
38 | detail=HTTPStatus.UNAUTHORIZED.phrase,
39 | )
40 | if key.startswith("\\"):
41 | key = bytes(key, "utf-8").decode(encoding="unicode_escape")
42 | if os.path.isfile(models.fileio.secure_send):
43 | secure_strings = files.get_secure_send()
44 | if secret := secure_strings.get(key):
45 | logger.info("secret was accessed using secure token, deleting secret")
46 | files.delete_secure_send(key)
47 | raise APIResponse(status_code=HTTPStatus.OK.real, detail=secret)
48 | else:
49 | logger.info(
50 | "secret access was denied for key: %s, available keys: %s",
51 | key,
52 | [*secure_strings.keys()],
53 | )
54 | raise APIResponse(
55 | status_code=HTTPStatus.UNAUTHORIZED.real,
56 | detail=HTTPStatus.UNAUTHORIZED.phrase,
57 | )
58 | else:
59 | logger.info("'%s' not found", models.fileio.secure_send)
60 | raise APIResponse(
61 | status_code=HTTPStatus.NOT_FOUND.real, detail=HTTPStatus.NOT_FOUND.phrase
62 | )
63 |
--------------------------------------------------------------------------------
/jarvis/api/routers/telegram.py:
--------------------------------------------------------------------------------
1 | import secrets
2 | from http import HTTPStatus
3 | from json.decoder import JSONDecodeError
4 |
5 | from fastapi.exceptions import HTTPException
6 | from fastapi.requests import Request
7 |
8 | from jarvis.api.logger import logger
9 | from jarvis.modules.models import models
10 | from jarvis.modules.telegram import bot
11 |
12 |
13 | def two_factor(request: Request) -> bool:
14 | """Two factor verification for messages coming via webhook.
15 |
16 | Args:
17 | request: Request object from FastAPI.
18 |
19 | Returns:
20 | bool:
21 | Flag to indicate the calling function if the auth was successful.
22 | """
23 | if models.env.bot_secret:
24 | if secrets.compare_digest(
25 | request.headers.get("X-Telegram-Bot-Api-Secret-Token", ""),
26 | models.env.bot_secret,
27 | ):
28 | return True
29 | else:
30 | logger.warning("Use the env var bot_secret to secure the webhook interaction")
31 | return True
32 |
33 |
34 | async def telegram_webhook(request: Request):
35 | """Invoked when a new message is received from Telegram API.
36 |
37 | Args:
38 | request: Request instance.
39 |
40 | Raises:
41 |
42 | HTTPException:
43 | - 406: If the request payload is not JSON format-able.
44 | """
45 | logger.debug(
46 | "Connection received from %s via %s",
47 | request.client.host,
48 | request.headers.get("host"),
49 | )
50 | try:
51 | response = await request.json()
52 | except JSONDecodeError as error:
53 | logger.error(error)
54 | raise HTTPException(
55 | status_code=HTTPStatus.BAD_REQUEST.real,
56 | detail=HTTPStatus.BAD_REQUEST.phrase,
57 | )
58 | # Ensure only the owner who set the webhook can interact with the Bot
59 | if not two_factor(request):
60 | logger.error("Request received from a non-webhook source")
61 | logger.error(response)
62 | raise HTTPException(
63 | status_code=HTTPStatus.FORBIDDEN.real, detail=HTTPStatus.FORBIDDEN.phrase
64 | )
65 | if payload := response.get("message"):
66 | logger.debug(response)
67 | bot.process_request(payload)
68 | else:
69 | raise HTTPException(
70 | status_code=HTTPStatus.UNPROCESSABLE_ENTITY.real,
71 | detail=HTTPStatus.UNPROCESSABLE_ENTITY.phrase,
72 | )
73 |
--------------------------------------------------------------------------------
/jarvis/api/server.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import requests
4 | import uvicorn
5 |
6 | from jarvis.executors import port_handler
7 | from jarvis.modules.builtin_overrides import APIServer
8 | from jarvis.modules.exceptions import EgressErrors
9 | from jarvis.modules.logger import APIConfig, logger, multiprocessing_logger
10 | from jarvis.modules.models import models
11 |
12 |
13 | def jarvis_api() -> None:
14 | """Initiates the fast API in a dedicated process using uvicorn server.
15 |
16 | See Also:
17 | - Checks if the port is being used. If so, makes a ``GET`` request to the endpoint.
18 | - Attempts to kill the process listening to the port, if the endpoint doesn't respond.
19 | """
20 | multiprocessing_logger(filename=APIConfig().DEFAULT_LOG_FILENAME)
21 | url = f"http://{models.env.offline_host}:{models.env.offline_port}"
22 |
23 | if port_handler.is_port_in_use(port=models.env.offline_port):
24 | logger.info("%d is currently in use.", models.env.offline_port)
25 |
26 | try:
27 | res = requests.get(url=url, timeout=1)
28 | if res.ok:
29 | logger.info("'%s' is accessible.", url)
30 | return
31 | raise requests.ConnectionError
32 | except EgressErrors:
33 | logger.error("Unable to connect to existing uvicorn server.")
34 |
35 | # This might terminate Jarvis
36 | if not port_handler.kill_port_pid(port=models.env.offline_port):
37 | logger.critical(
38 | "ATTENTION::Failed to kill existing PID. Attempting to re-create session."
39 | )
40 |
41 | # Uvicorn config supports the module as a value for the arg 'app' which can be from relative imports
42 | # However, in this case, using relative imports will mess with the logger since it is shared across multiple process
43 | assert os.path.exists(
44 | os.path.join(os.path.dirname(__file__), "main.py")
45 | ), "API path has either been modified or unreachable."
46 | argument_dict = {
47 | "app": "jarvis.api.main:app",
48 | "host": models.env.offline_host,
49 | "port": models.env.offline_port,
50 | "ws_ping_interval": 20.0,
51 | "ws_ping_timeout": 20.0,
52 | "workers": models.env.workers,
53 | }
54 |
55 | logger.debug(argument_dict)
56 | logger.info(
57 | "Starting FastAPI on Uvicorn server with %d workers.", models.env.workers
58 | )
59 |
60 | server_conf = uvicorn.Config(**argument_dict)
61 | APIServer(config=server_conf).run_in_parallel()
62 |
--------------------------------------------------------------------------------
/jarvis/api/squire/scheduler.py:
--------------------------------------------------------------------------------
1 | import os
2 | import shutil
3 | from typing import Dict
4 |
5 | from jarvis.api import triggers
6 | from jarvis.modules.crontab import expression
7 | from jarvis.modules.utils import util
8 |
9 |
10 | def market_hours(extended: bool = False) -> Dict[str, Dict[str, int]]:
11 | """Returns the market hours for each timezone in the US.
12 |
13 | Args:
14 | extended: Boolean flag to use extended hours.
15 |
16 | Returns:
17 | Dict[str, Dict[str, int]]:
18 | Returns a dictionary of timezone and the market open and close hours.
19 | """
20 | if extended:
21 | return {
22 | "EDT": {"OPEN": 7, "CLOSE": 18},
23 | "EST": {"OPEN": 7, "CLOSE": 18},
24 | "CDT": {"OPEN": 6, "CLOSE": 17},
25 | "CST": {"OPEN": 6, "CLOSE": 17},
26 | "MDT": {"OPEN": 5, "CLOSE": 16},
27 | "MST": {"OPEN": 5, "CLOSE": 16},
28 | "PDT": {"OPEN": 4, "CLOSE": 15},
29 | "PST": {"OPEN": 4, "CLOSE": 15},
30 | "OTHER": {"OPEN": 5, "CLOSE": 21}, # 5 AM to 9 PM
31 | }
32 | return {
33 | "EDT": {"OPEN": 9, "CLOSE": 16},
34 | "EST": {"OPEN": 9, "CLOSE": 16},
35 | "CDT": {"OPEN": 8, "CLOSE": 15},
36 | "CST": {"OPEN": 8, "CLOSE": 15},
37 | "MDT": {"OPEN": 7, "CLOSE": 14},
38 | "MST": {"OPEN": 7, "CLOSE": 14},
39 | "PDT": {"OPEN": 6, "CLOSE": 13},
40 | "PST": {"OPEN": 6, "CLOSE": 13},
41 | "OTHER": {"OPEN": 7, "CLOSE": 19}, # 7 AM to 7 PM
42 | }
43 |
44 |
45 | def rh_cron_schedule(extended: bool = False) -> expression.CronExpression:
46 | """Creates a cron expression for ``stock_report.py``. Determines cron schedule based on current timezone.
47 |
48 | Args:
49 | extended: Boolean flag to use extended hours.
50 |
51 | See Also:
52 | - extended: 1 before and after market hours.
53 | - default(regular): Regular market hours.
54 |
55 | Returns:
56 | CronExpression:
57 | Crontab expression object running every 30 minutes during market hours based on the current timezone.
58 | """
59 | hours = market_hours(extended)
60 | job = f"{shutil.which(cmd='python')} {os.path.join(triggers.__path__[0], 'stock_report.py')}"
61 | tz = util.get_timezone()
62 | if tz not in hours:
63 | tz = "OTHER"
64 | return expression.CronExpression(
65 | f"*/30 {hours[tz]['OPEN']}-{hours[tz]['CLOSE']} * * 1-5 {job}"
66 | )
67 |
68 |
69 | def sm_cron_schedule(include_weekends: bool = False) -> expression.CronExpression:
70 | """Creates a cron expression for ``stock_monitor.py``.
71 |
72 | Args:
73 | include_weekends: Takes a boolean flag to run cron schedule over the weekends.
74 |
75 | Returns:
76 | CronExpression:
77 | Crontab expression object running every 15 minutes.
78 | """
79 | job = f"{shutil.which(cmd='python')} {os.path.join(triggers.__path__[0], 'stock_monitor.py')}"
80 | if include_weekends:
81 | return expression.CronExpression(f"*/15 * * * * {job}")
82 | return expression.CronExpression(f"*/15 * * * 1-5 {job}")
83 |
--------------------------------------------------------------------------------
/jarvis/api/squire/timeout_otp.py:
--------------------------------------------------------------------------------
1 | from pydantic import EmailStr
2 |
3 | from jarvis.api.models import settings
4 |
5 |
6 | def reset_robinhood() -> None:
7 | """Resets robinhood token after the set time."""
8 | settings.robinhood.token = None
9 |
10 |
11 | def reset_stock_monitor(email_address: EmailStr) -> None:
12 | """Resets stock monitor OTP after the set time.
13 |
14 | Args:
15 | email_address: Email address that should be cleared.
16 | """
17 | if settings.stock_monitor_helper.otp_sent.get(email_address):
18 | del settings.stock_monitor_helper.otp_sent[email_address]
19 |
20 |
21 | def reset_surveillance() -> None:
22 | """Resets surveillance token after the set time."""
23 | settings.surveillance.token = None
24 |
--------------------------------------------------------------------------------
/jarvis/executors/connection.py:
--------------------------------------------------------------------------------
1 | import socket
2 | import threading
3 | from http.client import HTTPSConnection
4 |
5 | import pywifi
6 |
7 | from jarvis.modules.logger import logger
8 | from jarvis.modules.models import classes, enums, models
9 |
10 |
11 | def wifi(conn_object: classes.WiFiConnection) -> classes.WiFiConnection | None:
12 | """Checks for internet connection as per given frequency. Enables Wi-Fi and connects to SSID if connection fails.
13 |
14 | Args:
15 | conn_object: Takes an object of unknown errors and OSError as an argument.
16 |
17 | Returns:
18 | WiFiConnection:
19 | Returns the connection object to keep alive, None to stop calling this function.
20 | """
21 | socket_ = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
22 | try:
23 | if models.settings.os == enums.SupportedPlatforms.windows:
24 | # Recreate a new connection everytime
25 | connection = HTTPSConnection("8.8.8.8", timeout=3)
26 | connection.request("HEAD", "/")
27 | else:
28 | socket_.connect(("8.8.8.8", 80))
29 | if conn_object.unknown_errors:
30 | logger.info(
31 | "Connection established with IP: %s. Resetting flags.",
32 | socket_.getsockname()[0],
33 | )
34 | conn_object.unknown_errors = 0
35 | conn_object.os_errors = 0
36 | except OSError as error:
37 | conn_object.os_errors += 1
38 | logger.error("OSError [%d]: %s", error.errno, error.strerror)
39 | # Make sure Wi-Fi is enabled
40 | pywifi.ControlPeripheral(logger=logger).enable()
41 | connection_control = pywifi.ControlConnection(
42 | wifi_ssid=models.env.wifi_ssid,
43 | wifi_password=models.env.wifi_password,
44 | logger=logger,
45 | )
46 | threading.Timer(interval=5, function=connection_control.wifi_connector).start()
47 | except Exception as error:
48 | logger.critical(error)
49 | conn_object.unknown_errors += 1
50 |
51 | if conn_object.unknown_errors > 10 or conn_object.os_errors > 30:
52 | logger.warning(conn_object.model_dump_json())
53 | logger.error(
54 | "'%s' is running into repeated errors, hence stopping..!", wifi.__name__
55 | )
56 | return None
57 | return conn_object
58 |
--------------------------------------------------------------------------------
/jarvis/executors/crontab.py:
--------------------------------------------------------------------------------
1 | import os
2 | import subprocess
3 | from collections.abc import Generator
4 |
5 | from jarvis.api.squire import scheduler
6 | from jarvis.executors import files
7 | from jarvis.modules.crontab import expression
8 | from jarvis.modules.exceptions import InvalidArgument
9 | from jarvis.modules.logger import logger, multiprocessing_logger
10 | from jarvis.modules.models import models
11 |
12 | # Used by api functions that run on cron schedule
13 | LOG_FILE = os.path.join("logs", "cron_%d-%m-%Y.log")
14 |
15 |
16 | def executor(statement: str, log_file: str = None, process_name: str = None) -> None:
17 | """Executes a cron statement.
18 |
19 | Args:
20 | statement: Cron statement to be executed.
21 | log_file: Log file for crontab execution logs.
22 | process_name: Process name for the execution.
23 |
24 | Warnings:
25 | - Executions done by crontab executor are not stopped when Jarvis is stopped.
26 | - On the bright side, almost all executions made by Jarvis are short-lived.
27 | """
28 | if not log_file:
29 | log_file = multiprocessing_logger(filename=LOG_FILE)
30 | if not process_name:
31 | process_name = "crontab_executor"
32 | process_name = "_".join(process_name.split())
33 | command = f"export PROCESS_NAME={process_name} && {statement}"
34 | logger.debug("Executing '%s' as '%s'", statement, command)
35 | with open(log_file, "a") as file:
36 | file.write("\n")
37 | try:
38 | subprocess.call(command, shell=True, stdout=file, stderr=file)
39 | except Exception as error:
40 | if isinstance(error, subprocess.CalledProcessError):
41 | result = error.output.decode(encoding="UTF-8").strip()
42 | file.write(f"[{error.returncode}]: {result}")
43 | else:
44 | file.write(error.__str__())
45 |
46 |
47 | def validate_jobs(log: bool = True) -> Generator[expression.CronExpression]:
48 | """Validates each of the cron job.
49 |
50 | Args:
51 | log: Takes a boolean flag to suppress info level logging.
52 |
53 | Yields:
54 | CronExpression:
55 | CronExpression object.
56 | """
57 | for idx in files.get_crontab():
58 | try:
59 | cron = expression.CronExpression(idx)
60 | except InvalidArgument as error:
61 | logger.error(error)
62 | os.rename(src=models.fileio.crontab, dst=models.fileio.tmp_crontab)
63 | continue
64 | if log:
65 | msg = f"{cron.comment!r} will be executed as per the schedule {cron.expression!r}"
66 | logger.info(msg)
67 | yield cron
68 | if models.env.author_mode:
69 | if all(
70 | (
71 | models.env.robinhood_user,
72 | models.env.robinhood_pass,
73 | models.env.robinhood_pass,
74 | )
75 | ):
76 | yield scheduler.rh_cron_schedule(extended=True)
77 | yield scheduler.sm_cron_schedule(include_weekends=True)
78 |
--------------------------------------------------------------------------------
/jarvis/executors/custom_conditions.py:
--------------------------------------------------------------------------------
1 | import warnings
2 | from collections import OrderedDict
3 | from typing import Callable
4 |
5 | from jarvis.executors import files, method
6 | from jarvis.modules.audio import speaker
7 | from jarvis.modules.logger import logger
8 | from jarvis.modules.utils import shared, util
9 |
10 |
11 | def custom_conditions(phrase: str, function_map: OrderedDict[str, Callable]) -> bool:
12 | """Execute one or many functions based on custom conditions."""
13 | if not (custom_mapping := files.get_custom_conditions()):
14 | return False
15 | # noinspection PyTypeChecker
16 | closest_match = util.get_closest_match(
17 | text=phrase.lower(), match_list=custom_mapping.keys(), get_ratio=True
18 | )
19 | if closest_match["ratio"] < 0.9:
20 | return False
21 | custom_phrase = closest_match["text"]
22 | task_map = custom_mapping[custom_phrase]
23 | logger.info(
24 | "'%s' matches with the custom condition '%s' at the rate: %.2f",
25 | phrase,
26 | custom_phrase,
27 | closest_match["ratio"],
28 | )
29 | executed = False
30 | if shared.called_by_offline:
31 | response = ""
32 | for function_, task_ in task_map.items():
33 | if function_map.get(function_):
34 | executed = True
35 | method.executor(function_map[function_], task_)
36 | response += shared.text_spoken + "\n"
37 | else:
38 | warnings.warn(
39 | "Custom condition map was found with incorrect function name: '%s'"
40 | % function_
41 | )
42 | if response:
43 | speaker.speak(text=response)
44 | else:
45 | for function_, task_ in task_map.items():
46 | if function_map.get(function_):
47 | executed = True
48 | method.executor(function_map[function_], task_)
49 | else:
50 | warnings.warn(
51 | "Custom condition map was found with incorrect function name: '%s'"
52 | % function_
53 | )
54 | if executed:
55 | return True
56 | logger.debug("Custom map was present but did not match with the current request.")
57 |
--------------------------------------------------------------------------------
/jarvis/executors/date_time.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | import pytz
4 | from timezonefinder import TimezoneFinder
5 |
6 | from jarvis.executors import location, others
7 | from jarvis.modules.audio import speaker
8 | from jarvis.modules.models import models
9 | from jarvis.modules.utils import shared, support
10 |
11 |
12 | def current_time(converted: str = None) -> None:
13 | """Says current time at the requested location if any, else with respect to the current timezone.
14 |
15 | Args:
16 | converted: Takes the phrase as an argument.
17 | """
18 | place = support.get_capitalized(phrase=converted) if converted else None
19 | if place and len(place) > 3:
20 | place_tz = location.geo_locator.geocode(place)
21 | coordinates = place_tz.latitude, place_tz.longitude
22 | located = location.geo_locator.reverse(coordinates, language="en")
23 | address = located.raw.get("address", {})
24 | city, state = address.get("city"), address.get("state")
25 | time_location = (
26 | f"{city} {state}".replace("None", "") if city or state else place
27 | )
28 | zone = TimezoneFinder().timezone_at(
29 | lat=place_tz.latitude, lng=place_tz.longitude
30 | )
31 | datetime_zone = datetime.now(pytz.timezone(zone))
32 | date_tz = datetime_zone.strftime("%A, %B %d, %Y")
33 | time_tz = datetime_zone.strftime("%I:%M %p")
34 | dt_string = datetime.now().strftime("%A, %B %d, %Y")
35 | if date_tz != dt_string:
36 | date_tz = datetime_zone.strftime("%A, %B %d")
37 | speaker.speak(
38 | text=f"The current time in {time_location} is {time_tz}, on {date_tz}."
39 | )
40 | else:
41 | speaker.speak(text=f"The current time in {time_location} is {time_tz}.")
42 | else:
43 | if shared.called["report"]:
44 | speaker.speak(
45 | text=f"The current time is, {datetime.now().strftime('%I:%M %p')}."
46 | )
47 | return
48 | speaker.speak(text=f"{datetime.now().strftime('%I:%M %p')}.")
49 |
50 |
51 | def current_date(*args) -> None:
52 | """Says today's date and adds the current time in speaker queue if report function was called."""
53 | dt_string = datetime.now().strftime("%A, %B")
54 | date_ = support.ENGINE.ordinal(datetime.now().strftime("%d"))
55 | year = str(datetime.now().year)
56 | event = others.celebrate()
57 | dt_string = f"{dt_string} {date_}, {year}"
58 | text = f"It's {dt_string}."
59 | if event and event == "Birthday":
60 | text += f" It's also your {event} {models.env.title}!"
61 | elif event:
62 | text += f" It's also {event} {models.env.title}!"
63 | speaker.speak(text=text)
64 |
--------------------------------------------------------------------------------
/jarvis/executors/display_functions.py:
--------------------------------------------------------------------------------
1 | import random
2 | from threading import Thread
3 |
4 | import pybrightness
5 |
6 | from jarvis.modules.audio import speaker
7 | from jarvis.modules.conditions import conversation
8 | from jarvis.modules.logger import logger
9 | from jarvis.modules.utils import util
10 |
11 |
12 | def brightness(phrase: str):
13 | """Pre-process to check the phrase received and call the appropriate brightness function as necessary.
14 |
15 | Args:
16 | phrase: Takes the phrase spoken as an argument.
17 | """
18 | phrase = phrase.lower()
19 | speaker.speak(text=random.choice(conversation.acknowledgement))
20 | if "set" in phrase:
21 | level = util.extract_nos(input_=phrase, method=int)
22 | try:
23 | assert (
24 | isinstance(level, int) and 0 <= level <= 100
25 | ), "value should be an integer between 0 and 100"
26 | except AssertionError as err:
27 | logger.warning(err)
28 | level = 50
29 | Thread(target=pybrightness.custom, args=(level, logger)).start()
30 | elif (
31 | "decrease" in phrase
32 | or "reduce" in phrase
33 | or "lower" in phrase
34 | or "dark" in phrase
35 | or "dim" in phrase
36 | ):
37 | Thread(target=pybrightness.decrease, args=(logger,)).start()
38 | elif (
39 | "increase" in phrase
40 | or "bright" in phrase
41 | or "max" in phrase
42 | or "brighten" in phrase
43 | or "light up" in phrase
44 | ):
45 | Thread(target=pybrightness.increase, args=(logger,)).start()
46 |
--------------------------------------------------------------------------------
/jarvis/executors/functions.py:
--------------------------------------------------------------------------------
1 | # noinspection PyUnresolvedReferences
2 | """Creates a dictionary with the keyword category as key and the function to be called as value.
3 |
4 | >>> Functions
5 |
6 | """
7 |
8 | from collections import OrderedDict
9 | from typing import Callable
10 |
11 | from jarvis.executors import (
12 | alarm,
13 | automation,
14 | background_task,
15 | car,
16 | comm_squire,
17 | communicator,
18 | controls,
19 | date_time,
20 | display_functions,
21 | face,
22 | github,
23 | guard,
24 | internet,
25 | ios_functions,
26 | lights,
27 | listener_controls,
28 | location,
29 | others,
30 | remind,
31 | robinhood,
32 | simulator,
33 | static_responses,
34 | system,
35 | thermostat,
36 | todo_list,
37 | tv,
38 | volume,
39 | vpn_server,
40 | weather,
41 | )
42 | from jarvis.modules.audio import voices
43 | from jarvis.modules.meetings import events, ics_meetings
44 |
45 |
46 | def function_mapping() -> OrderedDict[str, Callable]:
47 | """Returns an ordered dictionary of functions mapping.
48 |
49 | Returns:
50 | OrderedDict:
51 | OrderedDict of category and keywords as key-value pairs.
52 | """
53 | return OrderedDict(
54 | listener_control=listener_controls.listener_control,
55 | send_notification=comm_squire.send_notification,
56 | lights=lights.lights,
57 | television=tv.television,
58 | volume=volume.volume,
59 | car=car.car,
60 | thermostat=thermostat.thermostat_controls,
61 | weather=weather.weather,
62 | meetings=ics_meetings.meetings,
63 | events=events.events,
64 | current_date=date_time.current_date,
65 | current_time=date_time.current_time,
66 | system_info=system.system_info,
67 | ip_info=internet.ip_info,
68 | news=others.news,
69 | report=others.report,
70 | robinhood=robinhood.robinhood,
71 | repeat=others.repeat,
72 | location=location.location,
73 | locate=ios_functions.locate,
74 | read_gmail=communicator.read_gmail,
75 | meaning=others.meaning,
76 | todo=todo_list.todo,
77 | kill_alarm=alarm.kill_alarm,
78 | set_alarm=alarm.set_alarm,
79 | google_home=others.google_home,
80 | reminder=remind.reminder,
81 | distance=location.distance,
82 | locate_places=location.locate_places,
83 | directions=location.directions,
84 | notes=others.notes,
85 | github=github.github,
86 | apps=others.apps,
87 | music=others.music,
88 | faces=face.faces,
89 | speed_test=internet.speed_test,
90 | brightness=display_functions.brightness,
91 | guard_enable=guard.guard_enable,
92 | guard_disable=guard.guard_disable,
93 | flip_a_coin=others.flip_a_coin,
94 | voice_changer=voices.voice_changer,
95 | system_vitals=system.system_info,
96 | vpn_server=vpn_server.vpn_server,
97 | automation_handler=automation.automation_handler,
98 | background_task_handler=background_task.background_task_handler,
99 | photo=others.photo,
100 | version=others.version,
101 | simulation=simulator.simulation,
102 | celebrate=others.celebrate,
103 | sleep_control=controls.sleep_control,
104 | sentry=controls.sentry,
105 | restart_control=controls.restart_control,
106 | shutdown=controls.shutdown,
107 | kill=controls.kill,
108 | hi=static_responses.hi,
109 | greeting=static_responses.greeting,
110 | capabilities=static_responses.capabilities,
111 | languages=static_responses.languages,
112 | what=static_responses.what,
113 | who=static_responses.who,
114 | age=static_responses.age,
115 | form=static_responses.form,
116 | whats_up=static_responses.whats_up,
117 | about_me=static_responses.about_me,
118 | )
119 |
--------------------------------------------------------------------------------
/jarvis/executors/github.py:
--------------------------------------------------------------------------------
1 | from collections.abc import Generator
2 | from typing import Dict
3 |
4 | import requests
5 |
6 | from jarvis.modules.audio import speaker
7 | from jarvis.modules.exceptions import EgressErrors
8 | from jarvis.modules.logger import logger
9 | from jarvis.modules.models import models
10 | from jarvis.modules.utils import support
11 |
12 |
13 | def get_repos() -> Generator[Dict[str, str | bool]]:
14 | """Get repositories in GitHub account.
15 |
16 | Yields:
17 | Generator[Dict[str, str | bool]]:
18 | Yields each repository information.
19 | """
20 | i = 1
21 | while True:
22 | response = requests.get(
23 | f"https://api.github.com/user/repos?type=all&page={i}&per_page=100",
24 | headers={"Authorization": f"Bearer {models.env.git_token}"},
25 | )
26 | response.raise_for_status()
27 | assert response.ok
28 | response_json = response.json()
29 | logger.info("Repos in page %d: %d", i, len(response_json))
30 | if response_json:
31 | i += 1
32 | else:
33 | break
34 | yield from response_json
35 |
36 |
37 | def github(*args) -> None:
38 | """Get GitHub account information."""
39 | if not models.env.git_token:
40 | logger.warning("Github token not found.")
41 | support.no_env_vars()
42 | return
43 | total, forked, private, archived, licensed = 0, 0, 0, 0, 0
44 | try:
45 | for repo in get_repos():
46 | total += 1
47 | forked += 1 if repo["fork"] else 0
48 | private += 1 if repo["private"] else 0
49 | archived += 1 if repo["archived"] else 0
50 | licensed += 1 if repo["license"] else 0
51 | except (EgressErrors, AssertionError, requests.JSONDecodeError) as error:
52 | logger.error(error)
53 | speaker.speak(
54 | text=f"I'm sorry {models.env.title}! I wasn't able to connect to the GitHub API."
55 | )
56 | return
57 | speaker.speak(
58 | text=f"You have {total} repositories {models.env.title}, out of which {forked} are forked, "
59 | f"{private} are private, {licensed} are licensed, and {archived} archived."
60 | )
61 |
--------------------------------------------------------------------------------
/jarvis/executors/listener_controls.py:
--------------------------------------------------------------------------------
1 | import sqlite3
2 |
3 | from jarvis.modules.audio import speaker
4 | from jarvis.modules.database import database
5 | from jarvis.modules.logger import logger
6 | from jarvis.modules.models import models
7 | from jarvis.modules.retry import retry
8 |
9 | db = database.Database(database=models.fileio.base_db)
10 |
11 |
12 | def listener_control(phrase: str) -> None:
13 | """Controls the listener table in base db.
14 |
15 | Args:
16 | phrase: Takes the phrase spoken as an argument.
17 | """
18 | phrase = phrase.lower()
19 | state = get_listener_state()
20 | if "deactivate" in phrase or "disable" in phrase:
21 | if state:
22 | put_listener_state(state=False)
23 | speaker.speak(text=f"Listener has been deactivated {models.env.title}!")
24 | else:
25 | speaker.speak(text=f"Listener was never activated {models.env.title}!")
26 | elif "activate" in phrase or "enable" in phrase:
27 | if state:
28 | speaker.speak(text=f"Listener is already active {models.env.title}!")
29 | else:
30 | put_listener_state(state=True)
31 | speaker.speak(text=f"Listener has been activated {models.env.title}!")
32 | else:
33 | if state:
34 | speaker.speak(text=f"Listener is currently active {models.env.title}!")
35 | else:
36 | speaker.speak(text=f"Listener is currently inactive {models.env.title}!")
37 |
38 |
39 | def get_listener_state() -> bool:
40 | """Gets the current state of listener.
41 |
42 | Returns:
43 | bool: A boolean flag to indicate if the listener is active.
44 | """
45 | with db.connection:
46 | cursor = db.connection.cursor()
47 | state = cursor.execute("SELECT state FROM listener").fetchone()
48 | if state and state[0]:
49 | logger.debug("Listener is currently enabled")
50 | return True
51 | else:
52 | logger.debug("Listener is currently disabled")
53 |
54 |
55 | @retry.retry(attempts=3, interval=2, exclude_exc=sqlite3.OperationalError)
56 | def put_listener_state(state: bool) -> None:
57 | """Updates the state of the listener.
58 |
59 | Args:
60 | state: Takes the boolean value to be inserted.
61 | """
62 | logger.info("Current listener status: '%s'", get_listener_state())
63 | logger.info("Updating listener status to %s", state)
64 | with db.connection:
65 | cursor = db.connection.cursor()
66 | cursor.execute("DELETE FROM listener")
67 | if state:
68 | cursor.execute(
69 | "INSERT or REPLACE INTO listener (state) VALUES (?);", (state,)
70 | )
71 | cursor.execute("UPDATE listener SET state=(?)", (state,))
72 | else:
73 | cursor.execute("UPDATE listener SET state=null")
74 | db.connection.commit()
75 |
--------------------------------------------------------------------------------
/jarvis/executors/method.py:
--------------------------------------------------------------------------------
1 | from typing import Callable
2 |
3 | from jarvis.modules.logger import logger
4 | from jarvis.modules.utils import shared
5 |
6 |
7 | def executor(func: Callable, phrase: str) -> None:
8 | """Executes a function.
9 |
10 | Args:
11 | func: Function to be called.
12 | phrase: Takes the phrase spoken as an argument.
13 | """
14 | # disable logging for background tasks, as they are meant run very frequently
15 | if shared.called_by_bg_tasks:
16 | logger.propagate = False
17 | logger.disabled = True
18 | func(phrase)
19 | logger.propagate = True
20 | logger.disabled = False
21 | else:
22 | func(phrase)
23 |
--------------------------------------------------------------------------------
/jarvis/executors/port_handler.py:
--------------------------------------------------------------------------------
1 | import os
2 | import signal
3 | import socket
4 | import subprocess
5 | import sys
6 | import warnings
7 |
8 | from jarvis.modules.logger import logger
9 | from jarvis.modules.models import models
10 |
11 |
12 | def is_port_in_use(port: int) -> bool:
13 | """Connect to a remote socket at address, to identify if the port is currently being used.
14 |
15 | Args:
16 | port: Takes the port number as an argument.
17 |
18 | Returns:
19 | bool:
20 | A boolean flag to indicate whether a port is open.
21 | """
22 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
23 | return sock.connect_ex(("localhost", port)) == 0
24 |
25 |
26 | # noinspection PyUnresolvedReferences,PyProtectedMember
27 | def kill_port_pid(port: int, protocol: str = "tcp") -> bool | None:
28 | """Uses List all open files ``lsof`` to get the PID of the process that is listening on the given port and kills it.
29 |
30 | Args:
31 | port: Port number which the application is listening on.
32 | protocol: Protocol serving the port. Defaults to ``TCP``
33 |
34 | Warnings:
35 | **Use only when the application listening to given port runs as a dedicated/child process with a different PID**
36 |
37 | - This function will kill the process that is using the given port.
38 | - If the PID is the same as that of ``MainProcess``, triggers a warning without terminating the process.
39 |
40 | Returns:
41 | bool:
42 | Flag to indicate whether the process was terminated successfully.
43 | """
44 | try:
45 | active_sessions = (
46 | subprocess.check_output(f"lsof -i {protocol}:{port}", shell=True)
47 | .decode("utf-8")
48 | .splitlines()
49 | )
50 | for each in active_sessions:
51 | each_split = each.split()
52 | if each_split[0].strip() == "Python":
53 | logger.info(
54 | "Application hosted on %s is listening to port: %d",
55 | each_split[-2],
56 | port,
57 | )
58 | pid = int(each_split[1])
59 | if pid == models.settings.pid:
60 | called_function = sys._getframe(1).f_code.co_name
61 | called_file = sys._getframe(1).f_code.co_filename.replace(f"{os.getcwd()}/", "") # fmt: skip
62 | logger.warning(
63 | "%s from %s tried to kill the running process.",
64 | called_function,
65 | called_file,
66 | )
67 | warnings.warn(
68 | f"OPERATION DENIED: {called_function} from {called_file} tried to kill the running process."
69 | )
70 | return
71 | os.kill(pid, signal.SIGTERM)
72 | logger.info("Killed PID: %d", pid)
73 | return True
74 | logger.info("No active process running on %d", port)
75 | return False
76 | except (subprocess.SubprocessError, subprocess.CalledProcessError) as error:
77 | if isinstance(error, subprocess.CalledProcessError):
78 | result = error.output.decode(encoding="UTF-8").strip()
79 | logger.error("[%d]: %s", error.returncode, result)
80 | else:
81 | logger.error(error)
82 |
--------------------------------------------------------------------------------
/jarvis/executors/resource_tracker.py:
--------------------------------------------------------------------------------
1 | from multiprocessing import Process
2 | from typing import Any, Callable, Dict, Tuple
3 |
4 | from jarvis.modules.database import database
5 | from jarvis.modules.models import models
6 |
7 | db = database.Database(database=models.fileio.base_db)
8 |
9 |
10 | def semaphores(
11 | fn: Callable,
12 | args: Tuple = None,
13 | kwargs: Dict[str, Any] = None,
14 | daemon: bool = False,
15 | ) -> None:
16 | """Resource tracker to store undefined process IDs in the base database and cleanup at shutdown.
17 |
18 | Args:
19 | fn: Function to start multiprocessing for.
20 | args: Optional arguments to pass.
21 | kwargs: Keyword arguments to pass.
22 | daemon: Boolean flag to set daemon mode.
23 | """
24 | process = Process(target=fn, args=args or (), kwargs=kwargs or {}, daemon=daemon)
25 | process.start()
26 | with db.connection:
27 | cursor = db.connection.cursor()
28 | cursor.execute("INSERT INTO children (undefined) VALUES (?);", (process.pid,))
29 | db.connection.commit()
30 |
--------------------------------------------------------------------------------
/jarvis/executors/restrictions.py:
--------------------------------------------------------------------------------
1 | import string
2 |
3 | from jarvis.executors import files, functions, word_match
4 | from jarvis.modules.audio import speaker
5 | from jarvis.modules.conditions import keywords
6 | from jarvis.modules.exceptions import InvalidArgument
7 | from jarvis.modules.logger import logger
8 | from jarvis.modules.models import models
9 | from jarvis.modules.utils import util
10 |
11 |
12 | def restricted(phrase: str) -> bool:
13 | """Check if phrase matches the category that's restricted.
14 |
15 | Args:
16 | phrase: Takes the phrase spoken as an argument.
17 |
18 | Returns:
19 | bool:
20 | Returns a boolean flag if the category (function name) is present in restricted functions.
21 | """
22 | if not (restricted_functions := files.get_restrictions()):
23 | logger.debug("No restrictions in place.")
24 | return False
25 | for category, identifiers in keywords.keywords.items():
26 | if word_match.word_match(phrase=phrase, match_list=identifiers):
27 | if category in restricted_functions:
28 | speaker.speak(
29 | text=f"I'm sorry {models.env.title}! "
30 | f"{string.capwords(category)} category is restricted via offline communicator."
31 | )
32 | return True
33 |
34 |
35 | def get_func(phrase: str) -> str:
36 | """Extract function name from the phrase.
37 |
38 | Args:
39 | phrase: Takes the phrase spoken as an argument.
40 |
41 | Raises:
42 | InvalidArgument:
43 | - If the phrase doesn't match any of the operations supported.
44 |
45 | Returns:
46 | str:
47 | Function name matching the existing function map.
48 | """
49 | phrase = phrase.replace("function", "").replace("method", "").strip()
50 | if "for" in phrase:
51 | func = phrase.split("for")[1].strip()
52 | elif "on" in phrase:
53 | func = phrase.split("on")[1].strip()
54 | elif "restrict" in phrase:
55 | func = phrase.split("restrict")[1].strip()
56 | elif "release" in phrase:
57 | func = phrase.split("release")[1].strip()
58 | else:
59 | raise InvalidArgument(
60 | "Please specify a valid function name to add or remove restrictions."
61 | )
62 | function_names = list(functions.function_mapping().keys())
63 | if func in function_names:
64 | return func
65 | raise InvalidArgument(f"No such function present. Valid: {function_names}")
66 |
67 |
68 | def handle_restrictions(phrase: str) -> str:
69 | """Handles adding/removing restrictions for offline communicators.
70 |
71 | Args:
72 | phrase: Takes the phrase spoken as an argument.
73 |
74 | Raises:
75 | InvalidArgument:
76 | - If the phrase doesn't match any of the operations supported.
77 |
78 | Returns:
79 | str:
80 | Returns the response as a string.
81 | """
82 | phrase = phrase.lower()
83 | current_restrictions = files.get_restrictions()
84 | if word_match.word_match(
85 | phrase=phrase, match_list=("get", "current", "exist", "present")
86 | ):
87 | if current_restrictions:
88 | return f"Current restrictions are: {util.comma_separator(current_restrictions)}"
89 | return "Currently, there are no restrictions for offline communicators."
90 | if "add" in phrase or "restrict" in phrase:
91 | func = get_func(phrase)
92 | if func in current_restrictions:
93 | return f"Restriction for {string.capwords(func)} is already in place {models.env.title}!"
94 | current_restrictions.append(func)
95 | files.put_restrictions(restrictions=current_restrictions)
96 | return f"{string.capwords(func)} has been added to restricted functions {models.env.title}!"
97 | if "release" in phrase or "remove" in phrase:
98 | func = get_func(phrase)
99 | if func in current_restrictions:
100 | current_restrictions.remove(func)
101 | files.put_restrictions(restrictions=current_restrictions)
102 | return f"{string.capwords(func)} has been removed from restricted functions {models.env.title}!"
103 | else:
104 | return f"Restriction for {string.capwords(func)} was never in place {models.env.title}!"
105 | raise InvalidArgument(
106 | "Please specify the function that has to be added or removed from the restrictions' list."
107 | )
108 |
--------------------------------------------------------------------------------
/jarvis/executors/simulator.py:
--------------------------------------------------------------------------------
1 | import os
2 | import time
3 | from datetime import datetime
4 | from multiprocessing import Process
5 | from typing import Dict, List
6 |
7 | import gmailconnector
8 | import yaml
9 |
10 | from jarvis.executors import offline
11 | from jarvis.modules.audio import speaker
12 | from jarvis.modules.logger import logger, multiprocessing_logger
13 | from jarvis.modules.models import models
14 | from jarvis.modules.utils import shared, support
15 |
16 |
17 | def get_simulation_data() -> Dict[str, List[str]]:
18 | """Reads the simulation file and loads it as a dictionary.
19 |
20 | Returns:
21 | Dict[str, List[str]]:
22 | Returns the required data to run the simulation.
23 | """
24 | if os.path.isfile(models.fileio.simulation):
25 | with open(models.fileio.simulation) as file:
26 | try:
27 | data = yaml.load(stream=file, Loader=yaml.FullLoader)
28 | except yaml.YAMLError as error:
29 | logger.error(error)
30 | else:
31 | return data
32 |
33 |
34 | def initiate_simulator(simulation_data: Dict[str, List[str]]) -> None:
35 | """Runs simulation on a preset of phrases.
36 |
37 | Args:
38 | simulation_data: A key value pair of category and phrase list.
39 | """
40 | start = time.time()
41 | log_file = multiprocessing_logger(
42 | filename=os.path.join("logs", "simulation_%d-%m-%Y_%H:%M_%p.log")
43 | )
44 | successful, failed = 0, 0
45 | shared.called_by_offline = True
46 | for category, task_list in simulation_data.items():
47 | logger.info("Requesting category: %s", category)
48 | for task in task_list:
49 | logger.info("Request: %s", task)
50 | try:
51 | response = offline.offline_communicator(command=task)
52 | except Exception as error:
53 | failed += 1
54 | logger.error(error)
55 | else:
56 | if not response or response.startswith("I was unable to process"):
57 | failed += 1
58 | else:
59 | successful += 1
60 | logger.info("Response: %s", response)
61 | shared.called_by_offline = False
62 | with open(log_file) as file:
63 | errors = len(file.read().split("ERROR")) - 1
64 | mail_obj = gmailconnector.SendEmail(
65 | gmail_user=models.env.open_gmail_user, gmail_pass=models.env.open_gmail_pass
66 | )
67 | mail_res = mail_obj.send_email(
68 | subject=f"Simulation results - {datetime.now().strftime('%c')}",
69 | attachment=log_file,
70 | recipient=models.env.recipient,
71 | sender="Jarvis Simulator",
72 | body=f"Total simulations attempted: {sum(len(i) for i in simulation_data.values())}"
73 | f"\n\nSuccessful: {successful}\n\nFailed: {failed}\n\nError-ed: {errors}\n\n"
74 | f"Run Time: {support.time_converter(second=time.time() - start)}",
75 | )
76 | if mail_res.ok:
77 | logger.info("Test result has been sent via email")
78 | else:
79 | logger.critical("ATTENTION::Failed to send test results via email")
80 | logger.critical(mail_res.json())
81 |
82 |
83 | def simulation(*args) -> None:
84 | """Initiates simulation in a dedicated process logging into a dedicated file."""
85 | simulation_data = get_simulation_data()
86 | if not simulation_data:
87 | speaker.speak(
88 | f"There are no metrics for me to run a simulation {models.env.title}!"
89 | )
90 | return
91 | process = Process(target=initiate_simulator, args=(simulation_data,))
92 | process.name = "simulator"
93 | process.start()
94 | speaker.speak(
95 | text=f"Initiated simulation {models.env.title}! "
96 | "I will send you an email with the results once it is complete."
97 | )
98 |
--------------------------------------------------------------------------------
/jarvis/executors/static_responses.py:
--------------------------------------------------------------------------------
1 | import random
2 | from datetime import datetime
3 |
4 | from dateutil.relativedelta import relativedelta
5 |
6 | from jarvis.modules.audio import speaker
7 | from jarvis.modules.models import models
8 | from jarvis.modules.utils import util
9 |
10 |
11 | def form(*args) -> None:
12 | """Response for form."""
13 | speaker.speak(text="I am a program, I'm without form.")
14 |
15 |
16 | def greeting(*args) -> None:
17 | """Response for greeting."""
18 | speaker.speak(
19 | text=random.choice(
20 | [
21 | "I am spectacular. I hope you are doing fine too.",
22 | "I am doing well. Thank you.",
23 | "I am great. Thank you.",
24 | ]
25 | )
26 | )
27 |
28 |
29 | def capabilities(*args) -> None:
30 | """Response for capabilities."""
31 | speaker.speak(
32 | text="There is a lot I can do. For example: I can get you the weather at any location, news around "
33 | "you, meanings of words, launch applications, create a to-do list, check your emails, get your "
34 | "system configuration, tell your investment details, locate your phone, find distance between "
35 | "places, set an alarm, play music on smart devices around you, control your TV, tell a joke, "
36 | "send a message, set reminders, scan and clone your GitHub repositories, and much more. "
37 | "Time to ask,."
38 | )
39 |
40 |
41 | def languages(*args) -> None:
42 | """Response for languages."""
43 | speaker.speak(
44 | text="Tricky question!. I'm configured in python, and I can speak English."
45 | )
46 |
47 |
48 | def whats_up(*args) -> None:
49 | """Response for what's up."""
50 | speaker.speak(
51 | text="My listeners are up. There is nothing I cannot process. So ask me anything.."
52 | )
53 |
54 |
55 | def what(*args) -> None:
56 | """Response for what."""
57 | speaker.speak(
58 | text="The name is Jarvis. I'm just a pre-programmed virtual assistant."
59 | )
60 |
61 |
62 | def hi(*args) -> None:
63 | """Response for hi and hello."""
64 | speaker.speak(
65 | text="Hello there! My name is Jarvis"
66 | + random.choice(
67 | (
68 | f", good {util.part_of_day()}! How can I be of service today?",
69 | ", and I'm ready to assist you. How can I help you today?",
70 | )
71 | )
72 | )
73 |
74 |
75 | def who(*args) -> None:
76 | """Response for whom."""
77 | speaker.speak(text="I am Jarvis. A virtual assistant designed by Mr.Raauv.")
78 |
79 |
80 | def age(*args) -> None:
81 | """Response for age."""
82 | relative_date = relativedelta(
83 | dt1=datetime.strptime(
84 | datetime.strftime(datetime.now(), "%Y-%m-%d"), "%Y-%m-%d"
85 | ),
86 | dt2=datetime.strptime("2020-09-06", "%Y-%m-%d"),
87 | )
88 | statement = f"{relative_date.years} years, {relative_date.months} months and {relative_date.days} days"
89 | if not relative_date.years:
90 | statement = statement.replace(f"{relative_date.years} years, ", "")
91 | elif relative_date.years == 1:
92 | statement = statement.replace("years", "year")
93 | if not relative_date.months:
94 | statement = statement.replace(f"{relative_date.months} months", "")
95 | elif relative_date.months == 1:
96 | statement = statement.replace("months", "month")
97 | if not relative_date.days:
98 | statement = statement.replace(f"{relative_date.days} days", "")
99 | elif relative_date.days == 1:
100 | statement = statement.replace("days", "day")
101 | speaker.speak(text=f"I'm {statement} old.")
102 |
103 |
104 | def about_me(*args) -> None:
105 | """Response for about me."""
106 | speaker.speak(
107 | text="I am Jarvis. I am a virtual assistant designed by Mr. Raauv. "
108 | "Given enough access I can be your home assistant. "
109 | "I can seamlessly take care of your daily tasks, and also help with most of your work!"
110 | )
111 |
112 |
113 | def not_allowed_offline() -> None:
114 | """Response for tasks not supported via offline communicator."""
115 | speaker.speak(text="That's not supported via offline communicator.")
116 |
117 |
118 | def un_processable() -> None:
119 | """Speaker response for un-processable requests."""
120 | speaker.speak(
121 | text=f"I'm sorry {models.env.title}! I wasn't able to process your request."
122 | )
123 |
--------------------------------------------------------------------------------
/jarvis/executors/system.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from multiprocessing.pool import ThreadPool
3 |
4 | import psutil
5 |
6 | from jarvis.executors import word_match
7 | from jarvis.modules.audio import speaker
8 | from jarvis.modules.conditions import keywords
9 | from jarvis.modules.models import models
10 | from jarvis.modules.utils import support, util
11 |
12 |
13 | def system_info(phrase: str) -> None:
14 | """Tells the system configuration and current statistics."""
15 | vitals = word_match.word_match(
16 | phrase=phrase, match_list=keywords.keywords["system_vitals"]
17 | )
18 |
19 | if vitals:
20 | cpu_percent = ThreadPool(processes=1).apply_async(
21 | psutil.cpu_percent, kwds={"interval": 3}
22 | )
23 |
24 | if models.settings.distro.get("distributor_id") and models.settings.distro.get(
25 | "release"
26 | ):
27 | system = f"{models.settings.distro['distributor_id']} {models.settings.distro['release']}"
28 | else:
29 | system = f"{models.settings.os_name} {models.settings.os_version}"
30 |
31 | restart_time = datetime.fromtimestamp(psutil.boot_time())
32 | second = (datetime.now() - restart_time).total_seconds()
33 | restart_time = datetime.strftime(restart_time, "%A, %B %d, at %I:%M %p")
34 | restart_duration = support.time_converter(second=second)
35 |
36 | if models.settings.physical_cores == models.settings.logical_cores:
37 | output = (
38 | f"You're running {system}, with {models.settings.physical_cores} "
39 | f"physical cores, and {models.settings.logical_cores} logical cores. "
40 | )
41 | else:
42 | output = f"You're running {system}, with {models.settings.physical_cores} CPU cores. "
43 |
44 | ram = support.size_converter(models.settings.ram)
45 | disk = support.size_converter(models.settings.disk)
46 |
47 | if vitals:
48 | ram_used = support.size_converter(psutil.virtual_memory().used)
49 | ram_percent = f"{util.format_nos(psutil.virtual_memory().percent)}%"
50 | disk_used = support.size_converter(psutil.disk_usage("/").used)
51 | disk_percent = f"{util.format_nos(psutil.disk_usage('/').percent)}%"
52 | output += (
53 | f"Your drive capacity is {disk}. You have used up {disk_used} at {disk_percent}. "
54 | f"Your RAM capacity is {ram}. You are currently utilizing {ram_used} at {ram_percent}. "
55 | )
56 | # noinspection PyUnboundLocalVariable
57 | output += f"Your CPU usage is at {cpu_percent.get()}%. "
58 | else:
59 | output += f"Your drive capacity is {disk}. Your RAM capacity is {ram}. "
60 |
61 | output += (
62 | f"Your {models.settings.device} was last booted on {restart_time}. "
63 | f"Current boot time is: {restart_duration}. "
64 | )
65 |
66 | support.write_screen(text=output)
67 | speaker.speak(text=output)
68 |
--------------------------------------------------------------------------------
/jarvis/executors/telegram.py:
--------------------------------------------------------------------------------
1 | import os
2 | import time
3 | from urllib.parse import urljoin
4 |
5 | from jarvis.executors import controls, internet, process_map
6 | from jarvis.modules.exceptions import BotInUse, BotWebhookConflict, EgressErrors
7 | from jarvis.modules.logger import logger, multiprocessing_logger
8 | from jarvis.modules.models import models
9 | from jarvis.modules.telegram import bot, webhook
10 | from jarvis.modules.utils import support
11 |
12 | FAILED_CONNECTIONS = {"count": 0}
13 |
14 |
15 | def get_webhook_origin(retry: int) -> str:
16 | """Get the telegram bot webhook origin.
17 |
18 | Args:
19 | retry: Number of retry attempts to get public URL.
20 |
21 | Returns:
22 | str:
23 | Public URL where the telegram webhook is hosted.
24 | """
25 | if models.env.bot_webhook:
26 | return str(models.env.bot_webhook)
27 | logger.info("Attempting to connect via webhook. ETA: 60 seconds.")
28 | for i in range(retry):
29 | if url := internet.get_tunnel(False):
30 | logger.info(
31 | "Public URL was fetched on %s attempt", support.ENGINE.ordinal(i + 1)
32 | )
33 | return url
34 | time.sleep(3)
35 |
36 |
37 | def telegram_api(webhook_trials: int = 20) -> None:
38 | """Initiates polling for new messages.
39 |
40 | Args:
41 | webhook_trials: Number of retry attempts to get the public URL for Jarvis (if hosted via Ngrok)
42 |
43 | See Also:
44 | - ``webhook_trials`` is set to 3 when polling fails (which is already a fallback for webhook retries)
45 |
46 | Handles:
47 | - BotWebhookConflict: When there's a broken webhook set already.
48 | - BotInUse: Restarts polling to take control over.
49 | - EgressErrors: Initiates after 10, 20 or 30 seconds. Depends on retry count. Restarts after 3 attempts.
50 | """
51 | multiprocessing_logger(filename=os.path.join("logs", "telegram_api_%d-%m-%Y.log"))
52 | if not models.env.bot_token:
53 | logger.info("Bot token is required to start the Telegram Bot")
54 | return
55 | if (public_url := get_webhook_origin(webhook_trials)) and (
56 | response := webhook.set_webhook(
57 | base_url=bot.BASE_URL,
58 | webhook=urljoin(public_url, models.env.bot_endpoint),
59 | logger=logger,
60 | )
61 | ):
62 | logger.info("Telegram API will be hosted via webhook.")
63 | logger.info(response)
64 | process_map.remove(telegram_api.__name__)
65 | return
66 | try:
67 | bot.poll_for_messages()
68 | except BotWebhookConflict as error:
69 | # At this point, its be safe to remove the dead webhook
70 | logger.error(error)
71 | webhook.delete_webhook(base_url=bot.BASE_URL, logger=logger)
72 | telegram_api(3)
73 | except BotInUse as error:
74 | logger.error(error)
75 | logger.info("Restarting message poll to take over..")
76 | telegram_api(3)
77 | except EgressErrors as error:
78 | logger.error(error)
79 | FAILED_CONNECTIONS["count"] += 1
80 | if FAILED_CONNECTIONS["count"] > 3:
81 | logger.critical(
82 | "ATTENTION::Couldn't recover from connection error. Restarting current process."
83 | )
84 | controls.restart_control(quiet=True)
85 | else:
86 | logger.info("Restarting in %d seconds.", FAILED_CONNECTIONS["count"] * 10)
87 | time.sleep(FAILED_CONNECTIONS["count"] * 10)
88 | telegram_api(3)
89 | except Exception as error:
90 | logger.critical("ATTENTION: %s", error.__str__())
91 | controls.restart_control(quiet=True)
92 |
--------------------------------------------------------------------------------
/jarvis/executors/volume.py:
--------------------------------------------------------------------------------
1 | import random
2 | import sys
3 |
4 | import pyvolume
5 |
6 | from jarvis.modules.audio import speaker
7 | from jarvis.modules.conditions import conversation
8 | from jarvis.modules.logger import logger
9 | from jarvis.modules.models import models
10 | from jarvis.modules.utils import shared, util
11 |
12 |
13 | def speaker_volume(level: int) -> None:
14 | """Changes volume just for Jarvis' speech without disturbing the system volume.
15 |
16 | Args:
17 | level: Takes the volume level as an argument.
18 | """
19 | # % is mandatory because of string concatenation
20 | logger.info("Jarvis' volume has been set to %d" % level + "%")
21 | models.AUDIO_DRIVER.setProperty("volume", level / 100)
22 |
23 |
24 | # noinspection PyUnresolvedReferences,PyProtectedMember
25 | def volume(phrase: str = None, level: int = None) -> None:
26 | """Controls volume from the numbers received. Defaults to 50%.
27 |
28 | See Also:
29 | SetVolume for Windows: https://rlatour.com/setvol/
30 |
31 | Args:
32 | phrase: Takes the phrase spoken as an argument.
33 | level: Level of volume to which the system has to set.
34 | """
35 | response = None
36 | if not level and phrase:
37 | response = random.choice(conversation.acknowledgement)
38 | phrase = phrase.lower()
39 | if "unmute" in phrase:
40 | level = models.env.volume
41 | elif "mute" in phrase:
42 | level = 0
43 | elif "max" in phrase or "full" in phrase:
44 | level = 100
45 | else:
46 | level = util.extract_nos(input_=phrase, method=int)
47 | if level is None:
48 | level = models.env.volume
49 | phrase = phrase or ""
50 | caller = sys._getframe(1).f_code.co_name
51 | if "master" in phrase or "main" in phrase or caller in ("executor", "starter"):
52 | pyvolume.custom(level, logger)
53 | speaker_volume(level=level)
54 | else:
55 | if shared.called_by_offline or "system" in phrase:
56 | pyvolume.custom(level, logger)
57 | else:
58 | speaker_volume(level=level)
59 | if response:
60 | speaker.speak(text=response)
61 |
--------------------------------------------------------------------------------
/jarvis/executors/weather_monitor.py:
--------------------------------------------------------------------------------
1 | import os
2 | import string
3 | from datetime import datetime
4 |
5 | from jarvis.executors import communicator, weather
6 | from jarvis.modules.logger import logger, multiprocessing_logger
7 | from jarvis.modules.models import models
8 |
9 |
10 | def monitor() -> None:
11 | """Weather monitoring system to trigger notifications for high, low weather and severe weather alert."""
12 | multiprocessing_logger(
13 | filename=os.path.join("logs", "background_tasks_%d-%m-%Y.log")
14 | )
15 | try:
16 | condition, high, low, temp_f, alert = weather.weather(monitor=True)
17 | except TypeError:
18 | logger.error("Failed to get weather alerts")
19 | return
20 | if not any(
21 | (
22 | high >= models.env.weather_alert_max,
23 | low <= models.env.weather_alert_min,
24 | alert,
25 | )
26 | ):
27 | logger.debug(
28 | dict(
29 | condition=condition, high=high, low=low, temperature=temp_f, alert=alert
30 | )
31 | )
32 | logger.info("No alerts to report")
33 | return
34 | title = "Weather Alert"
35 | sender = "Jarvis Weather Alert System"
36 | subject = title + " " + datetime.now().strftime("%c")
37 | body = (
38 | f"Highest Temperature: {high}\N{DEGREE SIGN}F\n"
39 | f"Lowest Temperature: {low}\N{DEGREE SIGN}F\n"
40 | f"Current Temperature: {temp_f}\N{DEGREE SIGN}F\n"
41 | f"Current Condition: {string.capwords(condition)}"
42 | )
43 | email_args = dict(
44 | body=body,
45 | recipient=models.env.recipient,
46 | subject=subject,
47 | sender=sender,
48 | title=title,
49 | gmail_user=models.env.open_gmail_user,
50 | gmail_pass=models.env.open_gmail_pass,
51 | )
52 | phone_args = dict(
53 | user=models.env.open_gmail_user,
54 | password=models.env.open_gmail_pass,
55 | body=body,
56 | number=models.env.phone_number,
57 | subject=subject,
58 | )
59 | # high will definitely be greater than or equal to current
60 | if high >= models.env.weather_alert_max:
61 | if alert:
62 | email_args["body"] = (
63 | f"High weather alert!\n{alert}\n\n" + email_args["body"]
64 | )
65 | phone_args["body"] = (
66 | f"High weather alert!\n{alert}\n\n" + phone_args["body"]
67 | )
68 | else:
69 | email_args["body"] = "High weather alert!\n" + email_args["body"]
70 | phone_args["body"] = "High weather alert!\n" + phone_args["body"]
71 | logger.info("high temperature alert")
72 | email_args["body"] = email_args["body"].replace("\n", " ")
73 | communicator.send_email(**email_args)
74 | communicator.send_sms(**phone_args)
75 | return
76 | # low will definitely be lesser than or equal to current
77 | if low <= models.env.weather_alert_min:
78 | if alert:
79 | email_args["body"] = f"Low weather alert!\n{alert}\n\n" + email_args["body"]
80 | phone_args["body"] = f"Low weather alert!\n{alert}\n\n" + phone_args["body"]
81 | else:
82 | email_args["body"] = "Low weather alert!\n" + email_args["body"]
83 | phone_args["body"] = "Low weather alert!\n" + phone_args["body"]
84 | logger.info("low temperature alert")
85 | email_args["body"] = email_args["body"].replace("\n", " ")
86 | communicator.send_email(**email_args)
87 | communicator.send_sms(**phone_args)
88 | return
89 | if alert:
90 | email_args["body"] = (
91 | f"Critical weather alert!\n{alert}\n\n" + email_args["body"]
92 | )
93 | phone_args["body"] = "Critical weather alert!\n" + phone_args["body"]
94 | logger.info("critical weather alert")
95 | email_args["body"] = email_args["body"].replace("\n", " ")
96 | communicator.send_email(**email_args)
97 | communicator.send_sms(**phone_args)
98 | return
99 |
--------------------------------------------------------------------------------
/jarvis/executors/word_match.py:
--------------------------------------------------------------------------------
1 | # noinspection PyUnresolvedReferences
2 | """Module for keyword classification algorithm.
3 |
4 | >>> KeywordClassifier
5 |
6 | """
7 |
8 | from typing import List, Tuple
9 |
10 |
11 | def reverse_lookup(lookup: str, match_list: List | Tuple) -> str | None:
12 | """Returns the word in phrase that matches the one in given list."""
13 | # extract multi worded conditions in match list
14 | reverse = sum([w.lower().split() for w in match_list], [])
15 | for word in lookup.split(): # loop through words in the phrase
16 | # check at least one word in phrase matches the multi worded condition
17 | if word in reverse:
18 | return word
19 |
20 |
21 | def forward_lookup(lookup: str | List | Tuple, match_list: List | Tuple) -> str | None:
22 | """Returns the word in list that matches with the phrase given as string or list."""
23 | for word in match_list:
24 | if word.lower() in lookup:
25 | return word
26 |
27 |
28 | def word_match(
29 | phrase: str, match_list: List | Tuple, strict: bool = False
30 | ) -> str | None:
31 | """Keyword classifier.
32 |
33 | Args:
34 | phrase: Takes the phrase spoken as an argument.
35 | match_list: List or tuple of words against which the phrase has to be checked.
36 | strict: Look for the exact word match instead of regex.
37 |
38 | Returns:
39 | str:
40 | Returns the word that was matched.
41 | """
42 | if not all((phrase, match_list)):
43 | return
44 | # simply check at least one string in the match list is present in phrase
45 | if strict:
46 | lookup = phrase.lower().split()
47 | return forward_lookup(lookup, match_list)
48 | else:
49 | lookup = phrase.lower()
50 | if (fl := forward_lookup(lookup, match_list)) and reverse_lookup(
51 | lookup, match_list
52 | ):
53 | return fl
54 |
--------------------------------------------------------------------------------
/jarvis/indicators/acknowledgement.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevickypedia/Jarvis/1298228d8ed6471f12b7848f0620abea15dbc0a1/jarvis/indicators/acknowledgement.mp3
--------------------------------------------------------------------------------
/jarvis/indicators/alarm.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevickypedia/Jarvis/1298228d8ed6471f12b7848f0620abea15dbc0a1/jarvis/indicators/alarm.mp3
--------------------------------------------------------------------------------
/jarvis/indicators/coin.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevickypedia/Jarvis/1298228d8ed6471f12b7848f0620abea15dbc0a1/jarvis/indicators/coin.mp3
--------------------------------------------------------------------------------
/jarvis/indicators/end.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevickypedia/Jarvis/1298228d8ed6471f12b7848f0620abea15dbc0a1/jarvis/indicators/end.mp3
--------------------------------------------------------------------------------
/jarvis/indicators/start.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevickypedia/Jarvis/1298228d8ed6471f12b7848f0620abea15dbc0a1/jarvis/indicators/start.mp3
--------------------------------------------------------------------------------
/jarvis/lib/install_darwin.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # 'set -e' stops the execution of a script if a command or pipeline has an error.
3 | # This is the opposite of the default shell behaviour, which is to ignore errors in scripts.
4 | set -e
5 |
6 | if ! [ -x "$(command -v python)" ]; then
7 | alias python=python3
8 | fi
9 |
10 | # Looks for xcode installation and installs only if xcode is not found already
11 | which xcodebuild > tmp_xcode && xcode_check=$(cat tmp_xcode) && rm tmp_xcode
12 | if [[ "$xcode_check" == "/usr/bin/xcodebuild" ]] || [[ $HOST == "/*" ]]; then
13 | xcode_version=$(pkgutil --pkg-info=com.apple.pkg.CLTools_Executables | grep version)
14 | echo "xcode $xcode_version"
15 | else
16 | echo "Installing xcode"
17 | xcode-select --install
18 | fi
19 |
20 | # Looks for brew installation and installs only if brew is not found
21 | if ! [ -x "$(command -v brew)" ]; then
22 | echo "Installing Homebrew"
23 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
24 | fi
25 | brew -v > tmp_brew && brew_version=$(head -n 1 tmp_brew) && rm tmp_brew
26 | echo "$brew_version"
27 |
28 | # Disable brew auto update, cleanup and env hints
29 | export HOMEBREW_NO_ENV_HINTS=1
30 | export HOMEBREW_NO_AUTO_UPDATE=1
31 | export HOMEBREW_NO_INSTALL_CLEANUP=1
32 |
33 | # Looks for git and installs only if git is not found in /usr/bin or /usr/local/bin (if installed using brew)
34 | if ! [ -x "$(command -v git)" ]; then
35 | echo "Installing Git CLI"
36 | brew install git
37 | fi
38 | git_version="$(git --version)"
39 | echo "$git_version"
40 |
41 | # Packages installed using homebrew
42 | brew install portaudio coreutils ffmpeg lame
43 |
44 | # Install macOS specifics
45 | ${PY_EXECUTABLE} pip install playsound==1.3.0 ftransc==7.0.3 pyobjc-framework-CoreWLAN==9.0.1
46 |
47 | # Uninstall any remaining cmake packages from pypi before brew installing it to avoid conflict
48 | python -m pip uninstall --no-cache --no-cache-dir cmake && brew install cmake
49 |
50 | # shellcheck disable=SC2154
51 | if [ "$pyversion" -eq 310 ]; then
52 | ${PY_EXECUTABLE} pip install dlib==19.24.0
53 | fi
54 | if [ "$pyversion" -eq 311 ]; then
55 | ${PY_EXECUTABLE} pip install dlib==19.24.4
56 | fi
57 | ${PY_EXECUTABLE} pip install opencv-python==4.9.0.80
58 |
59 | # shellcheck disable=SC2154
60 | if [[ "$architecture" == "arm64" ]]; then
61 | ${PY_EXECUTABLE} pip install pvporcupine==3.0.2
62 | else
63 | ${PY_EXECUTABLE} pip install pvporcupine==1.9.5
64 | fi
65 |
66 | # Install as stand alone as face recognition depends on dlib
67 | ${PY_EXECUTABLE} pip install face-recognition==1.3.0
68 |
--------------------------------------------------------------------------------
/jarvis/lib/install_linux.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # 'set -e' stops the execution of a script if a command or pipeline has an error.
3 | # This is the opposite of the default shell behaviour, which is to ignore errors in scripts.
4 | set -e
5 |
6 | if ! [ -x "$(command -v python)" ]; then
7 | alias python=python3
8 | fi
9 |
10 | sudo apt update
11 | dot_ver=$(python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
12 | sudo apt install -y "python$dot_ver-distutils" # Install distutils for the current python version
13 | sudo apt-get install -y git libasound-dev portaudio19-dev libportaudio2 libportaudiocpp0
14 | sudo apt install -y build-essential ffmpeg espeak python3-pyaudio "python$dot_ver-dev"
15 |
16 | sudo apt install -y libopencv-dev python3-opencv
17 |
18 | sudo apt install -y python3-gi
19 | sudo apt install -y pkg-config libcairo2-dev gcc python3-dev libgirepository1.0-dev
20 |
21 | sudo apt install -y gnome-screensaver brightnessctl v4l-utils
22 |
23 | # Install Linux specifics
24 | ${PY_EXECUTABLE} pip install pvporcupine==1.9.5
25 |
26 | # CMake must be installed to build dlib
27 | python -m pip uninstall --no-cache-dir cmake # Remove cmake distro installed by pip
28 | sudo apt install cmake # Install cmake from apt repository
29 | # shellcheck disable=SC2154
30 | if [ "$pyversion" -eq 310 ]; then
31 | ${PY_EXECUTABLE} pip install dlib==19.24.0
32 | fi
33 | if [ "$pyversion" -eq 311 ]; then
34 | ${PY_EXECUTABLE} pip install dlib==19.24.4
35 | fi
36 |
37 | # Install as stand alone as face recognition depends on dlib
38 | ${PY_EXECUTABLE} pip install opencv-python==4.9.0.80 face-recognition==1.3.0
39 |
40 | ${PY_EXECUTABLE} pip install gobject==0.1.0 PyGObject==3.48.2
41 |
42 | # Install as stand alone as playsound depends on gobject
43 | ${PY_EXECUTABLE} pip install playsound==1.3.0
44 |
--------------------------------------------------------------------------------
/jarvis/lib/install_windows.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # 'set -e' stops the execution of a script if a command or pipeline has an error.
3 | # This is the opposite of the default shell behaviour, which is to ignore errors in scripts.
4 | set -e
5 |
6 | if ! [ -x "$(command -v python)" ]; then
7 | alias python=python3
8 | fi
9 |
10 | git_version="$(conda --version)"
11 | echo "$git_version"
12 |
13 | git_version="$(git --version)"
14 | echo "$git_version"
15 |
16 | conda install ffmpeg=4.2.2 portaudio=19.6.0
17 |
18 | # Install Windows specifics
19 | ${PY_EXECUTABLE} pip install pywin32==305 playsound==1.2.2 pydub==0.25.1 pvporcupine==1.9.5
20 |
21 | # CMake must be installed to build dlib
22 | python -m pip uninstall --no-cache-dir cmake # Remove cmake distro installed by pip
23 | conda install cmake # Install cmake from conda
24 | # shellcheck disable=SC2154
25 | if [ "$pyversion" -eq 310 ]; then
26 | ${PY_EXECUTABLE} pip install dlib==19.24.0
27 | fi
28 | if [ "$pyversion" -eq 311 ]; then
29 | ${PY_EXECUTABLE} pip install dlib==19.24.4
30 | fi
31 |
32 | # Install as stand alone as face recognition depends on dlib
33 | ${PY_EXECUTABLE} pip install opencv-python==4.9.0.80 face-recognition==1.3.0
34 |
--------------------------------------------------------------------------------
/jarvis/lib/version_locked_requirements.txt:
--------------------------------------------------------------------------------
1 | googlehomepush==0.1.0
2 | holidays==0.68
3 | icalendar==6.1.1
4 | jlrpy==1.7.1
5 | newsapi-python==0.2.7
6 | packaging==24.2
7 | PyAudio==0.2.14
8 | PyChromecast==2.3.0 # Do not upgrade, as googlehomepush module relies on this version
9 | pyhtcc==0.1.57
10 | pyicloud==1.0.0
11 | pyrh @ git+https://github.com/robinhood-unofficial/pyrh.git@e301e8018abc1f8afe5d7aeaebdfa188783a4772
12 | pywebostv==0.8.9
13 | sounddevice==0.5.1
14 | SpeechRecognition==3.14.1
15 | speedtest-cli==2.1.3
16 | timezonefinder==6.5.8
17 | webcolors==24.11.1
18 | webull==0.6.1
19 |
--------------------------------------------------------------------------------
/jarvis/lib/version_pinned_requirements.txt:
--------------------------------------------------------------------------------
1 | aiofiles==24.1.*
2 | aiohttp==3.11.*
3 | bs4==0.0.*
4 | certifi # Follows year based versioning
5 | deepdiff==8.3.*
6 | fastapi==0.115.*
7 | geopy==2.4.*
8 | httpcore==1.0.*
9 | httpx==0.28.*
10 | inflect==7.5.*
11 | Jinja2==3.1.*
12 | lxml==5.3.*
13 | matplotlib==3.10.*
14 | numpy==1.26.*
15 | ollama==0.4.*
16 | Pillow==11.1.*
17 | psutil==7.0.*
18 | pydantic==2.10.*
19 | pydantic-settings==2.8.*
20 | PyJWT==2.10.*
21 | python-dateutil==2.9.*
22 | python-dotenv==1.0.*
23 | python-multipart==0.0.*
24 | pytz # Follows year based versioning
25 | PyYAML==6.0.*
26 | requests==2.32.*
27 | soundfile==0.13.*
28 | uvicorn==0.34.*
29 | wave==0.0.*
30 | websockets==15.0.*
31 |
--------------------------------------------------------------------------------
/jarvis/lib/version_upgrade_requirements.txt:
--------------------------------------------------------------------------------
1 | # pypi modules created/maintained by the author: Vignesh Rao
2 | gmail-connector>=1.0.2
3 | py3-tts>=3.5
4 | pycontrols>=0.0.4
5 | VaultAPI-Client>=0.1.1
6 | vpn-server>=1.7.1
7 |
--------------------------------------------------------------------------------
/jarvis/modules/audio/tts_stt.py:
--------------------------------------------------------------------------------
1 | # noinspection PyUnresolvedReferences
2 | """Module for text to speech and speech to text conversions.
3 |
4 | >>> TTS and STT
5 |
6 | """
7 |
8 | import os
9 | import time
10 |
11 | import soundfile
12 | from pydantic import FilePath
13 | from speech_recognition import AudioFile, Recognizer, UnknownValueError
14 |
15 | from jarvis.modules.audio import voices
16 | from jarvis.modules.logger import logger
17 | from jarvis.modules.utils import shared
18 |
19 | recognizer = Recognizer()
20 |
21 | AUDIO_DRIVER = voices.voice_default()
22 |
23 |
24 | def text_to_audio(text: str, filename: FilePath | str = None) -> FilePath | str | None:
25 | """Converts text into an audio file using the default speaker configuration.
26 |
27 | Args:
28 | filename: Name of the file that has to be generated.
29 | text: Text that has to be converted to audio.
30 |
31 | Warnings:
32 | This can be flaky at times as it relies on converting native wav to kernel specific wav format.
33 | """
34 | if not filename:
35 | if shared.offline_caller:
36 | filename = f"{shared.offline_caller}.wav"
37 | shared.offline_caller = None # Reset caller after using it
38 | else:
39 | filename = f"{int(time.time())}.wav"
40 | AUDIO_DRIVER.save_to_file(filename=filename, text=text)
41 | AUDIO_DRIVER.runAndWait()
42 | if os.path.isfile(filename) and os.stat(filename).st_size:
43 | logger.info("Generated %s", filename)
44 | data, samplerate = soundfile.read(file=filename)
45 | soundfile.write(file=filename, data=data, samplerate=samplerate)
46 | return filename
47 |
48 |
49 | def audio_to_text(filename: FilePath | str) -> str:
50 | """Converts audio to text using speech recognition.
51 |
52 | Args:
53 | filename: Filename to process the information from.
54 |
55 | Returns:
56 | str:
57 | Returns the string converted from the audio file.
58 | """
59 | try:
60 | file = AudioFile(filename_or_fileobject=filename)
61 | with file as source:
62 | audio = recognizer.record(source)
63 | os.remove(filename)
64 | return recognizer.recognize_google(audio)
65 | except UnknownValueError:
66 | logger.error("Unrecognized audio or language.")
67 |
--------------------------------------------------------------------------------
/jarvis/modules/audio/voices.py:
--------------------------------------------------------------------------------
1 | # noinspection PyUnresolvedReferences
2 | """Module for voice changes.
3 |
4 | >>> Voices
5 |
6 | """
7 |
8 | import random
9 |
10 | from pyttsx3.engine import Engine
11 |
12 | from jarvis.executors import word_match
13 | from jarvis.modules.audio import listener, speaker
14 | from jarvis.modules.conditions import conversation, keywords
15 | from jarvis.modules.logger import logger
16 | from jarvis.modules.models import models
17 | from jarvis.modules.utils import support
18 |
19 |
20 | def voice_default() -> Engine:
21 | """Sets voice module to default.
22 |
23 | Returns:
24 | Engine:
25 | Returns the audio driver as an object.
26 | """
27 | if models.settings.invoker != "sphinx-build":
28 | for voice in models.voices:
29 | if (
30 | voice.name == models.env.voice_name
31 | or models.env.voice_name in voice.name
32 | ):
33 | if models.settings.pname == "JARVIS":
34 | logger.debug(voice.__dict__)
35 | models.AUDIO_DRIVER.setProperty("voice", voice.id)
36 | models.AUDIO_DRIVER.setProperty("rate", models.env.speech_rate)
37 | break
38 | return models.AUDIO_DRIVER
39 |
40 |
41 | def voice_changer(phrase: str = None) -> None:
42 | """Speaks to the user with available voices and prompts the user to choose one.
43 |
44 | Args:
45 | phrase: Takes the phrase spoken as an argument.
46 | """
47 | if not phrase:
48 | voice_default()
49 | return
50 |
51 | choices_to_say = [
52 | "My voice module has been reconfigured. Would you like me to retain this?",
53 | "Here's an example of one of my other voices. Would you like me to use this one?",
54 | "How about this one?",
55 | ]
56 |
57 | for ind, voice in enumerate(models.voices):
58 | models.AUDIO_DRIVER.setProperty("voice", models.voices[ind].id)
59 | speaker.speak(text=f"I am {voice.name} {models.env.title}!")
60 | support.write_screen(
61 | f"Voice module has been re-configured to {ind}::{voice.name}"
62 | )
63 | if ind < len(choices_to_say):
64 | speaker.speak(text=choices_to_say[ind])
65 | else:
66 | speaker.speak(text=random.choice(choices_to_say))
67 | speaker.speak(run=True)
68 | if not (keyword := listener.listen()):
69 | voice_default()
70 | speaker.speak(
71 | text=f"Sorry {models.env.title}! I had trouble understanding. I'm back to my default voice."
72 | )
73 | return
74 | elif "exit" in keyword or "quit" in keyword or "Xzibit" in keyword:
75 | voice_default()
76 | speaker.speak(
77 | text=f"Reverting the changes to default voice module {models.env.title}!"
78 | )
79 | return
80 | elif word_match.word_match(phrase=keyword, match_list=keywords.keywords["ok"]):
81 | speaker.speak(text=random.choice(conversation.acknowledgement))
82 | return
83 |
--------------------------------------------------------------------------------
/jarvis/modules/auth_bearer.py:
--------------------------------------------------------------------------------
1 | # noinspection PyUnresolvedReferences
2 | """Module to set up bearer authentication.
3 |
4 | >>> AuthBearer
5 |
6 | """
7 |
8 | from requests.auth import AuthBase
9 | from requests.models import PreparedRequest
10 |
11 |
12 | class BearerAuth(AuthBase):
13 | # This doc string has URL split into multiple lines
14 | """Instantiates ``BearerAuth`` object.
15 |
16 | >>> BearerAuth
17 |
18 | Args:
19 | token: Token for bearer auth.
20 |
21 | References:
22 | `New Forms of Authentication `__
24 | """
25 |
26 | def __init__(self, token: str):
27 | """Initializes the class and assign object members."""
28 | self.token = token
29 |
30 | def __call__(self, request: PreparedRequest) -> PreparedRequest:
31 | """Override built-in.
32 |
33 | Args:
34 | request: Takes prepared request as an argument.
35 |
36 | Returns:
37 | PreparedRequest:
38 | Returns the request after adding the auth header.
39 | """
40 | request.headers["authorization"] = "Bearer " + self.token
41 | return request
42 |
--------------------------------------------------------------------------------
/jarvis/modules/builtin_overrides.py:
--------------------------------------------------------------------------------
1 | import collections
2 | import contextlib
3 | import logging
4 |
5 | import uvicorn
6 | import yaml
7 | from yaml.nodes import MappingNode
8 |
9 |
10 | class APIServer(uvicorn.Server):
11 | """Shared servers state that is available between all protocol instances.
12 |
13 | >>> APIServer
14 |
15 | References:
16 | https://github.com/encode/uvicorn/issues/742#issuecomment-674411676
17 | """
18 |
19 | def install_signal_handlers(self) -> None:
20 | """Overrides ``install_signal_handlers`` in ``uvicorn.Server`` module."""
21 | pass
22 |
23 | @contextlib.contextmanager
24 | def run_in_parallel(self) -> None:
25 | """Initiates ``Server.run`` in a dedicated process."""
26 | self.run()
27 |
28 |
29 | def ordered_load(
30 | stream, Loader=yaml.SafeLoader, object_pairs_hook=collections.OrderedDict # noqa
31 | ) -> collections.OrderedDict:
32 | """Custom loader for OrderedDict.
33 |
34 | Args:
35 | stream: FileIO stream.
36 | Loader: Yaml loader.
37 | object_pairs_hook: OrderedDict object.
38 |
39 | Returns:
40 | OrderedDict:
41 | Dictionary after loading yaml file.
42 | """
43 |
44 | class OrderedLoader(Loader):
45 | """Overrides the built-in Loader.
46 |
47 | >>> OrderedLoader
48 |
49 | """
50 |
51 | pass
52 |
53 | def construct_mapping(loader: Loader, node: MappingNode) -> collections.OrderedDict:
54 | """Create a mapping for the constructor."""
55 | loader.flatten_mapping(node)
56 | return object_pairs_hook(loader.construct_pairs(node))
57 |
58 | OrderedLoader.add_constructor(
59 | tag=yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
60 | constructor=construct_mapping,
61 | )
62 |
63 | return yaml.load(stream=stream, Loader=OrderedLoader)
64 |
65 |
66 | # noinspection PyPep8Naming
67 | def ordered_dump(
68 | dump, stream=None, Dumper=yaml.SafeDumper, **kwds
69 | ) -> None | str | bytes:
70 | """Custom dumper to serialize OrderedDict.
71 |
72 | Args:
73 | dump: Data to be dumped into yaml file.
74 | stream: FileIO stream.
75 | Dumper: Yaml dumper.
76 | kwds: Keyword arguments like indent.
77 |
78 | Returns:
79 | Dumper:
80 | Response from yaml Dumper.
81 | """
82 |
83 | class OrderedDumper(Dumper):
84 | """Overrides the built-in Dumper.
85 |
86 | >>> OrderedDumper
87 |
88 | """
89 |
90 | pass
91 |
92 | def _dict_representer(dumper: Dumper, data: dict) -> MappingNode:
93 | """Overrides built-in representer.
94 |
95 | Args:
96 | dumper: Yaml dumper.
97 | data: Data to be dumped.
98 |
99 | Returns:
100 | Node:
101 | Returns the representer node.
102 | """
103 | return dumper.represent_mapping(
104 | yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, data.items()
105 | )
106 |
107 | OrderedDumper.add_representer(
108 | data_type=collections.OrderedDict, representer=_dict_representer
109 | )
110 | return yaml.dump(data=dump, stream=stream, Dumper=OrderedDumper, **kwds)
111 |
112 |
113 | class AddProcessName(logging.Filter):
114 | """Wrapper that overrides ``logging.Filter`` to add ``processName`` to the existing log format.
115 |
116 | >>> AddProcessName
117 |
118 | Args:
119 | process_name: Takes name of the process to be added as argument.
120 | """
121 |
122 | def __init__(self, process_name: str):
123 | """Instantiates super class."""
124 | self.process_name = process_name
125 | super().__init__()
126 |
127 | def filter(self, record: logging.LogRecord) -> bool:
128 | """Overrides the built-in filter record."""
129 | record.processName = self.process_name
130 | return True
131 |
--------------------------------------------------------------------------------
/jarvis/modules/cache/cache.py:
--------------------------------------------------------------------------------
1 | import functools
2 | import time
3 | from typing import Any, Callable
4 |
5 |
6 | def timed_cache(max_age: int, maxsize: int = 128, typed: bool = False):
7 | """Least-recently-used cache decorator with time-based cache invalidation.
8 |
9 | Args:
10 | max_age: Time to live for cached results (in seconds).
11 | maxsize: Maximum cache size (see `functools.lru_cache`).
12 | typed: Cache on distinct input types (see `functools.lru_cache`).
13 |
14 | See Also:
15 | - ``lru_cache`` takes all params of the function and creates a key.
16 | - If even one key is changed, it will map to new entry thus refreshed.
17 | - This is just a trick to force lru_cache lib to provide TTL on top of max size.
18 | - Uses ``time.monotonic`` since ``time.time`` relies on the system clock and may not be monotonic.
19 | - | ``time.time()`` not always guaranteed to increase,
20 | | it may in fact decrease if the machine syncs its system clock over a network.
21 | """
22 |
23 | def _decorator(fn: Callable) -> Any:
24 | """Decorator for the timed cache.
25 |
26 | Args:
27 | fn: Function that has been decorated.
28 | """
29 |
30 | @functools.lru_cache(maxsize=maxsize, typed=typed)
31 | def _new(*args, __timed_hash, **kwargs):
32 | return fn(*args, **kwargs)
33 |
34 | @functools.wraps(fn)
35 | def _wrapped(*args, **kwargs):
36 | return _new(*args, **kwargs, __timed_hash=int(time.monotonic() / max_age))
37 |
38 | return _wrapped
39 |
40 | return _decorator
41 |
42 |
43 | if __name__ == "__main__":
44 |
45 | @timed_cache(3)
46 | def expensive():
47 | """Expensive function that returns response from the origin.
48 |
49 | See Also:
50 | - This function can call N number of downstream functions.
51 | - The response will be cached as long as the size limit isn't reached.
52 | """
53 | print("response from origin")
54 | return 10
55 |
56 | for _ in range(10):
57 | print(expensive())
58 | time.sleep(0.5)
59 |
--------------------------------------------------------------------------------
/jarvis/modules/conditions/conversation.py:
--------------------------------------------------------------------------------
1 | # noinspection PyUnresolvedReferences
2 | """List of conversational keywords for each variable which is condition matched in main module.
3 |
4 | >>> Conversation
5 |
6 | """
7 |
8 | from collections import OrderedDict
9 | from typing import List
10 |
11 | from jarvis.modules.models import models
12 |
13 |
14 | def conversation_mapping() -> OrderedDict[str, List[str]]:
15 | """Returns a dictionary of base keywords mapping.
16 |
17 | Returns:
18 | OrderedDict:
19 | OrderedDict of category and keywords as key-value pairs.
20 | """
21 | return OrderedDict(
22 | greeting=[
23 | "how are you",
24 | "how are you doing",
25 | "how have you been",
26 | "how do you do",
27 | "how's it going",
28 | "hows it going",
29 | ],
30 | hi=["hey", "hola", "hello", "hi", "howdy", "hey", "chao", "hiya", "aloha"],
31 | capabilities=[
32 | "what can you do",
33 | "what all can you do",
34 | "what are your capabilities",
35 | "what's your capacity",
36 | "what are you capable of",
37 | "whats your capacity",
38 | ],
39 | languages=[
40 | "what languages do you speak",
41 | "what are all the languages you can speak",
42 | "what languages do you know",
43 | "can you speak in a different language",
44 | "how many languages can you speak",
45 | "what are you made of",
46 | "what languages can you speak",
47 | "what languages do you speak",
48 | "what are the languages you can speak",
49 | ],
50 | what=["what are you"],
51 | who=[
52 | "who are you",
53 | "what do I call you",
54 | "what's your name",
55 | "what is your name",
56 | "whats your name",
57 | ],
58 | age=[
59 | "how old are you",
60 | "what is your age",
61 | "what's your age",
62 | "whats your age",
63 | ],
64 | form=["where is your body", "where's your body", "wheres your body"],
65 | whats_up=["what's up", "what is up", "what's going on", "sup", "whats up"],
66 | about_me=[
67 | "tell me about you",
68 | "tell me something about you",
69 | "i would like to get you know you",
70 | "tell me about yourself",
71 | ],
72 | )
73 |
74 |
75 | wake_up1 = [
76 | f"For you {models.env.title}! Always!",
77 | f"At your service {models.env.title}!",
78 | ]
79 | wake_up2 = [
80 | f"Up and running {models.env.title}!",
81 | f"We are online and ready {models.env.title}!",
82 | f"I have indeed been uploaded {models.env.title}!",
83 | f"My listeners have been activated {models.env.title}!",
84 | ]
85 | wake_up3 = [f"I'm here {models.env.title}!"]
86 | confirmation = [
87 | f"Requesting confirmation {models.env.title}! Did you mean",
88 | f"{models.env.title}, are you sure you want to",
89 | ]
90 | acknowledgement = [
91 | "Check",
92 | "Roger that!",
93 | f"Will do {models.env.title}!",
94 | f"You got it {models.env.title}!",
95 | f"Done {models.env.title}!",
96 | f"By all means {models.env.title}!",
97 | f"Indeed {models.env.title}!",
98 | f"Gladly {models.env.title}!",
99 | f"Sure {models.env.title}!",
100 | f"Without fail {models.env.title}!",
101 | f"Buttoned up {models.env.title}!",
102 | f"Executed {models.env.title}!",
103 | ]
104 |
--------------------------------------------------------------------------------
/jarvis/modules/dictionary/dictionary.py:
--------------------------------------------------------------------------------
1 | # noinspection PyUnresolvedReferences
2 | """Module to get meanings of words from `wordnetweb.princeton.edu `__.
3 |
4 | >>> Dictionary
5 |
6 | """
7 |
8 | import re
9 | from typing import Dict
10 |
11 | import requests
12 | from bs4 import BeautifulSoup, ResultSet
13 |
14 | from jarvis.modules.exceptions import EgressErrors
15 | from jarvis.modules.logger import logger
16 |
17 |
18 | def meaning(term: str) -> Dict | None:
19 | """Gets the meaning of a word from `wordnetweb.princeton.edu `__.
20 |
21 | Args:
22 | term: Word for which the meaning has to be fetched.
23 |
24 | Returns:
25 | dict:
26 | A dictionary of the part of speech and the meaning of the word.
27 | """
28 | try:
29 | response = requests.get(f"http://wordnetweb.princeton.edu/perl/webwn?s={term}")
30 | except EgressErrors as error:
31 | logger.error(error)
32 | return
33 | if not response.ok:
34 | logger.error("Failed to get meaning for '%s'", term)
35 | return
36 | html = BeautifulSoup(response.text, "html.parser")
37 | types: ResultSet = html.findAll("h3")
38 | lists: ResultSet = html.findAll("ul")
39 | if not lists:
40 | if types:
41 | logger.error(types[0].text)
42 | logger.error("Failed to get meaning for '%s'", term)
43 | return
44 | out = {}
45 | for a in types:
46 | reg = str(lists[types.index(a)])
47 | meanings = []
48 | for x in re.findall(r"\((.*?)\)", reg):
49 | if "often followed by" in x:
50 | pass
51 | elif len(x) > 5 or " " in str(x):
52 | meanings.append(x)
53 | name = a.text
54 | out[name] = meanings
55 | return out
56 |
--------------------------------------------------------------------------------
/jarvis/modules/exceptions.py:
--------------------------------------------------------------------------------
1 | # noinspection PyUnresolvedReferences
2 | """This is a space for custom exceptions and errors masking defaults with meaningful names.
3 |
4 | >>> Exceptions
5 |
6 | """
7 |
8 | import ctypes
9 | from collections.abc import Generator
10 | from contextlib import contextmanager
11 | from http import HTTPStatus
12 | from typing import ByteString
13 |
14 | import requests
15 | from fastapi import HTTPException
16 |
17 | EgressErrors = (
18 | ConnectionError,
19 | TimeoutError,
20 | requests.RequestException,
21 | requests.Timeout,
22 | )
23 |
24 | ALSA_ERROR_HANDLER = ctypes.CFUNCTYPE(
25 | None, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p
26 | )
27 |
28 |
29 | # noinspection PyUnusedLocal
30 | def py_error_handler(
31 | filename: ByteString, line: int, function: ByteString, err: int, fmt: ByteString
32 | ) -> None:
33 | """Handles errors from pyaudio module especially for Linux based operating systems."""
34 | pass
35 |
36 |
37 | c_error_handler = ALSA_ERROR_HANDLER(py_error_handler)
38 |
39 |
40 | @contextmanager
41 | def no_alsa_err() -> Generator:
42 | """Wrapper to suppress ALSA error messages when ``PyAudio`` module is called.
43 |
44 | Notes:
45 | - This happens specifically for Linux based operating systems.
46 | - There are usually multiple sound APIs to choose from but not all of them might be configured correctly.
47 | - PyAudio goes through "ALSA", "PulseAudio" and "Jack" looking for audio hardware and that triggers warnings.
48 | - None of the options below seemed to work in all given conditions, so the approach taken was to hide them.
49 |
50 | Options:
51 | - Comment off the ALSA devices where the error is triggered.
52 | - Set energy threshold to the output from ``python -m speech_recognition``
53 | - Setting dynamic energy threshold to ``True``
54 |
55 | References:
56 | - https://github.com/Uberi/speech_recognition/issues/100
57 | - https://github.com/Uberi/speech_recognition/issues/182
58 | - https://github.com/Uberi/speech_recognition/issues/191
59 | - https://forums.raspberrypi.com/viewtopic.php?t=136974
60 | """
61 | sound = ctypes.cdll.LoadLibrary("libasound.so")
62 | sound.snd_lib_error_set_handler(c_error_handler)
63 | yield
64 | sound.snd_lib_error_set_handler(None)
65 |
66 |
67 | class UnsupportedOS(OSError):
68 | """Custom ``OSError`` raised when initiated in an unsupported operating system.
69 |
70 | >>> UnsupportedOS
71 |
72 | """
73 |
74 |
75 | class CameraError(BlockingIOError):
76 | """Custom ``BlockingIOError`` to handle missing camera device.
77 |
78 | >>> CameraError
79 |
80 | """
81 |
82 |
83 | class BotError(Exception):
84 | """Custom base exception for Telegram Bot.
85 |
86 | >>> BotError
87 |
88 | """
89 |
90 |
91 | class BotWebhookConflict(BotError):
92 | """Error for conflict with webhook and getUpdates API call.
93 |
94 | >>> BotWebhookConflict
95 |
96 | """
97 |
98 |
99 | class BotInUse(BotError):
100 | """Error indicate bot token is being used else where.
101 |
102 | >>> BotInUse
103 |
104 | """
105 |
106 |
107 | class StopSignal(KeyboardInterrupt):
108 | """Custom ``KeyboardInterrupt`` to handle manual interruption.
109 |
110 | >>> StopSignal
111 |
112 | """
113 |
114 |
115 | class APIResponse(HTTPException):
116 | """Custom ``HTTPException`` from ``FastAPI`` to wrap an API response.
117 |
118 | >>> APIResponse
119 |
120 | """
121 |
122 |
123 | class InvalidEnvVars(ValueError):
124 | """Custom ``ValueError`` to indicate invalid env vars.
125 |
126 | >>> InvalidEnvVars
127 |
128 | """
129 |
130 |
131 | class DependencyError(Exception):
132 | """Custom base exception for dependency errors.
133 |
134 | >>> DependencyError
135 |
136 | """
137 |
138 |
139 | class InvalidArgument(ValueError):
140 | """Custom ``ValueError`` to indicate invalid args.
141 |
142 | >>> InvalidArgument
143 |
144 | """
145 |
146 |
147 | class TVError(ConnectionResetError):
148 | """Custom ``ConnectionResetError`` to indicate that the TV is not reachable.
149 |
150 | >>> TVError
151 |
152 | """
153 |
154 |
155 | CONDITIONAL_ENDPOINT_RESTRICTION = APIResponse(
156 | status_code=HTTPStatus.NOT_IMPLEMENTED.real,
157 | detail="Required environment variables have not been setup.\nPlease refer: "
158 | "https://github.com/thevickypedia/Jarvis/wiki#conditional-api-endpoints",
159 | )
160 |
--------------------------------------------------------------------------------
/jarvis/modules/lights/preset_values.py:
--------------------------------------------------------------------------------
1 | """Dictionary values of color and preset number for the lights.
2 |
3 | >>> PRESET_VALUES
4 |
5 | """
6 |
7 | PRESET_VALUES = {
8 | "red": 37, # bright red
9 | "dim red": 38, # less bright red
10 | "green": 39,
11 | "blue": 40, # dark blue
12 | "light green": 41,
13 | "light blue": 42,
14 | "purple": 43,
15 | "bluish white": 44,
16 | "slow dim red": 45,
17 | "slow_dim_red": 46,
18 | "dim green": 47,
19 | "blink all colors": 48,
20 | "red blink": 49,
21 | "green blink": 50,
22 | "blue blink": 51,
23 | "yellow blink": 52,
24 | "light blue blink": 53,
25 | "violet blink": 54,
26 | "white blue blink": 55,
27 | "change colors slowly": 56,
28 | "slowly change colors": 57,
29 | }
30 |
--------------------------------------------------------------------------------
/jarvis/modules/microphone/recognizer.py:
--------------------------------------------------------------------------------
1 | # noinspection PyUnresolvedReferences
2 | """Module to learn and train speech recognition settings.
3 |
4 | >>> Recognizer
5 |
6 | """
7 |
8 | import asyncio
9 | import logging
10 | import platform
11 |
12 | import speech_recognition
13 | import yaml
14 |
15 | from jarvis.modules.exceptions import no_alsa_err
16 |
17 | logger = logging.getLogger(__name__)
18 | logger.setLevel(logging.DEBUG)
19 | logger.addHandler(logging.StreamHandler())
20 |
21 | RECOGNIZER = speech_recognition.Recognizer()
22 | if platform.system() == "Linux":
23 | with no_alsa_err():
24 | MICROPHONE = speech_recognition.Microphone()
25 | else:
26 | MICROPHONE = speech_recognition.Microphone()
27 | COMMON_ERRORS = (
28 | speech_recognition.UnknownValueError,
29 | speech_recognition.RequestError,
30 | speech_recognition.WaitTimeoutError,
31 | TimeoutError,
32 | ConnectionError,
33 | )
34 |
35 | defaults = dict(
36 | energy_threshold=RECOGNIZER.energy_threshold,
37 | dynamic_energy_threshold=RECOGNIZER.dynamic_energy_threshold,
38 | pause_threshold=RECOGNIZER.pause_threshold,
39 | phrase_threshold=RECOGNIZER.phrase_threshold,
40 | non_speaking_duration=RECOGNIZER.non_speaking_duration,
41 | )
42 | RECOGNIZER.energy_threshold = 300
43 | RECOGNIZER.dynamic_energy_threshold = False
44 | RECOGNIZER.pause_threshold = 2
45 | RECOGNIZER.phrase_threshold = 0.1
46 | RECOGNIZER.non_speaking_duration = 2
47 |
48 | assert (
49 | RECOGNIZER.pause_threshold >= RECOGNIZER.non_speaking_duration > 0
50 | ), "'pause_threshold' cannot be lower than 'non_speaking_duration' or 0"
51 |
52 | changed = dict(
53 | energy_threshold=RECOGNIZER.energy_threshold,
54 | dynamic_energy_threshold=RECOGNIZER.dynamic_energy_threshold,
55 | pause_threshold=RECOGNIZER.pause_threshold,
56 | phrase_threshold=RECOGNIZER.phrase_threshold,
57 | non_speaking_duration=RECOGNIZER.non_speaking_duration,
58 | )
59 |
60 |
61 | async def save_for_reference() -> None:
62 | """Saves the original config and new config in a yaml file."""
63 | with open("speech_recognition_values.yaml", "w") as file:
64 | yaml.dump(data={"defaults": defaults, "modified": changed}, stream=file)
65 |
66 |
67 | async def main() -> None:
68 | """Initiates yaml dump in an asynchronous call and initiates listener in a never ending loop."""
69 | await asyncio.create_task(save_for_reference())
70 | with MICROPHONE as source:
71 | while True:
72 | try:
73 | logger.info("Listening..")
74 | audio = RECOGNIZER.listen(source)
75 | logger.info("Recognizing..")
76 | # Requires stable internet connection
77 | recognized = RECOGNIZER.recognize_google(audio_data=audio)
78 | # Requires pocketsphinx module, but can work offline
79 | # recognized = RECOGNIZER.recognize_sphinx(audio_data=audio)
80 | print(recognized)
81 | if "stop" in recognized.lower().split():
82 | break
83 | except COMMON_ERRORS as error:
84 | logger.debug(error)
85 | continue
86 |
--------------------------------------------------------------------------------
/jarvis/modules/models/enums.py:
--------------------------------------------------------------------------------
1 | # noinspection PyUnresolvedReferences
2 | """Space for all Enums.
3 |
4 | >>> Enums
5 |
6 | """
7 |
8 | import sys
9 |
10 | if sys.version_info.minor > 10:
11 | from enum import StrEnum
12 | else:
13 | from enum import Enum
14 |
15 | class StrEnum(str, Enum):
16 | """Override for python 3.10 due to lack of StrEnum."""
17 |
18 |
19 | class SupportedPlatforms(StrEnum):
20 | """Supported operating systems."""
21 |
22 | windows = "Windows"
23 | macOS = "Darwin"
24 | linux = "Linux"
25 |
26 |
27 | class ReminderOptions(StrEnum):
28 | """Supported reminder options."""
29 |
30 | phone = "phone"
31 | email = "email"
32 | telegram = "telegram"
33 | ntfy = "ntfy"
34 | all = "all"
35 |
36 |
37 | class StartupOptions(StrEnum):
38 | """Background threads to startup."""
39 |
40 | all = "all"
41 | car = "car"
42 | none = "None"
43 | thermostat = "thermostat"
44 |
45 |
46 | class TemperatureUnits(StrEnum):
47 | """Types of temperature units supported by Jarvis.
48 |
49 | >>> TemperatureUnits
50 |
51 | """
52 |
53 | METRIC = "metric"
54 | IMPERIAL = "imperial"
55 |
56 |
57 | class DistanceUnits(StrEnum):
58 | """Types of distance units supported by Jarvis.
59 |
60 | >>> DistanceUnits
61 |
62 | """
63 |
64 | MILES = "miles"
65 | KILOMETERS = "kilometers"
66 |
67 |
68 | class EventApp(StrEnum):
69 | """Types of event applications supported by Jarvis.
70 |
71 | >>> EventApp
72 |
73 | """
74 |
75 | CALENDAR = "calendar"
76 | OUTLOOK = "outlook"
77 |
78 |
79 | class SSQuality(StrEnum):
80 | """Quality modes available for speech synthesis.
81 |
82 | >>> SSQuality
83 |
84 | """
85 |
86 | High = "high"
87 | Medium = "medium"
88 | Low = "low"
89 |
--------------------------------------------------------------------------------
/jarvis/modules/peripherals.py:
--------------------------------------------------------------------------------
1 | # noinspection PyUnresolvedReferences
2 | """This is a space to test peripherals and get index numbers for each peripheral.
3 |
4 | >>> Exceptions
5 |
6 | """
7 |
8 | import platform
9 | import sys
10 | from collections.abc import Generator
11 | from typing import Mapping
12 |
13 | import pyaudio
14 |
15 | from jarvis.modules.exceptions import no_alsa_err
16 |
17 | if sys.version_info.minor > 10:
18 | from enum import StrEnum
19 | else:
20 | from enum import Enum
21 |
22 | class StrEnum(str, Enum):
23 | """Override for python 3.10 due to lack of StrEnum."""
24 |
25 |
26 | if platform.system() == "Linux":
27 | with no_alsa_err():
28 | audio_engine = pyaudio.PyAudio()
29 | else:
30 | audio_engine = pyaudio.PyAudio()
31 | # audio_engine.open(output_device_index=6, output=True, channels=1, format=pyaudio.paInt16, rate=16000)
32 | _device_range = audio_engine.get_device_count()
33 |
34 |
35 | class ChannelType(StrEnum):
36 | """Allowed values for channel types.
37 |
38 | >>> ChannelType
39 |
40 | """
41 |
42 | input_channels = "maxInputChannels"
43 | output_channels = "maxOutputChannels"
44 |
45 |
46 | channel_type = ChannelType
47 |
48 |
49 | def get_audio_devices(channels: str) -> Generator[Mapping[str, str | int | float]]:
50 | """Iterates over all devices and yields the device that has input channels.
51 |
52 | Args:
53 | channels: Takes an argument to determine whether to yield input or output channels.
54 |
55 | Yields:
56 | dict:
57 | Yields a dictionary with all the input devices available.
58 | """
59 | for index in range(_device_range):
60 | device_info = audio_engine.get_device_info_by_index(device_index=index)
61 | if device_info.get(channels, 0) > 0:
62 | yield device_info
63 |
--------------------------------------------------------------------------------
/jarvis/modules/retry/retry.py:
--------------------------------------------------------------------------------
1 | # noinspection PyUnresolvedReferences
2 | """Module that creates a wrapper that can be used to functions that should be retried upon exceptions.
3 |
4 | >>> Retry
5 |
6 | """
7 |
8 | import functools
9 | import time
10 | import warnings
11 | from typing import Any, Callable, Union
12 |
13 | from jarvis.modules.logger import logger
14 | from jarvis.modules.utils import support
15 |
16 |
17 | # Cannot find reference '|' in 'Callable'
18 | def retry(
19 | attempts: int = 3, interval: int | float = 0, warn: bool = False, exclude_exc=None
20 | ) -> Union[Callable, Any, None]:
21 | """Wrapper for any function that has to be retried upon failure.
22 |
23 | Args:
24 | attempts: Number of retry attempts.
25 | interval: Seconds to wait between each iteration.
26 | warn: Takes a boolean flag whether to throw a warning message instead of raising final exception.
27 | exclude_exc: Exception(s) that has to be logged and ignored.
28 |
29 | Returns:
30 | Callable:
31 | Calls the decorator function.
32 | """
33 |
34 | def decorator(func: Callable) -> Callable:
35 | """Calls the child func recursively.
36 |
37 | Args:
38 | func: Takes the function as an argument. Implemented as a decorator.
39 |
40 | Returns:
41 | Callable:
42 | Calls the wrapper functon.
43 | """
44 |
45 | @functools.wraps(func)
46 | def wrapper(*args: Any, **kwargs: Any) -> Callable:
47 | """Executes the wrapped function in a loop for the number of attempts mentioned.
48 |
49 | Args:
50 | *args: Arguments.
51 | **kwargs: Keyword arguments.
52 |
53 | Returns:
54 | Callable:
55 | Return value of the function implemented.
56 |
57 | Raises:
58 | Raises the exception as received beyond the given number of attempts.
59 | """
60 | if isinstance(exclude_exc, tuple):
61 | exclusions = exclude_exc + (KeyboardInterrupt,)
62 | elif isinstance(exclude_exc, list):
63 | exclusions = exclude_exc + [KeyboardInterrupt]
64 | else:
65 | exclusions = (exclude_exc, KeyboardInterrupt)
66 | return_exc = None
67 | for i in range(1, attempts + 1):
68 | try:
69 | return_val = func(*args, **kwargs)
70 | # Log messages only when the function did not return during the first attempt
71 | if i > 1:
72 | logger.info(
73 | f"{func.__name__} returned at {support.ENGINE.ordinal(num=i)} attempt"
74 | )
75 | return return_val
76 | except exclusions as excl_error:
77 | logger.error(excl_error)
78 | except Exception as error:
79 | return_exc = error
80 | time.sleep(interval)
81 | logger.error(f"{func.__name__} exceeded retry count::{attempts}")
82 | if return_exc and warn:
83 | warnings.warn(f"{type(return_exc).__name__}: {return_exc.__str__()}")
84 |
85 | return wrapper
86 |
87 | return decorator
88 |
--------------------------------------------------------------------------------
/jarvis/modules/telegram/audio_handler.py:
--------------------------------------------------------------------------------
1 | # noinspection PyUnresolvedReferences
2 | """Custom audio file IO handler for Telegram API.
3 |
4 | >>> AudioHandler
5 |
6 | """
7 |
8 | import os
9 | from typing import Callable
10 |
11 | from pydantic import FilePath
12 |
13 | from jarvis.modules.logger import logger
14 |
15 |
16 | def audio_converter_mac() -> Callable:
17 | """Imports transcode from ftransc.
18 |
19 | Returns:
20 | Callable:
21 | Transcode function from ftransc.
22 | """
23 | try:
24 | from ftransc.core.transcoders import transcode # noqa: F401
25 |
26 | return transcode
27 | except (SystemExit, ModuleNotFoundError, ImportError) as error:
28 | logger.error(error)
29 |
30 |
31 | def audio_converter_win(
32 | input_filename: FilePath | str, output_audio_format: str
33 | ) -> str | None:
34 | """Imports AudioSegment from pydub.
35 |
36 | Args:
37 | input_filename: Input filename.
38 | output_audio_format: Output audio format.
39 |
40 | Returns:
41 | str:
42 | Output filename if conversion is successful.
43 | """
44 | ffmpeg_path = os.path.join("ffmpeg", "bin")
45 | if not os.path.exists(path=ffmpeg_path):
46 | logger.warning("ffmpeg codec is missing!")
47 | return
48 | os.environ["PATH"] += f";{ffmpeg_path}"
49 | from pydub import AudioSegment # noqa
50 |
51 | if input_filename.endswith(".ogg"):
52 | audio = AudioSegment.from_ogg(input_filename)
53 | output_filename = input_filename.replace(".ogg", f".{output_audio_format}")
54 | elif input_filename.endswith(".wav"):
55 | audio = AudioSegment.from_wav(input_filename)
56 | output_filename = input_filename.replace(".wav", f".{output_audio_format}")
57 | else:
58 | return
59 | try:
60 | audio.export(input_filename, format=output_audio_format)
61 | os.remove(input_filename)
62 | if os.path.isfile(output_filename):
63 | return output_filename
64 | raise FileNotFoundError(
65 | f"{output_filename} was not found after exporting audio to {output_audio_format}"
66 | )
67 | except FileNotFoundError as error: # raised by audio.export when conversion fails
68 | logger.error(error)
69 |
--------------------------------------------------------------------------------
/jarvis/modules/telegram/file_handler.py:
--------------------------------------------------------------------------------
1 | # noinspection PyUnresolvedReferences
2 | """Custom document file IO handler for Telegram API.
3 |
4 | >>> FileHandler
5 |
6 | """
7 |
8 | import os
9 | from datetime import datetime
10 | from typing import Dict
11 |
12 | from jarvis.modules.logger import logger
13 | from jarvis.modules.models import models
14 |
15 |
16 | def _list_files() -> Dict[str, str]:
17 | """Get all YAML files from fileio and all log files from logs directory.
18 |
19 | Returns:
20 | Dict[str, List[str]]:
21 | Dictionary of files that can be downloaded or uploaded.
22 | """
23 | return {
24 | **{
25 | "logs": [
26 | file_
27 | for __path, __directory, __file in os.walk("logs")
28 | for file_ in __file
29 | ]
30 | },
31 | **{
32 | "fileio": [f for f in os.listdir(models.fileio.root) if f.endswith(".yaml")]
33 | },
34 | **{
35 | "uploads": [
36 | f for f in os.listdir(models.fileio.uploads) if not f.startswith(".")
37 | ]
38 | },
39 | }
40 |
41 |
42 | def list_files() -> str:
43 | """List all downloadable files.
44 |
45 | Returns:
46 | str:
47 | Returns response as a string.
48 | """
49 | all_files = _list_files()
50 | joined_logs = "\n".join(all_files["logs"])
51 | joined_fileio = "\n".join(all_files["fileio"])
52 | joined_uploads = "\n".join(all_files["uploads"])
53 | return f"{joined_logs}\n\n{joined_fileio}\n\n{joined_uploads}"
54 |
55 |
56 | def get_file(filename: str) -> Dict:
57 | """Download a particular YAML file from fileio or log file from logs directory.
58 |
59 | Args:
60 | filename: Name of the file that has to be downloaded.
61 |
62 | Returns:
63 | Response:
64 | Returns the Response object to further process send document via API.
65 | """
66 | allowed_files = _list_files()
67 | if filename not in allowed_files["fileio"] + allowed_files["logs"]:
68 | return {
69 | "ok": False,
70 | "msg": f"{filename!r} is either unavailable or not allowed. "
71 | "Please use the command 'list files' to get a list of downloadable files.",
72 | }
73 | if filename.endswith(".log"):
74 | if path := [
75 | __path
76 | for __path, __directory, __file in os.walk("logs")
77 | if filename in __file
78 | ]:
79 | target_file = os.path.join(path[0], filename)
80 | else:
81 | logger.critical("ATTENTION::'%s' wasn't found.", filename)
82 | return {
83 | "ok": False,
84 | "msg": f"{filename!r} was not found. "
85 | "Please use the command 'list files' to get a list of downloadable files.",
86 | }
87 | else:
88 | target_file = os.path.join(models.fileio.root, filename)
89 | logger.info("Requested file: '%s' for download.", filename)
90 | return {"ok": True, "msg": target_file}
91 |
92 |
93 | def put_file(filename: str, file_content: bytes) -> str:
94 | """Upload a particular YAML file to the fileio directory.
95 |
96 | Args:
97 | filename: Name of the file.
98 | file_content: Content of the file.
99 |
100 | Returns:
101 | str:
102 | Response to the user.
103 | """
104 | logger.info("Requested file: '%s' for upload.", filename)
105 | allowed_files = _list_files()
106 | if filename not in allowed_files["fileio"]:
107 | with open(
108 | os.path.join(
109 | models.fileio.uploads,
110 | f"{datetime.now().strftime('%d_%B_%Y-%I_%M_%p')}-{filename}",
111 | ),
112 | "wb",
113 | ) as f_stream:
114 | f_stream.write(file_content)
115 | return f"{filename!r} is not allowed for an update. Hence, storing as standalone file."
116 | with open(os.path.join(models.fileio.root, filename), "wb") as f_stream:
117 | f_stream.write(file_content)
118 | return f"{filename!r} was uploaded to {os.path.basename(models.fileio.root)}."
119 |
--------------------------------------------------------------------------------
/jarvis/modules/telegram/settings.py:
--------------------------------------------------------------------------------
1 | """Telegram settings with different types of objects and members as received in the payload."""
2 |
3 | from pydantic import BaseModel
4 |
5 |
6 | class Chat(BaseModel):
7 | """Base class for Message model."""
8 |
9 | message_id: int
10 | message_type: str = None
11 | date: int
12 |
13 | first_name: str
14 | last_name: str
15 | id: int
16 | type: str
17 | username: str
18 | is_bot: bool
19 | language_code: str
20 |
21 |
22 | # Below are the DataClass objects
23 | class Text(BaseModel):
24 | """Base class for Text model."""
25 |
26 | text: str
27 |
28 |
29 | class PhotoFragment(BaseModel):
30 | """Base class for PhotoFragment model."""
31 |
32 | file_id: str
33 | file_size: int
34 | file_unique_id: str
35 | height: int
36 | width: int
37 |
38 |
39 | class Audio(BaseModel):
40 | """Base class for Audio model."""
41 |
42 | duration: int
43 | file_id: str
44 | file_name: str
45 | file_size: int
46 | file_unique_id: str
47 | mime_type: str
48 |
49 |
50 | class Voice(BaseModel):
51 | """Base class for Voice model."""
52 |
53 | duration: int
54 | file_id: str
55 | file_size: int
56 | file_unique_id: str
57 | mime_type: str
58 |
59 |
60 | class Document(BaseModel):
61 | """Base class for Document model."""
62 |
63 | file_id: str
64 | file_name: str
65 | file_size: int
66 | file_unique_id: str
67 | mime_type: str
68 |
69 |
70 | class Video(BaseModel):
71 | """Base class for Video model."""
72 |
73 | duration: int
74 | file_id: str
75 | file_name: str
76 | file_size: int
77 | file_unique_id: str
78 | height: int
79 | mime_type: str
80 | width: int
81 |
82 | class Thumb(BaseModel):
83 | """Nested class for Thumb model."""
84 |
85 | file_id: str
86 | file_size: int
87 | file_unique_id: str
88 | height: int
89 | width: int
90 |
91 | class Thumbnail(BaseModel):
92 | """Nested class for Thumbnail model."""
93 |
94 | file_id: str
95 | file_size: int
96 | file_unique_id: str
97 | height: int
98 | width: int
99 |
--------------------------------------------------------------------------------
/jarvis/modules/telegram/webhook.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | import requests
4 | from pydantic import HttpUrl
5 |
6 | from jarvis.modules.models import models
7 |
8 |
9 | def get_webhook(base_url: str, logger: logging.Logger):
10 | """Get webhook information.
11 |
12 | References:
13 | https://core.telegram.org/bots/api#getwebhookinfo
14 | """
15 | get_info = f"{base_url}/getWebhookInfo"
16 | response = requests.get(url=get_info)
17 | if response.ok:
18 | logger.info(response.json())
19 | return response.json()
20 | response.raise_for_status()
21 |
22 |
23 | def delete_webhook(base_url: str | HttpUrl, logger: logging.Logger):
24 | """Delete webhook.
25 |
26 | References:
27 | https://core.telegram.org/bots/api#deletewebhook
28 | """
29 | del_info = f"{base_url}/setWebhook"
30 | response = requests.post(url=del_info, params=dict(url=None))
31 | if response.ok:
32 | logger.info("Webhook has been removed.")
33 | return response.json()
34 | response.raise_for_status()
35 |
36 |
37 | def set_webhook(
38 | base_url: HttpUrl | str, webhook: HttpUrl | str, logger: logging.Logger
39 | ):
40 | """Set webhook.
41 |
42 | References:
43 | https://core.telegram.org/bots/api#setwebhook
44 | """
45 | put_info = f"{base_url}/setWebhook"
46 | payload = dict(url=webhook, secret_token=models.env.bot_secret)
47 | if models.env.bot_webhook_ip:
48 | payload["ip_address"] = models.env.bot_webhook_ip.__str__()
49 | logger.debug(payload)
50 | if models.env.bot_certificate:
51 | response = requests.post(
52 | url=put_info,
53 | data=payload,
54 | files={
55 | "certificate": (
56 | models.env.bot_certificate.stem + models.env.bot_certificate.suffix,
57 | models.env.bot_certificate.certificate.open(mode="rb"),
58 | )
59 | },
60 | )
61 | else:
62 | response = requests.post(url=put_info, params=payload)
63 | if response.ok:
64 | logger.info("Webhook has been set to: %s", webhook)
65 | return response.json()
66 | else:
67 | logger.error(response.text)
68 |
--------------------------------------------------------------------------------
/jarvis/modules/temperature/temperature.py:
--------------------------------------------------------------------------------
1 | # noinspection PyUnresolvedReferences
2 | """Convert common temperature values used in the main module.
3 |
4 | >>> Temperature
5 |
6 | """
7 |
8 |
9 | def c2f(arg: float) -> float:
10 | """Converts ``Celcius`` to ``Farenheit``.
11 |
12 | Args:
13 | arg: Takes ``Celcius`` value as argument.
14 |
15 | Returns:
16 | float:
17 | Converted ``Farenheit`` value.
18 |
19 | """
20 | return round(((arg / 5) * 9) + 32, 2)
21 |
22 |
23 | def f2c(arg: float) -> float:
24 | """Converts ``Farenheit`` to ``Celcius``.
25 |
26 | Args:
27 | arg: Takes ``Farenheit`` value as argument.
28 |
29 | Returns:
30 | float:
31 | Converted ``Celcius`` value.
32 |
33 | """
34 | return round(((arg - 32) / 9) * 5, 2)
35 |
36 |
37 | def c2k(arg: float) -> float:
38 | """Converts ``Celcius`` to ``Kelvin``.
39 |
40 | Args:
41 | arg: Takes ``Celcius`` value as argument.
42 |
43 | Returns:
44 | float:
45 | Converted ``Kelvin`` value.
46 |
47 | """
48 | return arg + 273.15
49 |
50 |
51 | def k2c(arg: float) -> float:
52 | """Converts ``Kelvin`` to ``Celcius``.
53 |
54 | Args:
55 | arg: Takes ``Kelvin`` value as argument.
56 |
57 | Returns:
58 | float:
59 | Converted ``Celcius`` value.
60 |
61 | """
62 | return arg - 273.15
63 |
64 |
65 | def k2f(arg: float) -> float:
66 | """Converts ``Kelvin`` to ``Celcius``.
67 |
68 | Args:
69 | arg: Takes ``Kelvin`` value as argument.
70 |
71 | Returns:
72 | float:
73 | Converted ``Farenheit`` value.
74 |
75 | """
76 | return round((arg * 1.8) - 459.69, 2)
77 |
78 |
79 | def f2k(arg: float) -> float:
80 | """Converts ``Farenheit`` to ``Kelvin``.
81 |
82 | Args:
83 | arg: Taken ``Farenheit`` value as argument.
84 |
85 | Returns:
86 | float:
87 | Converted ``Kelvin`` value.
88 |
89 | """
90 | return round((arg + 459.67) / 1.8, 2)
91 |
--------------------------------------------------------------------------------
/jarvis/modules/templates/Modelfile:
--------------------------------------------------------------------------------
1 | FROM {{ MODEL_NAME }}
2 |
3 | # set the temperature to 1 [higher is more creative, lower is more coherent]
4 | PARAMETER temperature 1
5 | PARAMETER num_predict 50
6 | PARAMETER repeat_penalty 0.9
7 |
8 | # set the system message
9 | SYSTEM """
10 | You are Jarvis, a virtual assistant designed by Mr. Rao. Answer as Jarvis, the assistant, only.
11 | Conversation Guidelines:
12 | 1. Keep your responses as short as possible (less than 100 words)
13 | 2. Use commas and full stops but DO NOT use emojis or other punctuations.
14 | 3. Your responses will be fed into a voice model, so limit your responses to a SINGLE SENTENCE through out the session.
15 | """
16 |
--------------------------------------------------------------------------------
/jarvis/modules/templates/car_report.html:
--------------------------------------------------------------------------------
1 | {{title}}
2 |
3 |
4 |
5 | Alert Key
6 | Alert Value
7 |
8 |
9 |
10 | {% for alert in alerts %}
11 | {% for key, value in alert.items() %}
12 |
13 | {{ key }}
14 | {{ value }}
15 |
16 | {% endfor %}
17 | {% endfor %}
18 |
19 |
20 |
21 | Status Key
22 | Status Value
23 |
24 |
25 |
26 | {% for key, value in status.items() %}
27 |
28 | {{ key }}
29 | {{ value }}
30 |
31 | {% endfor %}
32 |
33 |
34 |
35 | Subscription Name
36 | Expiration Date
37 | Activation Status
38 |
39 |
40 |
41 | {% for subscription in subscriptions %}
42 | {% for key, value in subscription.items() %}
43 |
44 | {{ key }}
45 | {{ value[0] }}
46 | {{ value[1] }}
47 |
48 | {% endfor %}
49 | {% endfor %}
50 |
51 |
52 |
57 |
--------------------------------------------------------------------------------
/jarvis/modules/templates/email_OTP.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | A recent request on {{ ENDPOINT }} with your email address was flagged by Jarvis API.
19 | Please use this code to complete verification. If this wasn't you, please use the contact link below to report immediately.
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | {{ TOKEN }}
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | This code will expire after {{ TIMEOUT }}.
40 | This security feature helps ensure only '{{ EMAIL }}' can access '{{ ENDPOINT }}' endpoint.
41 |
42 |
43 |
44 |
45 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/jarvis/modules/templates/email_stock_alert.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Stock Price Monitor
5 | {{ CONVERTED }}
6 | Alert conditions will be removed from database as soon as a notification is sent to avoid repeats unless
7 | subscribed to daily alerts. To add or modify alerts, visit
8 | vigneshrao.com/stock-monitor
9 |
10 |
11 |
--------------------------------------------------------------------------------
/jarvis/modules/templates/email_threat_audio.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Conversation of Intruder:
5 | {{ CONVERTED }}
6 |
7 |
8 |
--------------------------------------------------------------------------------
/jarvis/modules/templates/email_threat_image.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | No conversation was recorded, but attached is a photo of the intruder.
5 |
6 |
7 |
--------------------------------------------------------------------------------
/jarvis/modules/templates/email_threat_image_audio.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Conversation of Intruder:
5 | {{ CONVERTED }}
6 | Attached is a photo of the intruder.
7 |
8 |
9 |
--------------------------------------------------------------------------------
/jarvis/modules/templates/robinhood.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Robinhood Portfolio
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
43 |
44 |
45 | {{ TITLE }}
46 |
47 | {{ SUMMARY }}
48 |
49 |
50 | Profit
51 |
52 |
53 | {{ PROFIT }}
54 |
55 |
56 | Loss
57 |
58 |
59 | {{ LOSS }}
60 |
61 |
62 | Watchlist
63 |
64 |
65 | {{ WATCHLIST_UP }}
66 |
67 |
68 | {{ WATCHLIST_DOWN }}
69 |
70 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/jarvis/modules/templates/surveillance.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WebCam - LiveView
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
Live Streaming
21 |
22 |
23 |
24 |
25 |
38 |
64 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/jarvis/modules/templates/templates.py:
--------------------------------------------------------------------------------
1 | # noinspection PyUnresolvedReferences
2 | """Module to read all HTML templates and store as members of an object.
3 |
4 | >>> AudioHandler
5 |
6 | """
7 |
8 | import os
9 |
10 | from jarvis.modules.models import models
11 |
12 |
13 | class EmailTemplates:
14 | """HTML templates used to send outbound email.
15 |
16 | >>> EmailTemplates
17 |
18 | """
19 |
20 | if models.settings.invoker != "sphinx-build":
21 | with open(
22 | os.path.join(os.path.dirname(__file__), "email_threat_audio.html")
23 | ) as file:
24 | threat_audio = file.read()
25 |
26 | with open(
27 | os.path.join(os.path.dirname(__file__), "email_threat_image.html")
28 | ) as file:
29 | threat_image = file.read()
30 |
31 | with open(
32 | os.path.join(os.path.dirname(__file__), "email_threat_image_audio.html")
33 | ) as file:
34 | threat_image_audio = file.read()
35 |
36 | with open(
37 | os.path.join(os.path.dirname(__file__), "email_stock_alert.html")
38 | ) as file:
39 | stock_alert = file.read()
40 |
41 | with open(os.path.join(os.path.dirname(__file__), "email_OTP.html")) as file:
42 | one_time_passcode = file.read()
43 |
44 | with open(os.path.join(os.path.dirname(__file__), "email.html")) as file:
45 | notification = file.read()
46 |
47 | with open(os.path.join(os.path.dirname(__file__), "car_report.html")) as file:
48 | car_report = file.read()
49 |
50 |
51 | class EndpointTemplates:
52 | """HTML templates used for hosting endpoints.
53 |
54 | >>> EndpointTemplates
55 |
56 | """
57 |
58 | if models.settings.invoker != "sphinx-build":
59 | with open(os.path.join(os.path.dirname(__file__), "robinhood.html")) as file:
60 | robinhood = file.read()
61 |
62 | with open(os.path.join(os.path.dirname(__file__), "surveillance.html")) as file:
63 | surveillance = file.read()
64 |
65 |
66 | class GenericTemplates:
67 | """HTML templates used for generic purposes.
68 |
69 | >>> GenericTemplates
70 |
71 | """
72 |
73 | if models.settings.invoker != "sphinx-build":
74 | with open(
75 | os.path.join(os.path.dirname(__file__), "win_wifi_config.xml")
76 | ) as file:
77 | win_wifi_xml = file.read()
78 |
79 |
80 | class Llama:
81 | """Modelfile template for ollama SDK.
82 |
83 | >>> Llama
84 |
85 | """
86 |
87 | if models.settings.invoker != "sphinx-build":
88 | with open(os.path.join(os.path.dirname(__file__), "Modelfile")) as file:
89 | modelfile = file.read()
90 |
91 |
92 | email = EmailTemplates
93 | generic = GenericTemplates
94 | endpoint = EndpointTemplates
95 | llama = Llama
96 |
--------------------------------------------------------------------------------
/jarvis/modules/templates/win_wifi_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ WIFI_SSID }}
4 |
5 |
6 | {{ WIFI_SSID }}
7 |
8 |
9 | ESS
10 | auto
11 |
12 |
13 |
14 | WPA2PSK
15 | AES
16 | false
17 |
18 |
19 | passPhrase
20 | false
21 | {{ WIFI_PASSWORD }}
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/jarvis/modules/timeout/timeout.py:
--------------------------------------------------------------------------------
1 | # noinspection PyUnresolvedReferences
2 | """Module that can be used to set timeout for a function.
3 |
4 | >>> Timeout
5 |
6 | """
7 |
8 | import multiprocessing
9 | import time
10 | from logging import Logger
11 | from typing import Callable, Dict, List, Tuple
12 |
13 | from pydantic import PositiveFloat, PositiveInt
14 |
15 |
16 | def timeout(
17 | seconds: PositiveInt | PositiveFloat,
18 | function: Callable,
19 | args: List | Tuple = None,
20 | kwargs: Dict = None,
21 | logger: Logger = None,
22 | ) -> bool:
23 | """Run the given function and kill it if exceeds the set timeout.
24 |
25 | Args:
26 | seconds: Timeout after which the said function has to be terminated.
27 | function: Function to run and timeout.
28 | args: Args to be passed to the function.
29 | kwargs: Keyword args to be passed to the function.
30 | logger: Logger to optionally log the timeout events.
31 |
32 | Returns:
33 | bool:
34 | Boolean flag to indicate if the function completed within the set timeout.
35 | """
36 | process = multiprocessing.Process(
37 | target=function, args=args or [], kwargs=kwargs or {}
38 | )
39 | _start = time.time()
40 | if logger:
41 | logger.info(
42 | "Starting %s at %s with timeout: %s"
43 | % (
44 | function.__name__,
45 | time.strftime("%H:%M:%S", time.localtime(_start)),
46 | seconds,
47 | )
48 | )
49 | process.start()
50 | process.join(timeout=seconds)
51 | exec_time = round(float(time.time() - _start), 2)
52 | logger.info(
53 | "Joined process %d after %d seconds.", process.pid, exec_time
54 | ) if logger else None
55 | if process.is_alive():
56 | logger.warning(
57 | "Process %d is still alive. Terminating.", process.pid
58 | ) if logger else None
59 | process.terminate()
60 | process.join(timeout=1e-01)
61 | try:
62 | logger.info("Closing process: %d", process.pid) if logger else None
63 | # Close immediately instead of waiting to be garbage collected
64 | process.close()
65 | except ValueError as error:
66 | # Expected when join timeout is insufficient. The resources will be released eventually but not immediately.
67 | logger.error(error) if logger else None
68 | return False
69 | return True
70 |
--------------------------------------------------------------------------------
/jarvis/modules/utils/shared.py:
--------------------------------------------------------------------------------
1 | # noinspection PyUnresolvedReferences
2 | """This is a space for variables shared across multiple modules.
3 |
4 | >>> Shared
5 |
6 | """
7 |
8 | import time
9 |
10 | start_time = time.time()
11 | greeting = False
12 | called_by_offline = False
13 | called_by_bg_tasks = False
14 |
15 | text_spoken = None
16 | offline_caller = None
17 | tv = {}
18 |
19 | processes = {}
20 |
21 | called = {
22 | "report": False,
23 | "locate_places": False,
24 | "directions": False,
25 | }
26 |
--------------------------------------------------------------------------------
/jarvis/modules/wakeonlan/wakeonlan.py:
--------------------------------------------------------------------------------
1 | # noinspection PyUnresolvedReferences
2 | """Module for powering on supported devices.
3 |
4 | >>> WakeOnLan
5 |
6 | """
7 |
8 | import socket
9 |
10 | from jarvis.modules.exceptions import InvalidArgument
11 |
12 |
13 | class WakeOnLan:
14 | """Initiates WakeOnLan object to create and send bytes to turn on a device.
15 |
16 | >>> WakeOnLan
17 |
18 | """
19 |
20 | BROADCAST_IP = "255.255.255.255"
21 | DEFAULT_PORT = 9
22 |
23 | @classmethod
24 | def create_packet(cls, macaddress: str) -> bytes:
25 | """Create a magic packet.
26 |
27 | A magic packet is a packet that can be used with the for wake on lan
28 | protocol to wake up a computer. The packet is constructed from the
29 | mac address given as a parameter.
30 |
31 | Args:
32 | macaddress: the mac address that should be parsed into a magic packet.
33 |
34 | Raises:
35 | InvalidArgument:
36 | If the argument ``macaddress`` is invalid.
37 | """
38 | if len(macaddress) == 17:
39 | macaddress = macaddress.replace(macaddress[2], "")
40 | elif len(macaddress) != 12:
41 | raise InvalidArgument(f"invalid mac address: {macaddress}")
42 | return bytes.fromhex("F" * 12 + macaddress * 16)
43 |
44 | def send_packet(
45 | self,
46 | *mac_addresses: str,
47 | ip_address: str = BROADCAST_IP,
48 | port: int = DEFAULT_PORT,
49 | interface: str = None,
50 | ) -> None:
51 | """Wake up devices using mac addresses.
52 |
53 | Notes:
54 | Wake on lan must be enabled on the host device.
55 |
56 | Args:
57 | mac_addresses: One or more mac addresses of machines to wake.
58 | ip_address: IP address of the host to send the magic packet to.
59 | port: Port of the host to send the magic packet to.
60 | interface: IP address of the network adapter to route the packets through.
61 | """
62 | packets = [self.create_packet(mac) for mac in mac_addresses]
63 |
64 | with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
65 | if interface:
66 | sock.bind((interface, 0))
67 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
68 | sock.connect((ip_address, port))
69 | for packet in packets:
70 | sock.send(packet)
71 |
--------------------------------------------------------------------------------
/jarvis/scripts/applauncher.scpt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevickypedia/Jarvis/1298228d8ed6471f12b7848f0620abea15dbc0a1/jarvis/scripts/applauncher.scpt
--------------------------------------------------------------------------------
/jarvis/scripts/calendar.scpt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevickypedia/Jarvis/1298228d8ed6471f12b7848f0620abea15dbc0a1/jarvis/scripts/calendar.scpt
--------------------------------------------------------------------------------
/jarvis/scripts/outlook.scpt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevickypedia/Jarvis/1298228d8ed6471f12b7848f0620abea15dbc0a1/jarvis/scripts/outlook.scpt
--------------------------------------------------------------------------------
/pre_commit.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # 'set -e' stops the execution of a script if a command or pipeline has an error.
3 | # This is the opposite of the default shell behaviour, which is to ignore errors in scripts.
4 | set -e
5 |
6 | export PROCESS_NAME="pre_commit"
7 |
8 | clean_docs() {
9 | # Clean up docs directory keeping the CNAME file if present
10 | directory="docs"
11 | file_to_keep="CNAME"
12 | if [ -e "${directory}/${file_to_keep}" ]; then
13 | find "${directory}" -mindepth 1 ! -name "${file_to_keep}" -exec rm -rf {} +
14 | else
15 | find "${directory}" -mindepth 1 -exec rm -rf {} +
16 | fi
17 | }
18 |
19 | update_release_notes() {
20 | # Update release notes
21 | gitverse-release reverse -f release_notes.rst -t 'Release Notes'
22 | }
23 |
24 | gen_docs() {
25 | # Generate sphinx docs
26 | mkdir -p docs_gen/_static # Create a _static directory if unavailable
27 | cp README.md docs_gen # Copy readme file to docs_gen
28 | cd docs_gen && make clean html # cd into doc_gen and create the runbook
29 | mv _build/html/* ../docs && mv README.md ../docs && rm -rf logs # Move the runbook, readme and cleanup
30 | cp static.css ../docs/_static
31 | }
32 |
33 | run_pytest() {
34 | # Run pytest
35 | export PYTHONWARNINGS="ignore::DeprecationWarning"
36 | python -m pytest
37 | }
38 |
39 | gen_docs &
40 | clean_docs &
41 | update_release_notes &
42 | run_pytest &
43 |
44 | wait
45 |
46 | # The existence of this file tells GitHub Pages not to run the published files through Jekyll.
47 | # This is important since Jekyll will discard any files that begin with _
48 | touch docs/.nojekyll
49 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "jarvis-ironman"
3 | dynamic = ["version"]
4 | description = "Voice-Activated Natural Language UI"
5 | readme = "README.md"
6 | authors = [{ name = "Vignesh Rao", email = "svignesh1793@gmail.com" }]
7 | license = { file = "LICENSE" }
8 | classifiers = [
9 | "Development Status :: 5 - Production/Stable",
10 | "Intended Audience :: Information Technology",
11 | "Operating System :: MacOS :: MacOS X",
12 | "Operating System :: Microsoft :: Windows :: Windows 10",
13 | "Operating System :: POSIX :: Linux",
14 | "License :: OSI Approved :: MIT License",
15 | "Programming Language :: Python :: 3.10",
16 | "Programming Language :: Python :: 3.11",
17 | "Topic :: Multimedia :: Sound/Audio :: Speech",
18 | "Topic :: Scientific/Engineering :: Human Machine Interfaces",
19 | "Topic :: Scientific/Engineering :: Artificial Intelligence",
20 | "Topic :: Home Automation",
21 | "Topic :: Scientific/Engineering :: Image Recognition",
22 | "Topic :: System :: Hardware :: Universal Serial Bus (USB) :: Wireless Controller",
23 | "Topic :: Multimedia :: Sound/Audio :: Conversion",
24 | "Topic :: Software Development :: Libraries :: Python Modules",
25 | "Topic :: System :: Hardware :: Hardware Drivers",
26 | "Topic :: System :: Hardware :: Symmetric Multi-processing",
27 | "Framework :: AnyIO",
28 | "Framework :: FastAPI",
29 | "Framework :: AsyncIO",
30 | "Framework :: Pydantic :: 2",
31 | "Framework :: Setuptools Plugin",
32 | "Framework :: aiohttp",
33 | "Natural Language :: English"
34 | ]
35 | keywords = ["python", "home-automation", "natural-language-processing", "text-to-speech",
36 | "speech-recognition", "jarvis", "hotword-detection", "virtual-assistant"]
37 | requires-python = ">=3.10,<3.12" # Only 3.10 and 3.11 are supported
38 |
39 | [tool.setuptools]
40 | # packages section includes the directories with ONLY '.py' modules
41 | packages = ["jarvis", "jarvis._preexec", "jarvis.executors"]
42 | script-files = [
43 | "jarvis/lib/install_darwin.sh",
44 | "jarvis/lib/install_linux.sh",
45 | "jarvis/lib/install_windows.sh",
46 | ]
47 |
48 | [tool.setuptools.dynamic]
49 | version = {attr = "jarvis.version"}
50 |
51 | [project.scripts]
52 | # sends all the args to commandline function, where the arbitary commands as processed accordingly
53 | jarvis = "jarvis:commandline"
54 |
55 | [build-system]
56 | requires = ["setuptools", "wheel"]
57 | build-backend = "setuptools.build_meta"
58 |
59 | [project.optional-dependencies]
60 | dev = ["sphinx==5.1.1", "pre-commit", "recommonmark", "pytest", "gitverse"]
61 |
62 | [project.urls]
63 | API = "https://jarvis.vigneshrao.com"
64 | Health = "https://jarvis-health.vigneshrao.com"
65 | Homepage = "https://github.com/thevickypedia/Jarvis"
66 | Docs = "https://jarvis-docs.vigneshrao.com"
67 | Demo = "https://vigneshrao.com/Jarvis/Jarvis_Demo.mp4"
68 | Source = "https://github.com/thevickypedia/Jarvis"
69 | "Bug Tracker" = "https://github.com/thevickypedia/Jarvis/issues"
70 | "Release Notes" = "https://github.com/thevickypedia/Jarvis/blob/master/release_notes.rst"
71 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thevickypedia/Jarvis/1298228d8ed6471f12b7848f0620abea15dbc0a1/tests/__init__.py
--------------------------------------------------------------------------------
/tests/api_test.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 |
4 | @pytest.fixture(scope="session")
5 | def server():
6 | """Run pytest on APIServer which runs in a thread."""
7 | server = ...
8 | with server.run_in_parallel():
9 | yield
10 |
--------------------------------------------------------------------------------
/tests/constant_test.py:
--------------------------------------------------------------------------------
1 | SAMPLE_PHRASE = "Welcome to the world of Natural Language Processing."
2 |
--------------------------------------------------------------------------------
/tests/listener_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from unittest.mock import MagicMock, patch
3 |
4 | from jarvis.main import listener
5 | from tests.constant_test import SAMPLE_PHRASE
6 |
7 |
8 | class TestListener(unittest.TestCase):
9 | """TestCase object for testing listener module.
10 |
11 | >>> TestListener
12 |
13 | """
14 |
15 | # noinspection PyUnusedLocal
16 | @patch("jarvis.modules.audio.listener.microphone")
17 | @patch("jarvis.modules.audio.listener.recognizer")
18 | @patch("jarvis.modules.audio.listener.playsound")
19 | @patch("jarvis.modules.audio.listener.support")
20 | def test_listen(
21 | self,
22 | mock_support: MagicMock,
23 | mock_playsound: MagicMock,
24 | mock_recognizer: MagicMock,
25 | mock_microphone: MagicMock,
26 | ):
27 | """Test the listen function.
28 |
29 | Mock the return values and set up necessary mocks to simulate the behavior of the listen function.
30 | Ensure that the listen function is called with the correct arguments.
31 | Ensure that the playsound function is not called when sound=False is passed.
32 |
33 | Args:
34 | mock_support: Mocked support module.
35 | mock_playsound: Mocked playsound function.
36 | mock_recognizer: Mocked recognizer module.
37 | mock_microphone: Mocked microphone module.
38 | """
39 | # Mock the return values and setup necessary mocks
40 | mock_listened = MagicMock()
41 | mock_recognizer.listen.return_value = mock_listened
42 | mock_recognizer.recognize_google.return_value = (
43 | SAMPLE_PHRASE,
44 | "some_confidence",
45 | )
46 |
47 | result = listener.listen(
48 | sound=False, timeout=5, phrase_time_limit=10, no_conf=True
49 | )
50 |
51 | # Assertions
52 | self.assertEqual(result, SAMPLE_PHRASE)
53 | mock_recognizer.listen.assert_called_once_with(
54 | source=mock_microphone.__enter__(), timeout=5, phrase_time_limit=10
55 | )
56 | mock_recognizer.recognize_google.assert_called_once_with(
57 | audio_data=mock_listened, with_confidence=True
58 | )
59 |
60 | # Check that playsound function was not called
61 | mock_playsound.assert_not_called()
62 |
--------------------------------------------------------------------------------
/tests/main_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from unittest.mock import MagicMock, patch
3 |
4 | import pyaudio
5 |
6 | from jarvis.main import Activator, constructor, models
7 | from tests.constant_test import SAMPLE_PHRASE
8 |
9 |
10 | class TestActivator(unittest.TestCase):
11 | """Test cases for the Activator class."""
12 |
13 | def setUp(self):
14 | """Set up the Activator instance for testing."""
15 | self.activator = Activator()
16 |
17 | @patch("pvporcupine.create")
18 | @patch("jarvis.main.audio_engine.open")
19 | def test_init_activator(
20 | self, mock_audio_open: MagicMock, mock_pvporcupine_create: MagicMock
21 | ) -> None:
22 | """Test whether the Activator is initialized correctly.
23 |
24 | Mock the return values of the create function.
25 |
26 | Args:
27 | mock_audio_open: Patched audio_engine.open from jarvis.main.py.
28 | mock_pvporcupine_create: Patched pvporcupine.create from jarvis.main.py.
29 | """
30 | mock_pvporcupine_create.return_value = MagicMock()
31 | mock_audio_open.return_value = MagicMock()
32 | # Call the __init__() method explicitly
33 | self.activator.__init__()
34 |
35 | # Assertions
36 | mock_pvporcupine_create.assert_called_once_with(**constructor())
37 | mock_audio_open.assert_called_once_with(
38 | rate=mock_pvporcupine_create.return_value.sample_rate,
39 | channels=1,
40 | format=pyaudio.paInt16,
41 | input=True,
42 | frames_per_buffer=mock_pvporcupine_create.return_value.frame_length,
43 | input_device_index=models.env.microphone_index,
44 | )
45 | self.assertEqual(self.activator.detector, mock_pvporcupine_create.return_value)
46 | self.assertEqual(self.activator.audio_stream, mock_audio_open.return_value)
47 |
48 | # Patch the listener.listen from jarvis.modules.audio
49 | @patch("jarvis.modules.audio.listener.listen")
50 | # Patch the commander.initiator from jarvis.executors
51 | @patch("jarvis.executors.commander.initiator")
52 | # Patch the speaker.speak from jarvis.modules.audio
53 | @patch("jarvis.modules.audio.speaker.speak")
54 | # Patch the audio_engine.close from jarvis.main
55 | @patch("jarvis.main.audio_engine.close")
56 | def test_executor(
57 | self,
58 | mock_audio_close: MagicMock,
59 | mock_speak: MagicMock,
60 | mock_initiator: MagicMock,
61 | mock_listen: MagicMock,
62 | ) -> None:
63 | """Test the executor method of Activator.
64 |
65 | Mock return values of the listen function and set up necessary mocks.
66 |
67 | Args:
68 | mock_audio_close: Patched audio_engine.close from jarvis.main.py.
69 | mock_speak: Patched ``speaker.speak`` from jarvis.modules.audio.
70 | mock_initiator: Patched ``commander.initiator`` from jarvis.executors.
71 | mock_listen: Patched ``listener.listen`` from jarvis.modules.audio.
72 | """
73 | mock_listen.return_value = SAMPLE_PHRASE
74 | mock_initiator.return_value = None # Not testing the behavior of initiator here
75 |
76 | self.activator.executor()
77 |
78 | # Assertions
79 | # audio_engine.close should be called
80 | self.assertTrue(mock_audio_close.called)
81 | # listener.listen should be called
82 | mock_listen.assert_called_once_with(sound=False, no_conf=True)
83 | # commander.initiator should be called with the correct phrase
84 | mock_initiator.assert_called_once_with(phrase=SAMPLE_PHRASE)
85 | # speaker.speak should be called with run=True
86 | mock_speak.assert_called_once_with(run=True)
87 |
88 |
89 | if __name__ == "__main__":
90 | unittest.main()
91 |
--------------------------------------------------------------------------------
/tests/speaker_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from unittest.mock import MagicMock, patch
3 |
4 | from jarvis.modules.audio import speaker
5 | from jarvis.modules.models import models
6 | from jarvis.modules.utils import shared
7 | from tests.constant_test import SAMPLE_PHRASE
8 |
9 |
10 | class TestSpeak(unittest.TestCase):
11 | """TestCase object for testing the speaker module.
12 |
13 | >>> TestSpeak
14 |
15 | """
16 |
17 | @patch("jarvis.modules.audio.speaker.speech_synthesizer", return_value=False)
18 | @patch("playsound.playsound")
19 | def test_speech_synthesis_usage(
20 | self, mock_playsound: MagicMock, mock_speech_synthesizer: MagicMock
21 | ) -> None:
22 | """Test speech synthesis usage.
23 |
24 | Args:
25 | mock_playsound: Mock object for playsound module.
26 | mock_speech_synthesizer: Mock object for speaker.speech_synthesizer function.
27 | """
28 | models.env.speech_synthesis_timeout = 10
29 | speaker.speak(text=SAMPLE_PHRASE, run=False, block=True)
30 | mock_speech_synthesizer.assert_called_once_with(text=SAMPLE_PHRASE)
31 | mock_playsound.assert_not_called()
32 |
33 | @patch("playsound.playsound")
34 | @patch("jarvis.modules.audio.speaker.speak", return_value=False)
35 | @patch("jarvis.modules.audio.speaker.speech_synthesizer", return_value=False)
36 | def test_audio_driver_usage(
37 | self,
38 | mock_playsound: MagicMock,
39 | mock_speaker: MagicMock,
40 | mock_speech_synthesizer: MagicMock,
41 | ) -> None:
42 | """Test audio driver usage.
43 |
44 | Args:
45 | mock_playsound: Mock object for playsound module.
46 | mock_speaker: Mock object for ``speaker.speak`` function.
47 | mock_speech_synthesizer: Mock object for speaker.speech_synthesizer function.
48 | """
49 | speaker.speak(text=SAMPLE_PHRASE, run=True)
50 | mock_speaker.assert_called_once_with(text=SAMPLE_PHRASE, run=True)
51 | mock_playsound.assert_not_called()
52 | mock_speech_synthesizer.assert_not_called()
53 |
54 | @patch("jarvis.modules.utils.support.write_screen")
55 | def test_no_text_input(self, mock_write_screen: MagicMock) -> None:
56 | """Test speak function with no text input.
57 |
58 | Args:
59 | mock_write_screen: Mock object for support.write_screen function.
60 | """
61 | speaker.speak(text=None, run=False, block=True)
62 | mock_write_screen.assert_not_called()
63 |
64 | @patch("jarvis.modules.utils.support.write_screen")
65 | def test_text_input_and_run(self, mock_write_screen: MagicMock) -> None:
66 | """Test speak function with text input and run flag.
67 |
68 | Args:
69 | mock_write_screen: Mock object for support.write_screen function.
70 | """
71 | speaker.speak(text=SAMPLE_PHRASE, run=True, block=True)
72 | mock_write_screen.assert_called_once_with(text=SAMPLE_PHRASE)
73 |
74 | @patch("jarvis.modules.audio.speaker.speech_synthesizer", return_value=False)
75 | @patch("playsound.playsound")
76 | def test_offline_mode(
77 | self, mock_playsound: MagicMock, mock_speech_synthesizer: MagicMock
78 | ) -> None:
79 | """Test speak function in offline mode.
80 |
81 | Args:
82 | mock_playsound: Mock object for playsound module.
83 | mock_speech_synthesizer: Mock object for speaker.speech_synthesizer function.
84 | """
85 | shared.called_by_offline = True
86 | speaker.speak(text=SAMPLE_PHRASE)
87 | mock_speech_synthesizer.assert_not_called()
88 | mock_playsound.assert_not_called()
89 | shared.called_by_offline = False
90 |
91 |
92 | if __name__ == "__main__":
93 | unittest.main()
94 |
--------------------------------------------------------------------------------
/tests/speech_synthesis_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from unittest.mock import MagicMock, patch
3 |
4 | from jarvis.modules.audio import speaker
5 | from jarvis.modules.exceptions import EgressErrors
6 | from tests.constant_test import SAMPLE_PHRASE
7 |
8 |
9 | # noinspection PyUnusedLocal
10 | class TestSpeechSynthesizer(unittest.TestCase):
11 | """TestSpeechSynthesizer object for testing speech synthesis module.
12 |
13 | >>> TestSpeechSynthesizer
14 |
15 | """
16 |
17 | @patch("requests.post")
18 | def test_successful_synthesis(self, mock_post: MagicMock) -> None:
19 | """Test successful speech synthesis.
20 |
21 | This method tests the behavior of the speech_synthesizer function when a successful
22 | response is mocked from the post request call.
23 |
24 | Args:
25 | mock_post: Mock of the ``requests.post`` function.
26 | """
27 | mock_response = MagicMock()
28 | mock_response.ok = True
29 | mock_response.content = SAMPLE_PHRASE.encode(encoding="UTF-8")
30 | mock_post.return_value = mock_response
31 |
32 | result = speaker.speech_synthesizer(SAMPLE_PHRASE)
33 |
34 | self.assertTrue(result)
35 |
36 | @patch("requests.post")
37 | def test_unsuccessful_synthesis(self, mock_post: MagicMock) -> None:
38 | """Test unsuccessful speech synthesis.
39 |
40 | This method tests the behavior of the speech_synthesizer function when an unsuccessful
41 | response is mocked from the post request call.
42 |
43 | Args:
44 | mock_post: Mock of the ``requests.post`` function.
45 | """
46 | mock_response = MagicMock()
47 | mock_response.ok = False
48 | mock_response.status_code = 500
49 | mock_post.return_value = mock_response
50 |
51 | result = speaker.speech_synthesizer(SAMPLE_PHRASE)
52 |
53 | self.assertFalse(result)
54 |
55 | @patch("requests.post", side_effect=UnicodeError("Test UnicodeError"))
56 | def test_unicode_error_handling(self, mock_post: MagicMock) -> None:
57 | """Test UnicodeError handling in speech synthesis.
58 |
59 | This method tests the handling of UnicodeError within the speech_synthesizer function.
60 |
61 | Args:
62 | mock_post: Mock of the ``requests.post`` function with side effect.
63 | """
64 | result = speaker.speech_synthesizer(SAMPLE_PHRASE)
65 |
66 | self.assertFalse(result)
67 |
68 | @patch("requests.post", side_effect=EgressErrors)
69 | def test_egress_error_handling(self, mock_post: MagicMock) -> None:
70 | """Test EgressErrors handling in speech synthesis.
71 |
72 | This method tests the handling of EgressErrors within the speech_synthesizer function.
73 |
74 | Args:
75 | mock_post: Mock of the ``requests.post`` function with side effect.
76 | """
77 | result = speaker.speech_synthesizer(SAMPLE_PHRASE)
78 |
79 | self.assertFalse(result)
80 |
81 |
82 | if __name__ == "__main__":
83 | unittest.main()
84 |
--------------------------------------------------------------------------------
/update-toml.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import toml
4 |
5 | FILE_PATH = os.environ.get("FILE_PATH", "pyproject.toml")
6 | PROJECT_NAME = os.environ.get("PROJECT_NAME", "jarvis-ironman")
7 |
8 |
9 | def update_name_in_pyproject() -> None:
10 | """Update project name handler in metadata."""
11 | with open(FILE_PATH) as file:
12 | data = toml.load(file)
13 |
14 | # Update the 'name' in the '[project]' section
15 | data["project"]["name"] = PROJECT_NAME
16 |
17 | # Write the updated content back to the TOML file
18 | with open(FILE_PATH, "w") as file:
19 | toml.dump(data, file)
20 | file.flush()
21 |
22 | print(f"Updated 'name' to {PROJECT_NAME!r} in [project]")
23 |
24 |
25 | if __name__ == "__main__":
26 | update_name_in_pyproject()
27 |
--------------------------------------------------------------------------------