Skip to content

Commit c18c623

Browse files
authored
Merge pull request #225 from ckan/python-3.14-support
Python 3.14 support, Drop Python 2 support, cleanup
2 parents 9641027 + 77bd851 commit c18c623

24 files changed

Lines changed: 154 additions & 286 deletions

.github/workflows/publish-pypi.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: Publish to PyPI
2+
3+
# Publish to PyPI when a tag is pushed
4+
on:
5+
push:
6+
tags:
7+
- 'ckanapi-**'
8+
9+
jobs:
10+
build:
11+
if: github.repository == 'ckan/ckanapi'
12+
name: Build distribution
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
- name: Set up Python
17+
uses: actions/setup-python@v5
18+
with:
19+
python-version: "3.11"
20+
- name: Install pypa/build
21+
run: python3 -m pip install build --user
22+
- name: Build a binary wheel and a source tarball
23+
run: python3 -m build
24+
- name: Store the distribution packages
25+
uses: actions/upload-artifact@v4
26+
with:
27+
name: python-package-distributions
28+
path: dist/
29+
30+
publish-to-pypi:
31+
name: Publish Python distribution on PyPI
32+
needs:
33+
- build
34+
runs-on: ubuntu-latest
35+
environment:
36+
name: pypi
37+
url: https://pypi.org/p/ckanapi
38+
permissions:
39+
id-token: write
40+
steps:
41+
- name: Download all the dists
42+
uses: actions/download-artifact@v4
43+
with:
44+
name: python-package-distributions
45+
path: dist/
46+
- name: Publish distribution to PyPI
47+
uses: pypa/gh-action-pypi-publish@release/v1
48+
49+
publishSkipped:
50+
if: github.repository != 'ckan/ckanapi'
51+
runs-on: ubuntu-latest
52+
steps:
53+
- run: |
54+
echo "## Skipping PyPI publish on downstream repository" >> $GITHUB_STEP_SUMMARY

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ jobs:
44
test:
55
strategy:
66
matrix:
7-
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
7+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
88
runs-on: ubuntu-latest
99
container:
1010
# INFO: python 2 is no longer supported in

README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,8 @@ For this example, we use --insecure as the CKAN demo uses a
6565
self-signed certificate.
6666

6767
Local CKAN actions may be run by specifying the config file with -c.
68-
If no remote server or config file is specified the CLI will look for
69-
a development.ini file in the current directory, much like paster
70-
commands.
68+
If no remote server or config file is specified, the CLI will look for
69+
a ckan.ini file in the current directory, much like `ckan` commands.
7170

7271
Local CKAN actions are performed by the site user (default system
7372
administrator) when -u is not specified.
@@ -497,7 +496,7 @@ groups = demo.action.group_list(id='data-explorer')
497496

498497
To run the tests:
499498

500-
python setup.py test
499+
python -m unittest discover
501500

502501

503502
## License

ckanapi/cli/action.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def action(ckan, arguments, stdin=None):
2727
action_args = {}
2828
with open(expanduser(arguments['--input'])) as in_f:
2929
action_args = json.loads(
30-
in_f.read().decode('utf-8') if sys.version_info.major == 2 else in_f.read())
30+
in_f.read())
3131
else:
3232
action_args = {}
3333
for kv in arguments['KEY=STRING']:

ckanapi/cli/batch.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,6 @@
1212
from ckanapi.cli import workers
1313
from ckanapi.cli.utils import completion_stats, compact_json, quiet_int_pipe
1414

15-
try:
16-
unicode
17-
except NameError:
18-
unicode = str
19-
2015

2116
def batch_actions(ckan, arguments,
2217
worker_pool=None, stdin=None, stdout=None, stderr=None):
@@ -143,7 +138,7 @@ def reply(action, error, response):
143138
obj = json.loads(line.decode('utf-8'))
144139
except UnicodeDecodeError as e:
145140
obj = None
146-
reply('read', 'UnicodeDecodeError', unicode(e))
141+
reply('read', 'UnicodeDecodeError', str(e))
147142
continue
148143

