Commit 097b6972 authored by Roberto Leinardi's avatar Roberto Leinardi

Initial commit

parents
build/
dist/
*.egg-info/
*.egg
*.py[cod]
__pycache__/
*.so
*~
/.idea
*.autosave
/.mypy_cache/
.dmypy.json
/venv/
/gwe/data/gwe.db
**[0.1.0] 2018-12-31**
- Initial release
\ No newline at end of file
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
education, socio-economic status, nationality, personal appearance, race,
religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at roberto@leinardi.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
# Contributing
Considering that this project is actively maintained, contributions of all types are welcome.
## Opening issues
Open a new issue when:
- you notice an unwanted behavior
- you want a new feature implemented
- you have just some doubts
To open a new issue, please use the provided issue template and fill it out as much as possible.
If you are interested to an existing issue, feel free to comment the issue or subscribe to it.
## Submitting pull requests
If you want to fix a bug or implement a new feature, feel free to submit a new pull request.
To submit a pull request, you have to fork this repository and fill the PR template.
When you want to submit a pull request, remember to:
- follow this project's code style
- check for PyLint and Mypy errors
This diff is collapsed.
# Include the README
include README.md
# Include the license file
include COPYING.txt
\ No newline at end of file
# gwe
TBD
## Distribution dependencies
### (K/X)Ubuntu 18.04 or newer
```bash
sudo apt install gir1.2-gtksource-3.0 gir1.2-appindicator3-0.1 python3-gi-cairo python3-pip
```
### Fedora 28+
Install [(K)StatusNotifierItem/AppIndicator Support](https://extensions.gnome.org/extension/615/appindicator-support/)
### Arch Linux (Gnome)
```bash
sudo pacman -Syu python-pip libappindicator-gtk3
```
## Install using PIP
```bash
pip3 install gwe
```
Add the the executable path `~/.local/bin` to your PATH variable if missing.
## Update using PIP
```bash
pip3 install -U gwe
```
## Running the app
To start the app you have to run the command `gwe` in a terminal.
### Application entry
To add a desktop entry for the application run the following command:
```bash
gwe --application-entry
```
If you don't want to create this custom rule you can run gwe as root
(using sudo) but we advise against this solution.
## Command line options
| Parameter | Description|
|---------------------------|------------|
|-v, --version |Show the app version|
|--debug |Show debug messages|
|--hide-window |Start with the main window hidden|
|--application-entry |Add a desktop entry for the application|
|--autostart-on |Enable automatic start of the app on login|
|--autostart-off |Disable automatic start of the app on login|
## Python dependencies
## How to run the repository sources
```
sudo apt install python3-pip
git clone https://gitlab.com/leinardi/gwe.git
pip3 install injector
pip3 install matplotlib
pip3 install peewee
pip3 install pygobject
pip3 install pyxdg
pip3 install requests
pip3 install rx
ca gwe
./run
```
## How can I support this project?
The best way to support this plugin is to star it on both [GitLab](https://gitlab.com/leinardi/gwe) and [GitHub](https://github.com/leinardi/gwe).
Feedback is always welcome: if you found a bug or would like to suggest a feature,
feel free to open an issue on the [issue tracker](https://gitlab.com/leinardi/gwe/issues).
## Lincense
```
This file is part of gwe.
Copyright (c) 2018 Roberto Leinardi
gwe is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
gwe is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with gwe. If not, see <http://www.gnu.org/licenses/>.
```
\ No newline at end of file
# Releasing
1. Bump the `APP_VERSION` property in `gwe/conf.py` based on Major.Minor.Patch naming scheme
2. Update `CHANGELOG.md` for the impending release.
3. Update the `README.md` with the new changes (if necessary).
4. `python3 setup.py sdist bdist_wheel`
5. `git commit -am "Prepare for release X.Y.Z" && git push"` (where X.Y.Z is the version you set in step 1)
6. Create a new release on Github
1. Tag version `X.Y.Z` (`git tag -s X.Y.Z && git push --tags`)
2. Release title `X.Y.Z`
3. Paste the content from `CHANGELOG.md` as the description
4. Upload the `dist/gwe-X.X.X.tar.gz`
7. Create a PR from [master](../../tree/master) to [release](../../tree/release)
8. `twine upload dist/*`
# This file is part of gwe.
#
# Copyright (c) 2018 Roberto Leinardi
#
# gwe is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# gwe is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with gwe. If not, see <http://www.gnu.org/licenses/>.
# This file is part of gwe.
#
# Copyright (c) 2018 Roberto Leinardi
#
# gwe is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# gwe is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with gwe. If not, see <http://www.gnu.org/licenses/>.
import logging
import sys
from enum import Enum
from gettext import gettext as _
from typing import Any, Optional, List
from gi.repository import Gtk, Gio, GLib
from injector import inject
from peewee import SqliteDatabase
from gwe.conf import APP_NAME, APP_ID, APP_VERSION
from gwe.di import MainBuilder
from gwe.model import SpeedProfile, SpeedStep, Setting, CurrentSpeedProfile, load_db_default_data
from gwe.presenter.main import MainPresenter
from gwe.util.desktop_entry import set_autostart_entry, add_application_entry
from gwe.util.log import LOG_DEBUG_FORMAT
from gwe.util.udev import add_udev_rule, remove_udev_rule
from gwe.util.view import build_glib_option
from gwe.view.main import MainView
LOG = logging.getLogger(__name__)
class Application(Gtk.Application):
@inject
def __init__(self,
database: SqliteDatabase,
view: MainView,
presenter: MainPresenter,
builder: MainBuilder,
*args: Any,
**kwargs: Any) -> None:
LOG.debug("init Application")
GLib.set_application_name(_(APP_NAME))
super().__init__(*args, application_id=APP_ID,
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE,
**kwargs)
database.connect()
database.create_tables([SpeedProfile, SpeedStep, CurrentSpeedProfile, Setting])
if SpeedProfile.select().count() == 0:
load_db_default_data()
self.add_main_option_entries(self._get_main_option_entries())
self._view = view
self._presenter = presenter
self._presenter.application_quit = self.quit
self._window: Optional[Gtk.ApplicationWindow] = None
self._builder: Gtk.Builder = builder
self._start_hidden: bool = False
def do_activate(self) -> None:
if not self._window:
self._builder.connect_signals(self._presenter)
self._window: Gtk.ApplicationWindow = self._builder.get_object("application_window")
self._window.set_application(self)
self._window.show_all()
self._view.show()
self._window.present()
if self._start_hidden:
self._window.hide()
self._start_hidden = False
def do_startup(self) -> None:
Gtk.Application.do_startup(self)
def do_command_line(self, command_line: Gio.ApplicationCommandLine) -> int:
start_app = True
options = command_line.get_options_dict()
# convert GVariantDict -> GVariant -> dict
options = options.end().unpack()
exit_value = 0
if _Options.VERSION.value in options:
LOG.debug("Option %s selected", _Options.VERSION.value)
print(APP_VERSION)
start_app = False
if _Options.DEBUG.value in options:
logging.getLogger().setLevel(logging.DEBUG)
for handler in logging.getLogger().handlers:
handler.formatter = logging.Formatter(LOG_DEBUG_FORMAT)
LOG.debug("Option %s selected", _Options.DEBUG.value)
if _Options.HIDE_WINDOW.value in options:
LOG.debug("Option %s selected", _Options.HIDE_WINDOW.value)
self._start_hidden = True
if _Options.APPLICATION_ENTRY.value in options:
LOG.debug("Option %s selected", _Options.APPLICATION_ENTRY.value)
add_application_entry()
start_app = False
if _Options.AUTOSTART_ON.value in options:
LOG.debug("Option %s selected", _Options.AUTOSTART_ON.value)
set_autostart_entry(True)
start_app = False
if _Options.AUTOSTART_OFF.value in options:
LOG.debug("Option %s selected", _Options.AUTOSTART_OFF.value)
set_autostart_entry(True)
start_app = False
if start_app:
self.activate()
return exit_value
@staticmethod
def _get_main_option_entries() -> List[GLib.OptionEntry]:
options = [
build_glib_option(_Options.VERSION.value,
short_name='v',
description="Show the app version"),
build_glib_option(_Options.DEBUG.value,
description="Show debug messages"),
build_glib_option(_Options.HIDE_WINDOW.value,
description="Start with the main window hidden"),
]
linux_options = [
build_glib_option(_Options.APPLICATION_ENTRY.value,
description="Add a desktop entry for the application"),
build_glib_option(_Options.AUTOSTART_ON.value,
description="Enable automatic start of the app on login"),
build_glib_option(_Options.AUTOSTART_OFF.value,
description="Disable automatic start of the app on login"),
]
if sys.platform.startswith('linux'):
options += linux_options
return options
class _Options(Enum):
VERSION = 'version'
DEBUG = 'debug'
HIDE_WINDOW = 'hide-window'
APPLICATION_ENTRY = 'application-entry'
AUTOSTART_ON = 'autostart-on'
AUTOSTART_OFF = 'autostart-off'
# This file is part of gwe.
#
# Copyright (c) 2018 Roberto Leinardi
#
# gwe is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# gwe is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with gwe. If not, see <http://www.gnu.org/licenses/>.
from typing import Dict, Any
APP_PACKAGE_NAME = "gwe"
APP_NAME = "gwe"
APP_ID = "com.leinardi.gwe"
APP_VERSION = "0.1.0"
APP_ICON_NAME = APP_PACKAGE_NAME + ".svg"
APP_DB_NAME = APP_PACKAGE_NAME + ".db"
APP_MAIN_UI_NAME = "main.glade"
APP_EDIT_SPEED_PROFILE_UI_NAME = "edit_speed_profile.glade"
APP_PREFERENCES_UI_NAME = "preferences.glade"
APP_DESKTOP_ENTRY_NAME = APP_PACKAGE_NAME + ".desktop"
APP_DESCRIPTION = 'GUI to control cooling and overclock of nVidia cards'
APP_SOURCE_URL = 'https://gitlab.com/leinardi/gwe'
APP_AUTHOR = 'Roberto Leinardi'
APP_AUTHOR_EMAIL = 'roberto@leinardi.com'
MIN_TEMP = 20
MAX_TEMP = 90
FAN_MIN_DUTY = 25
FAN_MAX_DUTY = 100
SETTINGS_DEFAULTS: Dict[str, Any] = {
'settings_launch_on_login': False,
'settings_load_last_profile': True,
'settings_refresh_interval': 3,
'settings_show_app_indicator': True,
'settings_app_indicator_show_gpu_temp': True,
}
DESKTOP_ENTRY: Dict[str, str] = {
'Type': 'Application',
'Encoding': 'UTF-8',
'Name': APP_NAME,
'Comment': APP_DESCRIPTION,
'Terminal': 'false',
'Categories': 'System;Settings;',
}
This diff is collapsed.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="16"
width="16"
version="1.1"
id="svg4"
sodipodi:docname="gwe-symbolic.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="3373"
inkscape:window-height="1376"
id="namedview6"
showgrid="false"
inkscape:zoom="14.75"
inkscape:cx="-29.21376"
inkscape:cy="3.357314"
inkscape:window-x="67"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<g
id="g826">
<path
id="path836"
d="M 8 1 A 6.9999995 6.999999 3.5083546e-15 0 0 4.0195312 2.2519531 L 3.2265625 1.4589844 C 2.7374145 0.9697119 1.9500643 0.96969078 1.4609375 1.4589844 C 0.97181068 1.9482779 0.97178954 2.7353369 1.4609375 3.2246094 L 2.2480469 4.0117188 A 6.9999995 6.999999 3.5083546e-15 0 0 1 8 A 6.9999995 6.999999 3.5083546e-15 0 0 2.2519531 11.980469 L 1.4589844 12.773438 C 0.9697119 13.262584 0.96969078 14.049937 1.4589844 14.539062 C 1.9482779 15.028189 2.7353369 15.028211 3.2246094 14.539062 L 4.0117188 13.751953 A 6.9999995 6.999999 3.5083546e-15 0 0 8 15 A 6.9999995 6.999999 3.5083546e-15 0 0 11.980469 13.748047 L 12.773438 14.541016 C 13.262584 15.030288 14.049937 15.030309 14.539062 14.541016 C 15.028189 14.051722 15.028211 13.264663 14.539062 12.775391 L 13.751953 11.988281 A 6.9999995 6.999999 3.5083546e-15 0 0 15 8 A 6.9999995 6.999999 3.5083546e-15 0 0 13.748047 4.0195312 L 14.541016 3.2265625 C 15.030288 2.7374145 15.030309 1.9500643 14.541016 1.4609375 C 14.051722 0.97181068 13.264663 0.97178954 12.775391 1.4609375 L 11.988281 2.2480469 A 6.9999995 6.999999 3.5083546e-15 0 0 8 1 z M 8 2.5 A 5.4999995 5.4999995 0 0 1 13.5 8 A 5.4999995 5.4999995 0 0 1 8 13.5 A 5.4999995 5.4999995 0 0 1 2.5 8 A 5.4999995 5.4999995 0 0 1 8 2.5 z "
style="opacity:1;fill:#808080;fill-opacity:1;stroke:#1ad21a;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle
r="3.9999995"
cy="8"
cx="8"
id="path822"
style="opacity:1;fill:#808080;fill-opacity:1;stroke:#1ad21a;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</svg>
This diff is collapsed.
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
# This file is part of gwe.
#
# Copyright (c) 2018 Roberto Leinardi
#
# gwe is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# gwe is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with gwe. If not, see <http://www.gnu.org/licenses/>.
import logging
from gi.repository import Gtk
from injector import Module, provider, singleton, Injector, Key
from peewee import SqliteDatabase
from rx.disposables import CompositeDisposable
from rx.subjects import Subject
from gwe.conf import APP_PACKAGE_NAME, APP_MAIN_UI_NAME, APP_DB_NAME, APP_EDIT_SPEED_PROFILE_UI_NAME, \
APP_PREFERENCES_UI_NAME
from gwe.util.path import get_data_path, get_config_path
LOG = logging.getLogger(__name__)
SpeedProfileChangedSubject = Key("SpeedProfileChangedSubject")
SpeedStepChangedSubject = Key("SpeedStepChangedSubject")
MainBuilder = Key(APP_MAIN_UI_NAME)
EditSpeedProfileBuilder = Key(APP_EDIT_SPEED_PROFILE_UI_NAME)
PreferencesBuilder = Key(APP_PREFERENCES_UI_NAME)
# pylint: disable=no-self-use
class ProviderModule(Module):
@singleton
@provider
def provide_main_builder(self) -> MainBuilder:
LOG.debug("provide Gtk.Builder")
builder = Gtk.Builder()
builder.set_translation_domain(APP_PACKAGE_NAME)
builder.add_from_file(get_data_path(APP_MAIN_UI_NAME))
return builder
@singleton
@provider
def provide_edit_speed_profile_builder(self) -> EditSpeedProfileBuilder:
LOG.debug("provide Gtk.Builder")
builder = Gtk.Builder()
builder.set_translation_domain(APP_PACKAGE_NAME)
builder.add_from_file(get_data_path(APP_EDIT_SPEED_PROFILE_UI_NAME))
return builder
@singleton
@provider
def provide_preferences_builder(self) -> PreferencesBuilder:
LOG.debug("provide Gtk.Builder")
builder = Gtk.Builder()
builder.set_translation_domain(APP_PACKAGE_NAME)
builder.add_from_file(get_data_path(APP_PREFERENCES_UI_NAME))
return builder
@singleton
@provider
def provide_thread_pool_scheduler(self) -> CompositeDisposable:
LOG.debug("provide CompositeDisposable")
return CompositeDisposable()
@singleton
@provider
def provide_database(self) -> SqliteDatabase:
LOG.debug("provide CompositeDisposable")
return SqliteDatabase(get_config_path(APP_DB_NAME))
@singleton
@provider
def provide_speed_profile_changed_subject(self) -> SpeedProfileChangedSubject:
return Subject()
@singleton
@provider
def provide_speed_step_changed_subject(self) -> SpeedStepChangedSubject:
return Subject()
INJECTOR = Injector(ProviderModule)
# This file is part of gwe.
#
# Copyright (c) 2018 Roberto Leinardi
#
# gwe is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# gwe is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with gwe. If not, see <http://www.gnu.org/licenses/>.
import json
import logging
from distutils.version import LooseVersion
from typing import List, Tuple, Optional
import requests
from injector import singleton, inject
from rx import Observable
from gwe.conf import SETTINGS_DEFAULTS, APP_PACKAGE_NAME, APP_VERSION
from gwe.model import Setting
from gwe.repository import NvidiaRepository
LOG = logging.getLogger(__name__)
@singleton
class GetStatusInteractor:
@inject
def __init__(self,
nvidia_repository: NvidiaRepository,
) -> None:
self._nvidia_repository = nvidia_repository
def execute(self) -> Observable:
LOG.debug("GetStatusInteractor.execute()")
return Observable.defer(lambda: Observable.just(self._nvidia_repository.get_status()))
@singleton
class SettingsInteractor:
@inject
def __init__(self) -> None:
pass
@staticmethod
def get_bool(key: str, default: Optional[bool] = None) -> bool:
if default is None:
default = SETTINGS_DEFAULTS[key]
setting: Setting = Setting.get_or_none(key=key)
if setting is not None:
return bool(setting.value)
return bool(default)
@staticmethod
def set_bool(key: str, value: bool) -> None:
setting: Setting = Setting.get_or_none(key=key)
if setting is not None:
setting.value = value
setting.save()
else:
Setting.create(key=key, value=value)
@staticmethod
def get_int(key: str, default: Optional[int] = None) -> int:
if default is None:
default = SETTINGS_DEFAULTS[key]
setting: Setting = Setting.get_or_none(key=key)
if setting is not None:
return int(setting.value)
return default
@staticmethod
def set_int(key: str, value: int) -> None:
setting: Setting = Setting.get_or_none(key=key)
if setting is not None:
setting.value = value
setting.save()
else:
Setting.create(key=key, value=value)
@staticmethod
def get_str(key: str, default: Optional[str] = None) -> str:
if default is None:
default = SETTINGS_DEFAULTS[key]
setting: Setting = Setting.get_or_none(key=key)
if setting is not None:
return str(setting.value.decode("utf-8"))
return str(default)
@staticmethod
def set_str(key: str, value: str) -> None:
setting: Setting = Setting.get_or_none(key=key)
if setting is not None:
setting.value = value.encode("utf-8")
setting.save()
else:
Setting.create(key=key, value=value.encode("utf-8"))
@singleton
class CheckNewVersionInteractor:
URL_PATTERN = 'https://pypi.python.org/pypi/{package}/json'
@inject
def __init__(self) -> None: