Skip to content

Commit 0ddaa2e

Browse files
Merge pull request #34 from melexis/error-handling
Support Python 3.13
2 parents 50a7dd3 + fa24a8a commit 0ddaa2e

File tree

10 files changed

+113
-88
lines changed

10 files changed

+113
-88
lines changed

.github/workflows/python-package.yml

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ jobs:
99
strategy:
1010
fail-fast: false
1111
matrix:
12-
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
12+
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
1313
steps:
14-
- uses: actions/checkout@v3
14+
- uses: actions/checkout@v4
1515
- name: Set up Python ${{ matrix.python-version }}
16-
uses: actions/setup-python@v4
16+
uses: actions/setup-python@v5
1717
with:
1818
python-version: ${{ matrix.python-version }}
1919
- name: Install dependencies
@@ -23,16 +23,16 @@ jobs:
2323
- name: Run test
2424
run: tox -e py
2525
- name: Upload coverage to Codecov
26-
if: matrix.python-version == 3.9
27-
uses: codecov/codecov-action@v3
26+
if: matrix.python-version == 3.13
27+
uses: codecov/codecov-action@v5
2828
with:
2929
token: ${{ secrets.CODECOV_TOKEN }}
3030
files: ./coverage.xml
3131
fail_ci_if_error: true
3232
flags: unittests
3333
verbose: true
3434
- name: Static checks
35-
if: matrix.python-version == 3.9
35+
if: matrix.python-version == 3.13
3636
run: tox -e check
3737

