Compare commits

...

10 Commits

Author SHA1 Message Date
c5ee87535a WIP 2024-03-01 22:44:31 +01:00
33f357587e [ci] fuck bill gates 2024-03-01 00:09:41 +01:00
Gitlab CI
1dc85b7b75 Updated windows deps from CI 2024-02-29 22:55:23 +00:00
a9c22cf9ac [ci] windows stuff 2024-02-29 23:46:30 +01:00
4f526be6dc [ci] Add cache key 2024-02-29 23:44:22 +01:00
9e87ebc254 [ci] Fix pathes 2024-02-29 23:37:26 +01:00
7387bc5176 Update README.md 2024-02-29 23:35:59 +01:00
4c0715ee83 Propagate config changes better 2024-02-29 23:00:43 +01:00
64f72dbf35 Add browser_cookie3 dep 2024-02-29 22:00:51 +01:00
6ff20ca605 Implement cookie authentication 2024-02-29 21:59:29 +01:00
12 changed files with 85 additions and 34 deletions

View File

@ -9,6 +9,7 @@ variables:
value: "0" value: "0"
cache: cache:
key: cache-$CI_COMMIT_REF_SLUG-$WINDOWS_PYTHON_PACKAGE_NAME
paths: paths:
- .cache/pip - .cache/pip
- .venv/ - .venv/
@ -56,10 +57,8 @@ update_windows_deps:
- python --version - python --version
- pip install pipenv - pip install pipenv
script: script:
- cp Pipfile.lock Pipfile.lock.bak
- pipenv lock --dev - pipenv lock --dev
- cp Pipfile.lock Pipfile.lock.windows - cp Pipfile.lock Pipfile.lock.windows
- mv Pipfile.lock.bak Pipefile.lock
artifacts: artifacts:
paths: paths:
- Pipfile.lock.windows - Pipfile.lock.windows
@ -118,7 +117,7 @@ package_windows:
- refreshenv - refreshenv
- python --version - python --version
- pip install pipenv - pip install pipenv
- mv Pipenv.lock.windows Pipenv.lock - mv -force Pipfile.lock.windows Pipfile.lock
- pipenv sync --dev - pipenv sync --dev
script: script:
- pipenv run pyinstaller --onefile --windowed --name "${WINDOWS_AMD64_BINARY}" -i icon.ico src/fime/main.py - pipenv run pyinstaller --onefile --windowed --name "${WINDOWS_AMD64_BINARY}" -i icon.ico src/fime/main.py

View File

@ -9,7 +9,8 @@ requests = "~=2.28"
requests-futures = "~=1.0" requests-futures = "~=1.0"
packaging = "~=23.0" packaging = "~=23.0"
loguru = "~=0.6" loguru = "~=0.6"
browser-cookie3 = "*" browser-cookie3 = "~=0.19"
pebble = "~=5.0"
[dev-packages] [dev-packages]
pyinstaller = "~=5.6" pyinstaller = "~=5.6"

