From a27b32c26661a17ec25b3c5f41be7e1142916961 Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 19 Feb 2026 11:25:45 +0100 Subject: [PATCH 01/10] Remove pkg_resources to support py 3.14, drop Python 2 support --- ckanapi/cli/main.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/ckanapi/cli/main.py b/ckanapi/cli/main.py index 04ce7f5..89b029d 100644 --- a/ckanapi/cli/main.py +++ b/ckanapi/cli/main.py @@ -87,7 +87,6 @@ import sys import os from docopt import docopt -from pkg_resources import load_entry_point import subprocess from ckanapi.version import __version__ @@ -105,7 +104,6 @@ # explicit logger namespace for easy logging handlers log = getLogger('ckan.ckanapi') -PYTHON2 = str is bytes def parse_arguments(): # docopt is awesome @@ -119,11 +117,6 @@ def main(running_with_paster=False): arguments = parse_arguments() if not running_with_paster and not arguments['--remote']: - if PYTHON2: - ckan_ini = os.environ.get('CKAN_INI') - if ckan_ini and not arguments['--config']: - sys.argv[1:1] = ['--config', ckan_ini] - return _switch_to_paster(arguments) return _switch_to_ckan_click(arguments) if arguments['--remote']: From 560bbca16ce943c0c0e73bfd5e6bbb1d4cb16c12 Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 19 Feb 2026 11:50:05 +0100 Subject: [PATCH 02/10] Remove paster support --- ckanapi/cli/ckan_click.py | 2 +- ckanapi/cli/main.py | 13 +----- ckanapi/cli/paster.py | 29 ------------- ckanapi/tests/test_testapp.py | 77 ----------------------------------- setup.py | 3 -- 5 files changed, 3 insertions(+), 121 deletions(-) delete mode 100644 ckanapi/cli/paster.py delete mode 100644 ckanapi/tests/test_testapp.py diff --git a/ckanapi/cli/ckan_click.py b/ckanapi/cli/ckan_click.py index 6c730cb..d406a7d 100644 --- a/ckanapi/cli/ckan_click.py +++ b/ckanapi/cli/ckan_click.py @@ -10,4 +10,4 @@ def api(context, args): from ckanapi.cli.main import main import sys sys.argv[1:] = args - context.exit(main(running_with_paster=True) or 0) + context.exit(main(running_with_ckan_command=True) or 0) diff --git a/ckanapi/cli/main.py b/ckanapi/cli/main.py index 89b029d..b622030 100644 --- a/ckanapi/cli/main.py +++ b/ckanapi/cli/main.py @@ -110,13 +110,13 @@ def parse_arguments(): return docopt(__doc__, version=__version__) -def main(running_with_paster=False): +def main(running_with_ckan_command=False): """ ckanapi command line entry point """ arguments = parse_arguments() - if not running_with_paster and not arguments['--remote']: + if not running_with_ckan_command and not arguments['--remote']: return _switch_to_ckan_click(arguments) if arguments['--remote']: @@ -189,15 +189,6 @@ def main(running_with_paster=False): assert 0, arguments # we shouldn't be here -def _switch_to_paster(arguments): - """ - ** legacy python2-only ** - With --config we switch to the paster command version of the cli - """ - sys.argv[1:1] = ["--plugin=ckanapi", "ckanapi"] - sys.exit(load_entry_point('PasteScript', 'console_scripts', 'paster')()) - - def _switch_to_ckan_click(arguments): """ Local commands must be run through ckan CLI to set up environment diff --git a/ckanapi/cli/paster.py b/ckanapi/cli/paster.py deleted file mode 100644 index c21c967..0000000 --- a/ckanapi/cli/paster.py +++ /dev/null @@ -1,29 +0,0 @@ -import sys -from ckan.lib.cli import CkanCommand - -from ckanapi.cli import main - -class _Options(object): - pass - -class _DelegateParsing(object): - usage = main.__doc__ - - def parse_args(self, args): - assert sys.argv[1:3] == ['--plugin=ckanapi', 'ckanapi'], sys.argv - del sys.argv[1:3] - arguments = main.parse_arguments() - cfg = arguments['--config'] - options = _Options() - options.config = cfg if cfg is not None else './development.ini' - return options, [] - -class CKANAPICommand(CkanCommand): - summary = main.__doc__.split('\n')[0] - usage = main.__doc__ - parser = _DelegateParsing() - - def command(self): - self._load_config() - - return main.main(running_with_paster=True) diff --git a/ckanapi/tests/test_testapp.py b/ckanapi/tests/test_testapp.py deleted file mode 100644 index 533fada..0000000 --- a/ckanapi/tests/test_testapp.py +++ /dev/null @@ -1,77 +0,0 @@ - -import json -from io import BytesIO -try: - import unittest2 as unittest -except ImportError: - import unittest - -import ckanapi - - -UPLOAD_DATA = b""" -public info -""".lstrip() - -def wsgi_app(environ, start_response): - status = '200 OK' - headers = [('Content-type', 'application/json')] - - path = environ['PATH_INFO'] - if path == '/api/action/hello_world': - response = {'success': True, 'result': 'how are you?'} - elif path == '/api/action/invalid': - response = {'success': False, 'error': {'__type': 'Validation Error'}} - elif path == '/api/action/echo': - content = environ['wsgi.input'].read() - response = {'success': True, 'result': - json.loads(content)['message']} - elif path == '/api/action/upload': - # poor man's multipart parsing - data = environ['wsgi.input'].read().split('\r\n') - saw_file = False - for i, s in enumerate(data): - if saw_file: - if s == b'': - break - elif b'filename="f"' in s: - saw_file = True - response = {'success': True, 'result': data[i + 1].decode('ascii')} - elif path == '/api/action/not_ckan': - response = {'success': False, 'error': True} - - start_response(status, headers) - return [json.dumps(response)] - - -class TestTestAPPCKAN(unittest.TestCase): - def setUp(self): - try: - import paste.fixture - except ImportError: - raise unittest.SkipTest('paste not importable') - self.test_app = paste.fixture.TestApp(wsgi_app) - self.ckan = ckanapi.TestAppCKAN(self.test_app) - - def test_simple(self): - self.assertEqual( - self.ckan.action.hello_world(), 'how are you?') - - def test_invalid(self): - self.assertRaises( - ckanapi.ValidationError, - self.ckan.action.invalid) - - def test_data(self): - self.assertEqual( - self.ckan.action.echo(message='for you'), 'for you') - - def test_upload_action(self): - self.assertEqual( - self.ckan.action.upload(package_id='42', - f=BytesIO(UPLOAD_DATA)), 'public info\n') - - def test_not_ckan(self): - self.assertRaises( - ckanapi.ServerIncompatibleError, - self.ckan.action.not_ckan) diff --git a/setup.py b/setup.py index 856e689..cc50e4b 100644 --- a/setup.py +++ b/setup.py @@ -45,9 +45,6 @@ [console_scripts] ckanapi=ckanapi.cli.main:main - [paste.paster_command] - ckanapi=ckanapi.cli.paster:CKANAPICommand - [ckan.click_command] api=ckanapi.cli.ckan_click:api """ From fcfebba1466ae39bf8bc58c1f762a23cb69f6826 Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 19 Feb 2026 11:52:01 +0100 Subject: [PATCH 03/10] Refactor tests to use werkzeug instead of cgi.FieldStorage --- ckanapi/tests/mock/mock_ckan.py | 18 +++++------------- setup.py | 3 ++- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/ckanapi/tests/mock/mock_ckan.py b/ckanapi/tests/mock/mock_ckan.py index 2f717b3..0ad930e 100644 --- a/ckanapi/tests/mock/mock_ckan.py +++ b/ckanapi/tests/mock/mock_ckan.py @@ -1,12 +1,8 @@ import json -import cgi import csv -from wsgiref.util import setup_testing_defaults +from io import StringIO +from werkzeug.formparser import parse_form_data from wsgiref.simple_server import make_server -try: - from cStringIO import StringIO -except ImportError: - from io import StringIO def mock_ckan(environ, start_response): @@ -39,12 +35,8 @@ def mock_ckan(environ, start_response): "result": environ['CONTENT_TYPE'] }).encode('utf-8')] if environ['PATH_INFO'] == '/api/action/test_upload': - fs = cgi.FieldStorage( - fp=environ['wsgi.input'], - environ=environ, - keep_blank_values=True, - ) - upload_data = fs.getvalue('upload').decode('utf-8').splitlines() + _, form, files = parse_form_data(environ) + upload_data = files['upload'].stream.read().decode('utf-8').splitlines() csv_file = StringIO() writer = csv.writer(csv_file) for line_data in upload_data: @@ -57,7 +49,7 @@ def mock_ckan(environ, start_response): "help": "none", "success": True, "result": { - 'option': fs.getvalue('option'), + 'option': form['option'], 'last_row': records[-1], }, }).encode('utf-8')] diff --git a/setup.py b/setup.py index cc50e4b..e0da4a0 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,8 @@ 'simplejson', ] tests_require=[ - 'pyfakefs==5.3.5', + 'pyfakefs==5.10.2', + 'werkzeug', ] From b79e014cc4b0e2aecdd6bd4a0a9fb619690b7ea2 Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 19 Feb 2026 11:55:38 +0100 Subject: [PATCH 04/10] Remove Python 2 imports from tests --- ckanapi/tests/test_call.py | 5 +---- ckanapi/tests/test_cli_action.py | 5 +---- ckanapi/tests/test_cli_dump.py | 5 +---- ckanapi/tests/test_cli_load.py | 5 +---- ckanapi/tests/test_cli_workers.py | 5 +---- ckanapi/tests/test_datapackage.py | 5 +---- ckanapi/tests/test_remote.py | 10 ++-------- 7 files changed, 8 insertions(+), 32 deletions(-) diff --git a/ckanapi/tests/test_call.py b/ckanapi/tests/test_call.py index 4983b13..849c38b 100644 --- a/ckanapi/tests/test_call.py +++ b/ckanapi/tests/test_call.py @@ -1,8 +1,5 @@ import ckanapi -try: - import unittest2 as unittest -except ImportError: - import unittest +import unittest class TestCallAction(unittest.TestCase): diff --git a/ckanapi/tests/test_cli_action.py b/ckanapi/tests/test_cli_action.py index 44355b1..5cab8fa 100644 --- a/ckanapi/tests/test_cli_action.py +++ b/ckanapi/tests/test_cli_action.py @@ -1,9 +1,6 @@ from ckanapi.cli.action import action from ckanapi.errors import CLIError -try: - import unittest2 as unittest -except ImportError: - import unittest +import unittest from io import BytesIO diff --git a/ckanapi/tests/test_cli_dump.py b/ckanapi/tests/test_cli_dump.py index c3ab6a4..68afda4 100644 --- a/ckanapi/tests/test_cli_dump.py +++ b/ckanapi/tests/test_cli_dump.py @@ -5,10 +5,7 @@ import shutil from os.path import exists -try: - import unittest2 as unittest -except ImportError: - import unittest +import unittest from io import BytesIO diff --git a/ckanapi/tests/test_cli_load.py b/ckanapi/tests/test_cli_load.py index af6b132..7926940 100644 --- a/ckanapi/tests/test_cli_load.py +++ b/ckanapi/tests/test_cli_load.py @@ -2,10 +2,7 @@ from ckanapi.errors import NotFound, ValidationError, NotAuthorized import json -try: - import unittest2 as unittest -except ImportError: - import unittest +import unittest from io import BytesIO class MockCKAN(object): diff --git a/ckanapi/tests/test_cli_workers.py b/ckanapi/tests/test_cli_workers.py index cc9a845..eab730c 100644 --- a/ckanapi/tests/test_cli_workers.py +++ b/ckanapi/tests/test_cli_workers.py @@ -1,10 +1,7 @@ from ckanapi.cli.workers import worker_pool import os -try: - import unittest2 as unittest -except ImportError: - import unittest +import unittest class _MockPopen(object): diff --git a/ckanapi/tests/test_datapackage.py b/ckanapi/tests/test_datapackage.py index 28ac98e..3984ab9 100644 --- a/ckanapi/tests/test_datapackage.py +++ b/ckanapi/tests/test_datapackage.py @@ -2,10 +2,7 @@ dataset_to_datapackage, create_resource, create_datapackage, resource_filename, populate_schema_from_datastore) -try: - import unittest2 as unittest -except ImportError: - import unittest +import unittest from io import BytesIO import os from pyfakefs import fake_filesystem_unittest diff --git a/ckanapi/tests/test_remote.py b/ckanapi/tests/test_remote.py index 76186b9..1140680 100644 --- a/ckanapi/tests/test_remote.py +++ b/ckanapi/tests/test_remote.py @@ -6,10 +6,7 @@ import requests from ckanapi import RemoteCKAN, NotFound -try: - import unittest2 as unittest -except ImportError: - import unittest +import unittest try: from subprocess import DEVNULL except ImportError: @@ -18,10 +15,7 @@ from urllib2 import urlopen, URLError except ImportError: from urllib.request import urlopen, URLError -try: - from cStringIO import StringIO -except ImportError: - from io import StringIO +from io import StringIO TEST_CKAN = 'http://localhost:8901' From 73d8d19b4f3dc145f61d205052445fc7b67586f7 Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 19 Feb 2026 12:07:49 +0100 Subject: [PATCH 05/10] Drop support for Python 2 --- ckanapi/cli/action.py | 2 +- ckanapi/cli/batch.py | 13 ++++--------- ckanapi/cli/delete.py | 23 ++++++++--------------- ckanapi/cli/load.py | 20 ++++++-------------- ckanapi/cli/search.py | 2 +- ckanapi/datapackage.py | 3 +-- ckanapi/remoteckan.py | 8 ++------ ckanapi/tests/test_remote.py | 10 ++-------- setup.py | 5 +---- 9 files changed, 26 insertions(+), 60 deletions(-) diff --git a/ckanapi/cli/action.py b/ckanapi/cli/action.py index ceb5eaa..3e6b5de 100644 --- a/ckanapi/cli/action.py +++ b/ckanapi/cli/action.py @@ -27,7 +27,7 @@ def action(ckan, arguments, stdin=None): action_args = {} with open(expanduser(arguments['--input'])) as in_f: action_args = json.loads( - in_f.read().decode('utf-8') if sys.version_info.major == 2 else in_f.read()) + in_f.read()) else: action_args = {} for kv in arguments['KEY=STRING']: diff --git a/ckanapi/cli/batch.py b/ckanapi/cli/batch.py index 855195a..ec7be49 100644 --- a/ckanapi/cli/batch.py +++ b/ckanapi/cli/batch.py @@ -12,11 +12,6 @@ from ckanapi.cli import workers from ckanapi.cli.utils import completion_stats, compact_json, quiet_int_pipe -try: - unicode -except NameError: - unicode = str - def batch_actions(ckan, arguments, worker_pool=None, stdin=None, stdout=None, stderr=None): @@ -143,7 +138,7 @@ def reply(action, error, response): obj = json.loads(line.decode('utf-8')) except UnicodeDecodeError as e: obj = None - reply('read', 'UnicodeDecodeError', unicode(e)) + reply('read', 'UnicodeDecodeError', str(e)) continue requests_kwargs = None @@ -163,7 +158,7 @@ def reply(action, error, response): reply('read', 'IOError', { 'parameter':fkey, 'file_name':fvalue, - 'error':unicode(e.args[1]), + 'error':str(e.args[1]), }) continue @@ -173,9 +168,9 @@ def reply(action, error, response): except ValidationError as e: reply(action, 'ValidationError', e.error_dict) except SearchIndexError as e: - reply(action, 'SearchIndexError', unicode(e)) + reply(action, 'SearchIndexError', str(e)) except NotAuthorized as e: - reply(action, 'NotAuthorized', unicode(e)) + reply(action, 'NotAuthorized', str(e)) except NotFound: reply(action, 'NotFound', obj) else: diff --git a/ckanapi/cli/delete.py b/ckanapi/cli/delete.py index 2d841ec..f4861b5 100644 --- a/ckanapi/cli/delete.py +++ b/ckanapi/cli/delete.py @@ -8,20 +8,13 @@ from datetime import datetime from itertools import chain import re -try: - from urllib.parse import urlparse -except ImportError: - from urlparse import urlparse +from urllib.parse import urlparse from ckanapi.errors import (NotFound, NotAuthorized, ValidationError, SearchIndexError) from ckanapi.cli import workers from ckanapi.cli.utils import completion_stats, compact_json, quiet_int_pipe -try: - unicode -except NameError: - unicode = str def delete_things(ckan, thing, arguments, worker_pool=None, stdin=None, stdout=None, stderr=None): @@ -137,20 +130,20 @@ def extract_ids_or_names(line): except ValueError: return [line.strip()] # 5 if isinstance(j, list) and all( - isinstance(e, unicode) for e in j): + isinstance(e, str) for e in j): return j # 4 - elif isinstance(j, unicode): + elif isinstance(j, str): return [j] # 3 elif isinstance(j, dict): - if 'id' in j and isinstance(j['id'], unicode): + if 'id' in j and isinstance(j['id'], str): return [j['id']] # 1 - if 'name' in j and isinstance(j['name'], unicode): + if 'name' in j and isinstance(j['name'], str): return [j['name']] # 1 again if 'results' in j and isinstance(j['results'], list): out = [] for r in j['results']: if (not isinstance(r, dict) or 'id' not in r or - not isinstance(r['id'], unicode)): + not isinstance(r['id'], str)): break out.append(r['id']) else: @@ -203,7 +196,7 @@ def reply(error, response): try: name = json.loads(line.decode('utf-8')) except UnicodeDecodeError as e: - reply('UnicodeDecodeError', unicode(e)) + reply('UnicodeDecodeError', str(e)) continue try: @@ -213,7 +206,7 @@ def reply(error, response): ckan.call_action(thing_delete, {'id': name}, requests_kwargs=requests_kwargs) except NotAuthorized as e: - reply('NotAuthorized', unicode(e)) + reply('NotAuthorized', str(e)) except NotFound: reply('NotFound', name) else: diff --git a/ckanapi/cli/load.py b/ckanapi/cli/load.py index 3273207..450ab91 100644 --- a/ckanapi/cli/load.py +++ b/ckanapi/cli/load.py @@ -8,21 +8,13 @@ import requests from datetime import datetime import re -try: - from urllib.parse import urlparse -except ImportError: - from urlparse import urlparse +from urllib.parse import urlparse from ckanapi.errors import (NotFound, NotAuthorized, ValidationError, SearchIndexError) from ckanapi.cli import workers from ckanapi.cli.utils import completion_stats, compact_json, quiet_int_pipe -try: - unicode -except NameError: - unicode = str - def load_things(ckan, thing, arguments, worker_pool=None, stdin=None, stdout=None, stderr=None): @@ -167,7 +159,7 @@ def reply(action, error, response): obj = json.loads(line.decode('utf-8')) except UnicodeDecodeError as e: obj = None - reply('read', 'UnicodeDecodeError', unicode(e)) + reply('read', 'UnicodeDecodeError', str(e)) continue requests_kwargs = None @@ -191,7 +183,7 @@ def reply(action, error, response): except NotFound: pass except NotAuthorized as e: - reply('show', 'NotAuthorized', unicode(e)) + reply('show', 'NotAuthorized', str(e)) continue name = obj.get('name') if not existing and name: @@ -201,7 +193,7 @@ def reply(action, error, response): except NotFound: pass except NotAuthorized as e: - reply('show', 'NotAuthorized', unicode(e)) + reply('show', 'NotAuthorized', str(e)) continue if existing: @@ -233,9 +225,9 @@ def reply(action, error, response): except ValidationError as e: reply(act, 'ValidationError', e.error_dict) except SearchIndexError as e: - reply(act, 'SearchIndexError', unicode(e)) + reply(act, 'SearchIndexError', str(e)) except NotAuthorized as e: - reply(act, 'NotAuthorized', unicode(e)) + reply(act, 'NotAuthorized', str(e)) except NotFound: reply(act, 'NotFound', obj) else: diff --git a/ckanapi/cli/search.py b/ckanapi/cli/search.py index a6c2549..27f073d 100644 --- a/ckanapi/cli/search.py +++ b/ckanapi/cli/search.py @@ -35,7 +35,7 @@ def search_datasets(ckan, arguments, stdin=None, stdout=None, stderr=None): action_args = {} with open(expanduser(arguments['--input'])) as in_f: action_args = json.loads( - in_f.read().decode('utf-8') if sys.version_info.major == 2 else in_f.read()) + in_f.read()) else: action_args = {} for kv in arguments['KEY=STRING']: diff --git a/ckanapi/datapackage.py b/ckanapi/datapackage.py index 22f1de1..c9bd1a7 100644 --- a/ckanapi/datapackage.py +++ b/ckanapi/datapackage.py @@ -2,7 +2,6 @@ import requests import json -import six import slugify from ckanapi.cli.utils import pretty_json @@ -170,7 +169,7 @@ def _convert_to_datapackage_resource(resource_dict): resource['name'] = resource_dict['id'] schema = resource_dict.get('schema') - if isinstance(schema, six.string_types): + if isinstance(schema, str): try: resource['schema'] = json.loads(schema) except ValueError: diff --git a/ckanapi/remoteckan.py b/ckanapi/remoteckan.py index 7db7eda..75c4c62 100644 --- a/ckanapi/remoteckan.py +++ b/ckanapi/remoteckan.py @@ -1,9 +1,5 @@ -try: - from urllib2 import Request, urlopen, HTTPError - from urlparse import urlparse -except ImportError: - from urllib.request import Request, urlopen, HTTPError - from urllib.parse import urlparse +from urllib.request import Request, urlopen, HTTPError +from urllib.parse import urlparse from ckanapi.errors import CKANAPIError from ckanapi.common import (ActionShortcut, prepare_action, diff --git a/ckanapi/tests/test_remote.py b/ckanapi/tests/test_remote.py index 1140680..59604bd 100644 --- a/ckanapi/tests/test_remote.py +++ b/ckanapi/tests/test_remote.py @@ -7,14 +7,8 @@ from ckanapi import RemoteCKAN, NotFound import unittest -try: - from subprocess import DEVNULL -except ImportError: - DEVNULL = open(os.devnull, 'wb') -try: - from urllib2 import urlopen, URLError -except ImportError: - from urllib.request import urlopen, URLError +from subprocess import DEVNULL +from urllib.request import urlopen, URLError from io import StringIO TEST_CKAN = 'http://localhost:8901' diff --git a/setup.py b/setup.py index e0da4a0..6d7cfeb 100644 --- a/setup.py +++ b/setup.py @@ -1,16 +1,13 @@ #!/usr/bin/env python from setuptools import setup -import sys install_requires=[ 'setuptools', 'docopt', 'requests', - "python-slugify>=1.0,<5.0; python_version < '3.0'", - "python-slugify>=1.0; python_version >= '3.0'", - 'six>=1.9,<2.0', + 'python-slugify>=1.0', 'simplejson', ] tests_require=[ From 71432f7afc8414099cee99d8489e6be0b1c70c47 Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 19 Feb 2026 12:19:07 +0100 Subject: [PATCH 06/10] Migrate to pyproject.toml --- README.md | 7 +++---- pyproject.toml | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 50 --------------------------------------------- 3 files changed, 58 insertions(+), 54 deletions(-) create mode 100644 pyproject.toml delete mode 100644 setup.py diff --git a/README.md b/README.md index f24b42f..04e7d39 100644 --- a/README.md +++ b/README.md @@ -65,9 +65,8 @@ For this example, we use --insecure as the CKAN demo uses a self-signed certificate. Local CKAN actions may be run by specifying the config file with -c. -If no remote server or config file is specified the CLI will look for -a development.ini file in the current directory, much like paster -commands. +If no remote server or config file is specified, the CLI will look for +a ckan.ini file in the current directory, much like `ckan` commands. Local CKAN actions are performed by the site user (default system administrator) when -u is not specified. @@ -497,7 +496,7 @@ groups = demo.action.group_list(id='data-explorer') To run the tests: - python setup.py test + python -m unittest discover ## License diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6bbf8f0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,55 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "ckanapi" +version = "4.8" +description = "A command line interface and Python module for accessing the CKAN Action API" +license = {text = "MIT"} +authors = [ + {name = "Ian Ward", email = "ian@excess.org"}, +] +classifiers = [ + "Intended Audience :: Developers", + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12" + "Programming Language :: Python :: 3.13" + "Programming Language :: Python :: 3.14" +] +keywords = [ + "ckan", + "ckanext", + "API", +] + +requires-python = ">=3.9" +dependencies = [ + "setuptools", + "docopt", + "requests", + "python-slugify>=1.0", + "simplejson", +] + +[project.urls] +Homepage = "https://github.com/ckan/ckanapi" + +[project.optional-dependencies] +testing = [ + "pyfakefs==5.10.2", + "werkzeug", +] + +[project.scripts] +ckanapi = "ckanapi.cli.main:main" + +[project.entry-points."ckan.click_command"] +api = "ckanapi.cli.ckan_click:api" + +[tool.setuptools.packages.find] +include = ["ckanapi*"] diff --git a/setup.py b/setup.py deleted file mode 100644 index 6d7cfeb..0000000 --- a/setup.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env python - -from setuptools import setup - - -install_requires=[ - 'setuptools', - 'docopt', - 'requests', - 'python-slugify>=1.0', - 'simplejson', -] -tests_require=[ - 'pyfakefs==5.10.2', - 'werkzeug', -] - - -setup( - name='ckanapi', - version='4.8', - description= - 'A command line interface and Python module for ' - 'accessing the CKAN Action API', - license='MIT', - author='Ian Ward', - author_email='ian@excess.org', - url='https://github.com/ckan/ckanapi', - packages=[ - 'ckanapi', - 'ckanapi.tests', - 'ckanapi.tests.mock', - 'ckanapi.cli', - ], - install_requires=install_requires, - extras_require={ - 'testing': tests_require, - }, - tests_require=tests_require, - test_suite='ckanapi.tests', - zip_safe=False, - entry_points = """ - [console_scripts] - ckanapi=ckanapi.cli.main:main - - [ckan.click_command] - api=ckanapi.cli.ckan_click:api - """ - ) - From a51ea689c5fd8adec199af57694e3c2c102bc9e1 Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 19 Feb 2026 12:20:09 +0100 Subject: [PATCH 07/10] Update tested Python versions --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6124b6d..99b95dd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ jobs: test: strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] runs-on: ubuntu-latest container: # INFO: python 2 is no longer supported in From 3288ce61ebb2ec4efcfc2a8730aa584c0c234439 Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 19 Feb 2026 12:21:06 +0100 Subject: [PATCH 08/10] Fix toml syntax --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6bbf8f0..6719db6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,9 +17,9 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12" - "Programming Language :: Python :: 3.13" - "Programming Language :: Python :: 3.14" + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ] keywords = [ "ckan", From fb9c2f2368ab29de35680ab68e9c823371e7de62 Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 19 Feb 2026 12:47:12 +0100 Subject: [PATCH 09/10] Match version to the one on pypi --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6719db6..22ec09d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "ckanapi" -version = "4.8" +version = "4.9" description = "A command line interface and Python module for accessing the CKAN Action API" license = {text = "MIT"} authors = [ From 77bd8514be1803f7aa76f36b3eee72815f7fa41b Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 19 Feb 2026 14:33:48 +0100 Subject: [PATCH 10/10] Add workflow to publish to PyPI --- .github/workflows/publish-pypi.yml | 54 ++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/publish-pypi.yml diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml new file mode 100644 index 0000000..6d7a3b8 --- /dev/null +++ b/.github/workflows/publish-pypi.yml @@ -0,0 +1,54 @@ +name: Publish to PyPI + +# Publish to PyPI when a tag is pushed +on: + push: + tags: + - 'ckanapi-**' + +jobs: + build: + if: github.repository == 'ckan/ckanapi' + name: Build distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install pypa/build + run: python3 -m pip install build --user + - name: Build a binary wheel and a source tarball + run: python3 -m build + - name: Store the distribution packages + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + publish-to-pypi: + name: Publish Python distribution on PyPI + needs: + - build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/ckanapi + permissions: + id-token: write + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Publish distribution to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + + publishSkipped: + if: github.repository != 'ckan/ckanapi' + runs-on: ubuntu-latest + steps: + - run: | + echo "## Skipping PyPI publish on downstream repository" >> $GITHUB_STEP_SUMMARY