3838
deploy:
@@ -41,11 +41,11 @@ jobs:
4141
needs: test
4242
runs-on: ubuntu-latest
4343
steps:
44-
- uses: actions/checkout@v3
44+
- uses: actions/checkout@v4
4545
- name: Set up Python
46-
uses: actions/setup-python@v4
46+
uses: actions/setup-python@v5
4747
with:
48-
python-version: '3.7'
48+
python-version: '3.13'
4949
- name: Install dependencies
5050
run: |
5151
python -m pip install --upgrade pip
@@ -55,5 +55,5 @@ jobs:
5555
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
5656
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
5757
run: |
58-
python setup.py sdist bdist_wheel
58+
python -m build --wheel
5959
twine upload dist/*

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,4 @@ pip-selfcheck.json
9696
*.swp
9797

9898
# Offline version file
99-
src/mlx/__version__.py
99+
src/mlx/jira_juggler/_version.py

MANIFEST.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ global-exclude *.py[cod] __pycache__ *.so *.dylib
1212
include NOTICE
1313
include .env.example
1414

15-
exclude src/mlx/__version__.py
15+
exclude src/mlx/jira_juggler/_version.py
1616

1717
# added by check_manifest.py
1818
include *.tjp

pyproject.toml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
[build-system]
2+
requires = ["setuptools>=77", "wheel", "setuptools_scm>=7.1.0"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[tool.setuptools.packages.find]
6+
where = ["src/"]
7+
include = ["mlx.jira_juggler"]
8+
9+
[tool.setuptools_scm]
10+
write_to = "src/mlx/jira_juggler/_version.py"
11+
12+
[project]
13+
name = "mlx.jira_juggler"
14+
description = "A Python script for extracting data from Jira, and converting to TaskJuggler (tj3) output"
15+
authors = [
16+
{name = "Jasper Craeghs", email = "jce@melexis.com"}
17+
]
18+
readme = "README.rst"
19+
requires-python = ">=3.9"
20+
license = "Apache-2.0"
21+
license-files = ["LICENSE"]
22+
dynamic = ["version"]
23+
classifiers = [
24+
"Development Status :: 4 - Beta",
25+
"Intended Audience :: Developers",
26+
"Operating System :: Unix",
27+
"Operating System :: POSIX",
28+
"Operating System :: Microsoft :: Windows",
29+
"Programming Language :: Python",
30+
"Programming Language :: Python :: 3.9",
31+
"Programming Language :: Python :: 3.10",
32+
"Programming Language :: Python :: 3.11",
33+
"Programming Language :: Python :: 3.12",
34+
"Programming Language :: Python :: 3.13",
35+
]
36+
keywords = [
37+
"Jira",
38+
"taskjuggler",
39+
"gantt",
40+
"project planning",
41+
"planning",
42+
"software engineering",
43+
]
44+
dependencies = [
45+
"jira>=3.8",
46+
"python-dateutil>=2.8.0,<3.0",
47+
"natsort>=8.3.1",
48+
"python-decouple",
49+
]
50+
51+
[project.urls]
52+
Homepage = "https://github.com/melexis/jira-juggler"
53+
Repository = "https://github.com/melexis/jira-juggler"
54+
55+
[project.scripts]
56+
jira-juggler = "mlx.jira_juggler.jira_juggler:entrypoint"
57+
58+
[tool.setuptools.package-data]
59+
"*" = ["*"]

setup.py

Lines changed: 0 additions & 50 deletions
This file was deleted.

src/mlx/__init__.py

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/mlx/jira_juggler/__init__.py

Whitespace-only changes.
Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -594,16 +594,35 @@ def load_issues_from_jira(self, depend_on_preceding=False, sprint_field_name='',
594594
try:
595595
issues = jirahandle.search_issues(self.query, maxResults=JIRA_PAGE_SIZE, startAt=self.issue_count,
596596
expand='changelog')
597-
except JIRAError:
598-
logging.error('Invalid Jira query "%s"', self.query)
597+
except JIRAError as err:
598+
logging.error(f'Failed to query JIRA: {err}')
599+
if err.status_code == 401:
600+
logging.error('Please check your JIRA credentials in the .env file or environment variables.')
601+
elif err.status_code == 403:
602+
logging.error('You do not have permission to access this JIRA project or query.')
603+
elif err.status_code == 404:
604+
logging.error('The JIRA endpoint is not found. Please check the endpoint URL.')
605+
elif err.status_code == 400:
606+
# Parse and display the specific JQL errors more clearly
607+
try:
608+
error_data = err.response.json()
609+
if 'errorMessages' in error_data:
610+
for error_msg in error_data['errorMessages']:
611+
logging.error(f'JIRA query error: {error_msg}')
612+
except Exception:
613+
pass # Fall back to generic error if JSON parsing fails
614+
615+
logging.error('Invalid JQL query syntax. Please check your query.')
616+
else:
617+
logging.error(f'An unexpected error occurred: {err}')
599618
return None
600619

601620
if len(issues) <= 0:
602621
busy = False
603622

604623
self.issue_count += len(issues)
605624
for issue in issues:
606-
logging.debug('Retrieved %s: %s', issue.key, issue.fields.summary)
625+
logging.debug(f'Retrieved {issue.key}: {issue.fields.summary}')
607626
tasks.append(JugglerTask(issue))
608627

609628
self.validate_tasks(tasks)

tests/test_jira_juggler.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
pip.main(['install', 'mock'])
2424
from mock import MagicMock, patch, call
2525

26-
import mlx.jira_juggler as dut
26+
import mlx.jira_juggler.jira_juggler as dut
2727

2828
try:
2929
from jira import JIRA
@@ -167,7 +167,7 @@ class TestJiraJuggler(unittest.TestCase):
167167
def SetUp(self):
168168
'''SetUp is run before each test to provide clean working environment'''
169169

170-
@patch('mlx.jira_juggler.JIRA', autospec=True)
170+
@patch('mlx.jira_juggler.jira_juggler.JIRA', autospec=True)
171171
def test_empty_query_result(self, jira_mock):
172172
'''Test for Jira not returning any task on the given query'''
173173
jira_mock_object = MagicMock(spec=JIRA)
@@ -179,7 +179,7 @@ def test_empty_query_result(self, jira_mock):
179179
juggler.juggle()
180180
jira_mock_object.search_issues.assert_called_once_with(self.QUERY, maxResults=dut.JIRA_PAGE_SIZE, startAt=0, expand='changelog')
181181

182-
@patch('mlx.jira_juggler.JIRA', autospec=True)
182+
@patch('mlx.jira_juggler.jira_juggler.JIRA', autospec=True)
183183
def test_single_task_happy(self, jira_mock):
184184
'''Test for simple happy flow: single task is returned by Jira Server'''
185185
jira_mock_object = MagicMock(spec=JIRA)
@@ -203,7 +203,7 @@ def test_single_task_happy(self, jira_mock):
203203
self.assertEqual(self.ESTIMATE1 / self.SECS_PER_DAY, issues[0].properties['effort'].value)
204204
self.assertEqual(self.DEPENDS1, issues[0].properties['depends'].value)
205205

206-
@patch('mlx.jira_juggler.JIRA', autospec=True)
206+
@patch('mlx.jira_juggler.jira_juggler.JIRA', autospec=True)
207207
def test_single_task_email_happy(self, jira_mock):
208208
'''Test for simple happy flow: single task is returned by Jira Cloud'''
209209
jira_mock_object = MagicMock(spec=JIRA)
@@ -228,7 +228,7 @@ def test_single_task_email_happy(self, jira_mock):
228228
self.assertEqual(self.ESTIMATE1 / self.SECS_PER_DAY, issues[0].properties['effort'].value)
229229
self.assertEqual(self.DEPENDS1, issues[0].properties['depends'].value)
230230

231-
@patch('mlx.jira_juggler.JIRA', autospec=True)
231+
@patch('mlx.jira_juggler.jira_juggler.JIRA', autospec=True)
232232
def test_single_task_email_hidden(self, jira_mock):
233233
'''Test for error logging when user has restricted email visibility in Jira Cloud'''
234234
jira_mock_object = MagicMock(spec=JIRA)
@@ -254,7 +254,7 @@ def test_single_task_email_hidden(self, jira_mock):
254254
self.assertEqual(self.ESTIMATE1 / self.SECS_PER_DAY, issues[0].properties['effort'].value)
255255
self.assertEqual(self.DEPENDS1, issues[0].properties['depends'].value)
256256

257-
@patch('mlx.jira_juggler.JIRA', autospec=True)
257+
@patch('mlx.jira_juggler.jira_juggler.JIRA', autospec=True)
258258
def test_single_task_minimal(self, jira_mock):
259259
'''Test for minimal happy flow: single task with minimal content is returned by Jira
260260
@@ -276,7 +276,7 @@ def test_single_task_minimal(self, jira_mock):
276276
self.assertEqual(self.SUMMARY1, issues[0].summary)
277277
self.assertEqual(dut.JugglerTaskEffort.DEFAULT_VALUE, issues[0].properties['effort'].value)
278278

279-
@patch('mlx.jira_juggler.JIRA', autospec=True)
279+
@patch('mlx.jira_juggler.jira_juggler.JIRA', autospec=True)
280280
def test_estimate_too_low(self, jira_mock):
281281
'''Test for correcting an estimate which is too low'''
282282
jira_mock_object = MagicMock(spec=JIRA)
@@ -296,7 +296,7 @@ def test_estimate_too_low(self, jira_mock):
296296
self.assertEqual(self.SUMMARY1, issues[0].summary)
297297
self.assertEqual(dut.JugglerTaskEffort.MINIMAL_VALUE, issues[0].properties['effort'].value)
298298

299-
@patch('mlx.jira_juggler.JIRA', autospec=True)
299+
@patch('mlx.jira_juggler.jira_juggler.JIRA', autospec=True)
300300
def test_broken_depends(self, jira_mock):
301301
'''Test for removing a broken link to a dependant task'''
302302
jira_mock_object = MagicMock(spec=JIRA)
@@ -317,7 +317,7 @@ def test_broken_depends(self, jira_mock):
317317
self.assertEqual(self.SUMMARY1, issues[0].summary)
318318
self.assertEqual([], issues[0].properties['depends'].value)
319319

320-
@patch('mlx.jira_juggler.JIRA', autospec=True)
320+
@patch('mlx.jira_juggler.jira_juggler.JIRA', autospec=True)
321321
def test_task_depends(self, jira_mock):
322322
'''Test for dual happy flow: one task depends on the other'''
323323
jira_mock_object = MagicMock(spec=JIRA)
@@ -352,7 +352,7 @@ def test_task_depends(self, jira_mock):
352352
self.assertEqual(self.ESTIMATE2 / self.SECS_PER_DAY, issues[1].properties['effort'].value)
353353
self.assertEqual(self.DEPENDS2, issues[1].properties['depends'].value)
354354

355-
@patch('mlx.jira_juggler.JIRA', autospec=True)
355+
@patch('mlx.jira_juggler.jira_juggler.JIRA', autospec=True)
356356
def test_task_double_depends(self, jira_mock):
357357
'''Test for extended happy flow: one task depends on two others'''
358358
jira_mock_object = MagicMock(spec=JIRA)
@@ -397,7 +397,7 @@ def test_task_double_depends(self, jira_mock):
397397
self.assertEqual(self.ESTIMATE3 / self.SECS_PER_DAY, issues[2].properties['effort'].value)
398398
self.assertEqual(self.DEPENDS3, issues[2].properties['depends'].value)
399399

400-
@patch('mlx.jira_juggler.JIRA', autospec=True)
400+
@patch('mlx.jira_juggler.jira_juggler.JIRA', autospec=True)
401401
def test_resolved_task(self, jira_mock):
402402
'''Test that the last assignee in the Analyzed state is used and the Time Spent is used as effort
403403
Test that the most recent transition to the Approved/Resolved state is used to mark the end'''
@@ -465,7 +465,7 @@ def test_resolved_task(self, jira_mock):
465465
self.assertEqual(self.ESTIMATE2 / self.SECS_PER_DAY, issues[0].properties['effort'].value)
466466
self.assertEqual('2022-05-25 14:07:11.974000+02:00', str(issues[0].resolved_at_date))
467467

468-
@patch('mlx.jira_juggler.JIRA', autospec=True)
468+
@patch('mlx.jira_juggler.jira_juggler.JIRA', autospec=True)
469469
def test_closed_task(self, jira_mock):
470470
'''
471471
Test that a change of assignee after Resolved status has no effect and that the original time estimate is
@@ -506,7 +506,7 @@ def test_closed_task(self, jira_mock):
506506
self.assertEqual(self.ASSIGNEE1, issues[0].properties['allocate'].value)
507507
self.assertEqual(self.ESTIMATE1 / self.SECS_PER_DAY, issues[0].properties['effort'].value)
508508

509-
@patch('mlx.jira_juggler.JIRA', autospec=True)
509+
@patch('mlx.jira_juggler.jira_juggler.JIRA', autospec=True)
510510
def test_depend_on_preceding(self, jira_mock):
511511
'''Test --depends-on-preceding, --weeklymax and --current-date options'''
512512
jira_mock_object = MagicMock(spec=JIRA)

tox.ini

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
11
[tox]
22
envlist =
3-
py37, py38, py39, py310, py3.11
3+
py39, py310, py311, py312, py313
44
clean,
55
check,
66

77
[gh-actions]
88
python =
9-
3.7: py37
10-
3.8: py38
119
3.9: py39
1210
3.10: py310
1311
3.11: py311
12+
3.12: py312
13+
3.13: py313
1414

1515
[testenv]
1616
basepython =
1717
py: python3
1818
pypy: {env:TOXPYTHON:pypy}
19-
py37: {env:TOXPYTHON:python3.7}
20-
py38: {env:TOXPYTHON:python3.8}
2119
py39: {env:TOXPYTHON:python3.9}
2220
py310: {env:TOXPYTHON:python3.10}
2321
py311: {env:TOXPYTHON:python3.11}
22+
py312: {env:TOXPYTHON:python3.12}
23+
py313: {env:TOXPYTHON:python3.13}
2424
{clean,check,report,coveralls}: python3
2525
setenv =
2626
PYTHONPATH={toxinidir}/tests
@@ -39,16 +39,14 @@ commands =
3939

4040
[testenv:check]
4141
deps =
42-
docutils
4342
check-manifest
4443
flake8
45-
readme-renderer
4644
twine
4745
commands =
48-
python setup.py sdist
46+
python -m build --wheel
4947
twine check dist/*
5048
check-manifest {toxinidir} -u
51-
flake8 src tests setup.py
49+
flake8 src tests
5250
jira-juggler -h
5351

5452
[testenv:coveralls]

0 commit comments

Comments
 (0)