17
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "236b0d1f3ca2625232ac2e64bf963e188ff5fa85fc570e46f8e877bf13cb98d3" "sha256": "b9f0e0869765dff4160bfe632eb361b03a8b93bd8d9b19e36d371f2ff76c73f1"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -204,6 +204,15 @@
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==23.2" "version": "==23.2"
}, },
"pebble": {
"hashes": [
"sha256:c8a0659b215ff6dcc974516018fcd95c5c626d8bb9a6668dcfbf85880e6390dc",
"sha256:e7f7ecfd0107ab7cec9f3bb411a856c4d7d552202d4e9a8b038e9a64ae31fd8c"
],
"index": "pypi",
"markers": "python_version >= '3.6'",
"version": "==5.0.6"
},
"pycryptodomex": { "pycryptodomex": {
"hashes": [ "hashes": [
"sha256:0daad007b685db36d977f9de73f61f8da2a7104e20aca3effd30752fd56f73e1", "sha256:0daad007b685db36d977f9de73f61f8da2a7104e20aca3effd30752fd56f73e1",
@ -355,11 +364,11 @@
}, },
"pyinstaller-hooks-contrib": { "pyinstaller-hooks-contrib": {
"hashes": [ "hashes": [
"sha256:131494f9cfce190aaa66ed82e82c78b2723d1720ce64d012fbaf938f4ab01d35", "sha256:43f3e084ae5f826415399d72ecf2e32328fe859ad7455c7cddfc09f1a61c90b7",
"sha256:51a51ea9e1ae6bd5ffa7ec45eba7579624bf4f2472ff56dba0edc186f6ed46a6" "sha256:8f5ac1acdafde9e553c82242aeae2d2f8fb65ec8e2b0ba416547948108a73e01"
], ],
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==2024.1" "version": "==2024.2"
}, },
"pyproject-hooks": { "pyproject-hooks": {
"hashes": [ "hashes": [

View File

@ -379,11 +379,11 @@
}, },
"pyinstaller-hooks-contrib": { "pyinstaller-hooks-contrib": {
"hashes": [ "hashes": [
"sha256:131494f9cfce190aaa66ed82e82c78b2723d1720ce64d012fbaf938f4ab01d35", "sha256:43f3e084ae5f826415399d72ecf2e32328fe859ad7455c7cddfc09f1a61c90b7",
"sha256:51a51ea9e1ae6bd5ffa7ec45eba7579624bf4f2472ff56dba0edc186f6ed46a6" "sha256:8f5ac1acdafde9e553c82242aeae2d2f8fb65ec8e2b0ba416547948108a73e01"
], ],
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==2024.1" "version": "==2024.2"
}, },
"pyproject-hooks": { "pyproject-hooks": {
"hashes": [ "hashes": [

View File

@ -28,6 +28,10 @@ The Jira URL will just be the base URL to you're Jira Instance (e.g. `https://ji
The Jira Token is a Personal Access Token. See [here](https://confluence.atlassian.com/enterprise/using-personal-access-tokens-1026032365.html) on how to get one. The Jira Token is a Personal Access Token. See [here](https://confluence.atlassian.com/enterprise/using-personal-access-tokens-1026032365.html) on how to get one.
There is a new authentication method, which uses the cookies from your browser (most popular browsers should be
supported). To use it, you configure it and log into your Jira instance in your browser. If it doesn't work, try closing
your browser, to force it to write the cookies to the disk
## License ## License
Licensed under MIT license. See License.md for more details. Licensed under MIT license. See License.md for more details.

View File

@ -23,6 +23,8 @@ install_requires =
requests-futures requests-futures
packaging packaging
loguru loguru
browser_cookie3
pebble
[options.packages.find] [options.packages.find]
where = src where = src

View File

@ -1,3 +1,5 @@
from concurrent.futures import Executor
try: try:
from PySide6 import QtCore, QtGui, QtWidgets from PySide6 import QtCore, QtGui, QtWidgets
except ImportError: except ImportError:
@ -10,14 +12,14 @@ from fime.util import get_icon
class ImportTask(QtWidgets.QDialog): class ImportTask(QtWidgets.QDialog):
def __init__(self, config: Config, parent, *args, **kwargs): def __init__(self, config: Config, executor: Executor, parent, *args, **kwargs):
super().__init__(parent, *args, **kwargs) super().__init__(parent, *args, **kwargs)
self.setWindowTitle("New Tasks") self.setWindowTitle("New Tasks")
self.config = config self.config = config
self.line_edit = QtWidgets.QLineEdit(self) self.line_edit = QtWidgets.QLineEdit(self)
self.completer = TaskCompleter(config) self.completer = TaskCompleter(config, executor)
self.line_edit.setCompleter(self.completer) self.line_edit.setCompleter(self.completer)
self.line_edit.textChanged.connect(self.completer.update_picker) self.line_edit.textChanged.connect(self.completer.update_picker)
self.line_edit.setFocus() self.line_edit.setFocus()
@ -89,7 +91,7 @@ class ImportTask(QtWidgets.QDialog):
def showEvent(self, _): def showEvent(self, _):
self.auto_change_task_check_box.setChecked(self.config.import_auto_change_task) self.auto_change_task_check_box.setChecked(self.config.import_auto_change_task)
# pick up config changes # pick up config changes
self.completer.update_urls() self.completer.update()
self.line_edit.setText("") self.line_edit.setText("")
self.raise_() self.raise_()
self.line_edit.setFocus() self.line_edit.setFocus()

View File

@ -23,7 +23,7 @@ from fime.import_task import ImportTask
from fime.report import ReportDialog from fime.report import ReportDialog
from fime.settings import Settings from fime.settings import Settings
from fime.task_edit import TaskEdit from fime.task_edit import TaskEdit
from fime.util import get_screen_height, get_icon from fime.util import get_screen_height, get_icon, CompatPool
class App: class App:
@ -41,7 +41,9 @@ class App:
self.menu = QtWidgets.QMenu(None) self.menu = QtWidgets.QMenu(None)
self.import_task = ImportTask(self.config, None) self.executor = CompatPool()
self.import_task = ImportTask(self.config, self.executor, None)
self.import_task.accepted.connect(self.new_task_imported) self.import_task.accepted.connect(self.new_task_imported)
self.taskEdit = TaskEdit(self.tasks, None) self.taskEdit = TaskEdit(self.tasks, None)
@ -50,7 +52,7 @@ class App:
self.reportDialog = ReportDialog(self.tasks, Report(lcd), None) self.reportDialog = ReportDialog(self.tasks, Report(lcd), None)
self.reportDialog.accepted.connect(self.log_edited) self.reportDialog.accepted.connect(self.log_edited)
self.worklogDialog = WorklogDialog(self.config, Worklog(lcd), None) self.worklogDialog = WorklogDialog(self.config, self.executor, Worklog(lcd), None)
self.worklogDialog.accepted.connect(self.log_edited) self.worklogDialog.accepted.connect(self.log_edited)
self.settings = Settings(self.config, None) self.settings = Settings(self.config, None)
@ -153,7 +155,7 @@ class App:
menu_items.append(("Settings", partial(self.open_new_dialog, self.settings))) menu_items.append(("Settings", partial(self.open_new_dialog, self.settings)))
menu_items.append(("About", partial(self.open_new_dialog, self.about))) menu_items.append(("About", partial(self.open_new_dialog, self.about)))
menu_items.append(("Close", self.app.quit)) menu_items.append(("Close", self.quit_handler))
if self.config.flip_menu: if self.config.flip_menu:
menu_items.reverse() menu_items.reverse()
@ -169,17 +171,23 @@ class App:
action.setIcon(get_icon("go-next")) action.setIcon(get_icon("go-next"))
action.triggered.connect(item[1]) action.triggered.connect(item[1])
def sigterm_handler(self, signo, _frame): def quit_handler(self, signo=None, _frame=None):
logger.debug(f'handling signal "{signal.strsignal(signo)}"') if signo:
logger.debug(f'handling signal "{signal.strsignal(signo)}"')
logger.debug("Quitting app")
self.app.quit() self.app.quit()
logger.debug("Shutting down HTTP requests executor")
self.executor.stop()
self.executor.join()
logger.debug("HTTP requests executor is shutdown")
def run(self): def run(self):
timer = QtCore.QTimer(None) timer = QtCore.QTimer(None)
# interrupt event loop regularly for signal handling # interrupt event loop regularly for signal handling
timer.timeout.connect(lambda: None) timer.timeout.connect(lambda: None)
timer.start(500) timer.start(500)
signal.signal(signal.SIGTERM, self.sigterm_handler) signal.signal(signal.SIGTERM, self.quit_handler)
signal.signal(signal.SIGINT, self.sigterm_handler) signal.signal(signal.SIGINT, self.quit_handler)
if PYSIDE_6: if PYSIDE_6:
self.app.exec() self.app.exec()
else: else:

View File

@ -1,6 +1,7 @@
import os import os
import re import re
import threading import threading
from concurrent.futures import Executor
from enum import Enum, auto from enum import Enum, auto
from functools import reduce, partial from functools import reduce, partial
from queue import Queue, Empty from queue import Queue, Empty
@ -27,19 +28,18 @@ class TaskCompleter(QtWidgets.QCompleter):
running = QtCore.Signal() running = QtCore.Signal()
stopped = QtCore.Signal() stopped = QtCore.Signal()
def __init__(self, config: Config, parent=None, *args, **kwargs): def __init__(self, config: Config, executor: Executor, parent=None, *args, **kwargs):
super().__init__([], parent, *args, **kwargs) super().__init__([], parent, *args, **kwargs)
self.setFilterMode(QtCore.Qt.MatchFlag.MatchContains) self.setFilterMode(QtCore.Qt.MatchFlag.MatchContains)
self.setCaseSensitivity(QtCore.Qt.CaseInsensitive) self.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.session = FuturesSession() self.session = FuturesSession(executor=executor)
self.session.headers["Accept"] = "application/json" self.session.headers["Accept"] = "application/json"
add_auth(config, self.session)
self.config = config self.config = config
self.picker_url = None self.picker_url = None
self.search_url = None self.search_url = None
self.issue_url_tmpl = None self.issue_url_tmpl = None
self.issue_key_regex = re.compile(r"^[a-zA-Z0-9]+-[0-9]+") self.issue_key_regex = re.compile(r"^[a-zA-Z0-9]+-[0-9]+")
self.update_urls() self.update()
self.text = "" self.text = ""
self.response_text = "" self.response_text = ""
self.model_data = set() self.model_data = set()
@ -53,10 +53,12 @@ class TaskCompleter(QtWidgets.QCompleter):
self.rif_counter_lock = threading.Lock() self.rif_counter_lock = threading.Lock()
self.last_rif_state = TaskCompleter.RifState.STOPPED self.last_rif_state = TaskCompleter.RifState.STOPPED
def update_urls(self): def update(self):
self.picker_url = os.path.join(self.config.jira_url, "rest/api/2/issue/picker") self.picker_url = os.path.join(self.config.jira_url, "rest/api/2/issue/picker")
self.search_url = os.path.join(self.config.jira_url, "rest/api/2/search") self.search_url = os.path.join(self.config.jira_url, "rest/api/2/search")
self.issue_url_tmpl = os.path.join(self.config.jira_url, "rest/api/2/issue/{}") self.issue_url_tmpl = os.path.join(self.config.jira_url, "rest/api/2/issue/{}")
self.session = FuturesSession()
add_auth(self.config, self.session)
@QtCore.Slot() @QtCore.Slot()
def process_response(self): def process_response(self):

View File

@ -1,9 +1,11 @@
import enum import enum
import browser_cookie3
from loguru import logger from loguru import logger
from pebble import ProcessPool
from requests import Session from requests import Session
from fime.config import Config, AuthMethods from fime.config import Config, AuthMethods, Browsers
try: try:
from PySide6 import QtCore, QtGui, QtWidgets from PySide6 import QtCore, QtGui, QtWidgets
@ -14,6 +16,11 @@ except ImportError:
import fime.icons import fime.icons
class CompatPool(ProcessPool):
def submit(self, fn, *args, **kwargs):
return self.schedule(fn, args=args, kwargs=kwargs)
def get_screen_height(qobject): def get_screen_height(qobject):
if hasattr(qobject, "screen"): if hasattr(qobject, "screen"):
return qobject.screen().size().height() return qobject.screen().size().height()
@ -61,6 +68,21 @@ def add_auth(config: Config, session: Session):
case AuthMethods.TOKEN: case AuthMethods.TOKEN:
session.headers["Authorization"] = f"Bearer {config.jira_token}" session.headers["Authorization"] = f"Bearer {config.jira_token}"
case AuthMethods.COOKIES: case AuthMethods.COOKIES:
raise NotImplemented match config.cookie_source:
case Browsers.AUTO:
cookie_jar = browser_cookie3.load()
case Browsers.FIREFOX:
cookie_jar = browser_cookie3.firefox()
case Browsers.CHROME:
cookie_jar = browser_cookie3.chrome()
case Browsers.CHROMIUM:
cookie_jar = browser_cookie3.chromium()
case Browsers.EDGE:
cookie_jar = browser_cookie3.edge()
case Browsers.OPERA:
cookie_jar = browser_cookie3.opera()
case _:
raise AssertionError("Unknown cookie_source")
session.cookies = cookie_jar
case _: case _:
raise AssertionError("Unknown auth method") raise AssertionError("Unknown auth method")

View File

@ -1,3 +1,4 @@
from concurrent.futures import Executor
from datetime import date from datetime import date
from functools import reduce, partial from functools import reduce, partial
from typing import List, Tuple from typing import List, Tuple
@ -95,10 +96,11 @@ class WorklogDialog(QtWidgets.QDialog):
if not self.return_ or self.editor.text() == self.initial_text: if not self.return_ or self.editor.text() == self.initial_text:
self.edit_finished_row.emit(row) self.edit_finished_row.emit(row)
def __init__(self, config: Config, worklog: Worklog, parent, *args, **kwargs): def __init__(self, config: Config, executor: Executor, worklog: Worklog, parent, *args, **kwargs):
super().__init__(parent, *args, **kwargs) super().__init__(parent, *args, **kwargs)
self.config = config self.config = config
self.executor = executor
self.rest = None self.rest = None
self._changing_items = False self._changing_items = False
@ -176,7 +178,7 @@ class WorklogDialog(QtWidgets.QDialog):
def showEvent(self, _): def showEvent(self, _):
# reinitialize to purge caches and pick up config changes # reinitialize to purge caches and pick up config changes
self.rest = WorklogRest(self.config) self.rest = WorklogRest(self.config, self.executor)
self._worklog.date = date.today() self._worklog.date = date.today()
self.update_all() self.update_all()
self.upload_button.setEnabled(False) self.upload_button.setEnabled(False)

View File

@ -1,5 +1,5 @@
import os import os
from concurrent.futures import Future from concurrent.futures import Future, Executor
from datetime import date, datetime, timedelta, time from datetime import date, datetime, timedelta, time
from functools import partial from functools import partial
from textwrap import dedent from textwrap import dedent
@ -16,13 +16,13 @@ from fime.util import Status, add_auth
class WorklogRest: class WorklogRest:
def __init__(self, config: Config): def __init__(self, config: Config, executor: Executor):
self.config = config self.config = config
self.user_url = os.path.join(config.jira_url, "rest/api/2/myself") self.user_url = os.path.join(config.jira_url, "rest/api/2/myself")
self.issue_url = os.path.join(config.jira_url, "rest/api/2/issue/{}") self.issue_url = os.path.join(config.jira_url, "rest/api/2/issue/{}")
self.worklog_url = os.path.join(config.jira_url, "rest/api/2/issue/{}/worklog") self.worklog_url = os.path.join(config.jira_url, "rest/api/2/issue/{}/worklog")
self.worklog_update_url = os.path.join(config.jira_url, "rest/api/2/issue/{issue_key}/worklog/{worklog_id}") self.worklog_update_url = os.path.join(config.jira_url, "rest/api/2/issue/{issue_key}/worklog/{worklog_id}")
self.session = FuturesSession() self.session = FuturesSession(executor=executor)
self.session.headers["Accept"] = "application/json" self.session.headers["Accept"] = "application/json"
add_auth(config, self.session) add_auth(config, self.session)
self._user = None self._user = None