149144
requests_kwargs = None
@@ -163,7 +158,7 @@ def reply(action, error, response):
163158
reply('read', 'IOError', {
164159
'parameter':fkey,
165160
'file_name':fvalue,
166-
'error':unicode(e.args[1]),
161+
'error':str(e.args[1]),
167162
})
168163
continue
169164

@@ -173,9 +168,9 @@ def reply(action, error, response):
173168
except ValidationError as e:
174169
reply(action, 'ValidationError', e.error_dict)
175170
except SearchIndexError as e:
176-
reply(action, 'SearchIndexError', unicode(e))
171+
reply(action, 'SearchIndexError', str(e))
177172
except NotAuthorized as e:
178-
reply(action, 'NotAuthorized', unicode(e))
173+
reply(action, 'NotAuthorized', str(e))
179174
except NotFound:
180175
reply(action, 'NotFound', obj)
181176
else:

ckanapi/cli/ckan_click.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ def api(context, args):
1010
from ckanapi.cli.main import main
1111
import sys
1212
sys.argv[1:] = args
13-
context.exit(main(running_with_paster=True) or 0)
13+
context.exit(main(running_with_ckan_command=True) or 0)

ckanapi/cli/delete.py

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,13 @@
88
from datetime import datetime
99
from itertools import chain
1010
import re
11-
try:
12-
from urllib.parse import urlparse
13-
except ImportError:
14-
from urlparse import urlparse
11+
from urllib.parse import urlparse
1512

1613
from ckanapi.errors import (NotFound, NotAuthorized, ValidationError,
1714
SearchIndexError)
1815
from ckanapi.cli import workers
1916
from ckanapi.cli.utils import completion_stats, compact_json, quiet_int_pipe
2017

21-
try:
22-
unicode
23-
except NameError:
24-
unicode = str
2518

2619
def delete_things(ckan, thing, arguments,
2720
worker_pool=None, stdin=None, stdout=None, stderr=None):
@@ -137,20 +130,20 @@ def extract_ids_or_names(line):
137130
except ValueError:
138131
return [line.strip()] # 5
139132
if isinstance(j, list) and all(
140-
isinstance(e, unicode) for e in j):
133+
isinstance(e, str) for e in j):
141134
return j # 4
142-
elif isinstance(j, unicode):
135+
elif isinstance(j, str):
143136
return [j] # 3
144137
elif isinstance(j, dict):
145-
if 'id' in j and isinstance(j['id'], unicode):
138+
if 'id' in j and isinstance(j['id'], str):
146139
return [j['id']] # 1
147-
if 'name' in j and isinstance(j['name'], unicode):
140+
if 'name' in j and isinstance(j['name'], str):
148141
return [j['name']] # 1 again
149142
if 'results' in j and isinstance(j['results'], list):
150143
out = []
151144
for r in j['results']:
152145
if (not isinstance(r, dict) or 'id' not in r or
153-
not isinstance(r['id'], unicode)):
146+
not isinstance(r['id'], str)):
154147
break
155148
out.append(r['id'])
156149
else:
@@ -203,7 +196,7 @@ def reply(error, response):
203196
try:
204197
name = json.loads(line.decode('utf-8'))
205198
except UnicodeDecodeError as e:
206-
reply('UnicodeDecodeError', unicode(e))
199+
reply('UnicodeDecodeError', str(e))
207200
continue
208201

209202
try:
@@ -213,7 +206,7 @@ def reply(error, response):
213206
ckan.call_action(thing_delete, {'id': name},
214207
requests_kwargs=requests_kwargs)
215208
except NotAuthorized as e:
216-
reply('NotAuthorized', unicode(e))
209+
reply('NotAuthorized', str(e))
217210
except NotFound:
218211
reply('NotFound', name)
219212
else:

ckanapi/cli/load.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,13 @@
88
import requests
99
from datetime import datetime
1010
import re
11-
try:
12-
from urllib.parse import urlparse
13-
except ImportError:
14-
from urlparse import urlparse
11+
from urllib.parse import urlparse
1512

1613
from ckanapi.errors import (NotFound, NotAuthorized, ValidationError,
1714
SearchIndexError)
1815
from ckanapi.cli import workers
1916
from ckanapi.cli.utils import completion_stats, compact_json, quiet_int_pipe
2017

21-
try:
22-
unicode
23-
except NameError:
24-
unicode = str
25-
2618

2719
def load_things(ckan, thing, arguments,
2820
worker_pool=None, stdin=None, stdout=None, stderr=None):
@@ -167,7 +159,7 @@ def reply(action, error, response):
167159
obj = json.loads(line.decode('utf-8'))
168160
except UnicodeDecodeError as e:
169161
obj = None
170-
reply('read', 'UnicodeDecodeError', unicode(e))
162+
reply('read', 'UnicodeDecodeError', str(e))
171163
continue
172164

173165
requests_kwargs = None
@@ -191,7 +183,7 @@ def reply(action, error, response):
191183
except NotFound:
192184
pass
193185
except NotAuthorized as e:
194-
reply('show', 'NotAuthorized', unicode(e))
186+
reply('show', 'NotAuthorized', str(e))
195187
continue
196188
name = obj.get('name')
197189
if not existing and name:
@@ -201,7 +193,7 @@ def reply(action, error, response):
201193
except NotFound:
202194
pass
203195
except NotAuthorized as e:
204-
reply('show', 'NotAuthorized', unicode(e))
196+
reply('show', 'NotAuthorized', str(e))
205197
continue
206198

207199
if existing:
@@ -233,9 +225,9 @@ def reply(action, error, response):
233225
except ValidationError as e:
234226
reply(act, 'ValidationError', e.error_dict)
235227
except SearchIndexError as e:
236-
reply(act, 'SearchIndexError', unicode(e))
228+
reply(act, 'SearchIndexError', str(e))
237229
except NotAuthorized as e:
238-
reply(act, 'NotAuthorized', unicode(e))
230+
reply(act, 'NotAuthorized', str(e))
239231
except NotFound:
240232
reply(act, 'NotFound', obj)
241233
else:

ckanapi/cli/main.py

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@
8787
import sys
8888
import os
8989
from docopt import docopt
90-
from pkg_resources import load_entry_point
9190
import subprocess
9291

9392
from ckanapi.version import __version__
@@ -105,25 +104,19 @@
105104

106105
# explicit logger namespace for easy logging handlers
107106
log = getLogger('ckan.ckanapi')
108-
PYTHON2 = str is bytes
109107

110108
def parse_arguments():
111109
# docopt is awesome
112110
return docopt(__doc__, version=__version__)
113111

114112

115-
def main(running_with_paster=False):
113+
def main(running_with_ckan_command=False):
116114
"""
117115
ckanapi command line entry point
118116
"""
119117
arguments = parse_arguments()
120118

121-
if not running_with_paster and not arguments['--remote']:
122-
if PYTHON2:
123-
ckan_ini = os.environ.get('CKAN_INI')
124-
if ckan_ini and not arguments['--config']:
125-
sys.argv[1:1] = ['--config', ckan_ini]
126-
return _switch_to_paster(arguments)
119+
if not running_with_ckan_command and not arguments['--remote']:
127120
return _switch_to_ckan_click(arguments)
128121

129122
if arguments['--remote']:
@@ -196,15 +189,6 @@ def main(running_with_paster=False):
196189
assert 0, arguments # we shouldn't be here
197190

198191

199-
def _switch_to_paster(arguments):
200-
"""
201-
** legacy python2-only **
202-
With --config we switch to the paster command version of the cli
203-
"""
204-
sys.argv[1:1] = ["--plugin=ckanapi", "ckanapi"]
205-
sys.exit(load_entry_point('PasteScript', 'console_scripts', 'paster')())
206-
207-
208192
def _switch_to_ckan_click(arguments):
209193
"""
210194
Local commands must be run through ckan CLI to set up environment

ckanapi/cli/paster.py

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

0 commit comments

Comments
 (0)