diff --git a/ckan/controllers/user.py b/ckan/controllers/user.py index 4deaf17a1a8..7a0cf3389bb 100644 --- a/ckan/controllers/user.py +++ b/ckan/controllers/user.py @@ -103,8 +103,6 @@ def read(self, id=None): c.about_formatted = self._format_about(user_dict['about']) c.user_activity_stream = user_activity_list_html(context, {'id':c.user_dict['id']}) - - c.created_formatted = h.date_str_to_datetime(user_dict['created']).strftime('%b %d, %Y') return render('user/read.html') def me(self): diff --git a/ckan/lib/base.py b/ckan/lib/base.py index 501dc7f7233..753dcc2ffa4 100644 --- a/ckan/lib/base.py +++ b/ckan/lib/base.py @@ -87,6 +87,18 @@ def __before__(self, action, **params): i18n.handle_request(request, c) def _identify_user(self): + ''' + Identifies the user using two methods: + a) If he has logged into the web interface then repoze.who will + set REMOTE_USER. + b) For API calls he may set a header with his API key. + If the user is identified then: + c.user = user name (unicode) + c.author = user name + otherwise: + c.user = None + c.author = user\'s IP address (unicode) + ''' # see if it was proxied first c.remote_addr = request.environ.get('HTTP_X_FORWARDED_FOR', '') if not c.remote_addr: @@ -98,7 +110,7 @@ def _identify_user(self): c.user = c.user.decode('utf8') c.userobj = model.User.by_name(c.user) if c.userobj is None: - # This occurs when you are logged in with openid, clean db + # This occurs when you are logged in, clean db # and then restart i.e. only really for testers. There is no # user object, so even though repoze thinks you are logged in # and your cookie has ckan_display_name, we need to force user diff --git a/ckan/lib/dictization/model_save.py b/ckan/lib/dictization/model_save.py index aa121c0df6e..47a46b8b5a4 100644 --- a/ckan/lib/dictization/model_save.py +++ b/ckan/lib/dictization/model_save.py @@ -286,10 +286,14 @@ def package_dict_save(pkg_dict, context): package_tag_list_save(pkg_dict.get("tags", []), pkg, context) package_membership_list_save(pkg_dict.get("groups", []), pkg, context) - subjects = pkg_dict.get('relationships_as_subject', []) - relationship_list_save(subjects, pkg, 'relationships_as_subject', context) - objects = pkg_dict.get('relationships_as_object', []) - relationship_list_save(subjects, pkg, 'relationships_as_object', context) + # relationships are not considered 'part' of the package, so only + # process this if the key is provided + if 'relationships_as_subject' in pkg_dict: + subjects = pkg_dict.get('relationships_as_subject', []) + relationship_list_save(subjects, pkg, 'relationships_as_subject', context) + if 'relationships_as_object' in pkg_dict: + objects = pkg_dict.get('relationships_as_object', []) + relationship_list_save(objects, pkg, 'relationships_as_object', context) extras = package_extras_save(pkg_dict.get("extras", []), pkg, context) diff --git a/ckan/lib/hash.py b/ckan/lib/hash.py index 8aae27ae3b8..7fa52331912 100644 --- a/ckan/lib/hash.py +++ b/ckan/lib/hash.py @@ -12,7 +12,7 @@ def get_message_hash(value): # avoid getting config value at module scope since config may # not be read in yet secret = config['beaker.session.secret'] - return hmac.new(secret, value, hashlib.sha1).hexdigest() + return hmac.new(secret, value.encode('utf8'), hashlib.sha1).hexdigest() def get_redirect(): '''Checks the return_to value against the hash, and if it diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py index 4f7fc53806a..80926f47e17 100644 --- a/ckan/lib/helpers.py +++ b/ckan/lib/helpers.py @@ -281,11 +281,15 @@ def pager(self, *args, **kwargs): ) return super(Page, self).pager(*args, **kwargs) -def render_datetime(datetime_, date_format='%Y-%m-%d %H:%M'): +def render_datetime(datetime_, date_format=None, with_hours=False): '''Render a datetime object or timestamp string as a pretty string (Y-m-d H:m). If timestamp is badly formatted, then a blank string is returned. ''' + if not date_format: + date_format = '%b %d, %Y' + if with_hours: + date_format += ', %H:%M' if isinstance(datetime_, datetime.datetime): return datetime_.strftime(date_format) elif isinstance(datetime_, basestring): diff --git a/ckan/logic/validators.py b/ckan/logic/validators.py index 4f2392f8543..0101bc726c0 100644 --- a/ckan/logic/validators.py +++ b/ckan/logic/validators.py @@ -1,5 +1,6 @@ -import re import datetime +from itertools import count +import re from pylons.i18n import _, ungettext, N_, gettext from ckan.lib.navl.dictization_functions import Invalid, Missing, missing, unflatten from ckan.authz import Authorizer @@ -268,13 +269,17 @@ def tag_string_convert(key, data, errors, context): '''Takes a list of tags that is a comma-separated string (in data[key]) and parses tag names. These are added to the data dict, enumerated. They are also validated.''' - tag_string = data[key] - tags = [tag.strip() \ - for tag in tag_string.split(',') \ - if tag.strip()] + if isinstance(data[key], basestring): + tags = [tag.strip() \ + for tag in data[key].split(',') \ + if tag.strip()] + else: + tags = data[key] + + current_index = max( [int(k[1]) for k in data.keys() if len(k) == 3 and k[0] == 'tags'] + [-1] ) - for num, tag in enumerate(tags): + for num, tag in zip(count(current_index+1), tags): data[('tags', num, 'name')] = tag for tag in tags: diff --git a/ckan/public/css/style.css b/ckan/public/css/style.css index 140335ac9ff..33640224e24 100644 --- a/ckan/public/css/style.css +++ b/ckan/public/css/style.css @@ -1527,30 +1527,30 @@ body.authz form button { /* = Activity Streams = */ /* ==================== */ -.activity-stream-activity { +.activity-stream .activity { padding-bottom:1em; } -.activity-stream-activity a { +.activity-stream .activity a { font-weight:bold; } -.activity-stream-activity .actor { +.activity-stream .activity .actor { } -.activity-stream-activity .verb { +.activity-stream .activity .verb { background-color:PapayaWhip; padding:.25em; margin:.25em; } -.activity-stream-activity .object { +.activity-stream .activity .object { } -.activity-stream-activity .target { +.activity-stream .activity .target { } -.activity-stream-activity .date { +.activity-stream .activity .date { color:#999; } diff --git a/ckan/templates/_util.html b/ckan/templates/_util.html index 6142b80ceda..9e8a7fce367 100644 --- a/ckan/templates/_util.html +++ b/ckan/templates/_util.html @@ -384,7 +384,7 @@ - ${h.render_datetime(revision.timestamp)} + ${h.render_datetime(revision.timestamp, with_hours=True)} ${h.linked_user(revision.author)} @@ -451,8 +451,8 @@ -
+
${actor} @@ -474,7 +474,7 @@ - ${h.render_datetime(activity.timestamp, '%B %d %Y')} + ${h.render_datetime(activity.timestamp)}
diff --git a/ckan/templates/activity_streams/added_tag.html b/ckan/templates/activity_streams/added_tag.html index c90d7b18d62..7a92899dbac 100644 --- a/ckan/templates/activity_streams/added_tag.html +++ b/ckan/templates/activity_streams/added_tag.html @@ -6,7 +6,7 @@ py:strip="" > -${activity_stream_activity( +${activity_div( actor=h.linked_user(activity.user_id), verb='added', object="the tag "+h.tag_link(detail.data.tag), diff --git a/ckan/templates/activity_streams/changed_group.html b/ckan/templates/activity_streams/changed_group.html index 0aba79a7f36..32e8d87f9b1 100644 --- a/ckan/templates/activity_streams/changed_group.html +++ b/ckan/templates/activity_streams/changed_group.html @@ -6,7 +6,7 @@ py:strip="" > -${activity_stream_activity( +${activity_div( actor=h.linked_user(activity.user_id), verb='updated', object="the group "+h.group_link(activity.data.group), diff --git a/ckan/templates/activity_streams/changed_package.html b/ckan/templates/activity_streams/changed_package.html index 94faf4fc8fd..e80c83dab7b 100644 --- a/ckan/templates/activity_streams/changed_package.html +++ b/ckan/templates/activity_streams/changed_package.html @@ -6,7 +6,7 @@ py:strip="" > -${activity_stream_activity( +${activity_div( actor=h.linked_user(activity.user_id), verb='updated', object="the dataset "+h.dataset_link(activity.data.package), diff --git a/ckan/templates/activity_streams/changed_package_extra.html b/ckan/templates/activity_streams/changed_package_extra.html index 8d3a1c43837..6f213e7a597 100644 --- a/ckan/templates/activity_streams/changed_package_extra.html +++ b/ckan/templates/activity_streams/changed_package_extra.html @@ -6,7 +6,7 @@ py:strip="" > -${activity_stream_activity( +${activity_div( actor=h.linked_user(activity.user_id), verb='changed', object='the extra "'+detail.data.package_extra.key+'"', diff --git a/ckan/templates/activity_streams/changed_resource.html b/ckan/templates/activity_streams/changed_resource.html index 4e374fca74b..576ab16df9a 100644 --- a/ckan/templates/activity_streams/changed_resource.html +++ b/ckan/templates/activity_streams/changed_resource.html @@ -6,7 +6,7 @@ py:strip="" > -${activity_stream_activity( +${activity_div( actor=h.linked_user(activity.user_id), verb='updated', object='the resource '+h.resource_link(detail.data.resource, diff --git a/ckan/templates/activity_streams/changed_user.html b/ckan/templates/activity_streams/changed_user.html index f33395ac7d2..3205f365368 100644 --- a/ckan/templates/activity_streams/changed_user.html +++ b/ckan/templates/activity_streams/changed_user.html @@ -6,7 +6,7 @@ py:strip="" > -${activity_stream_activity( +${activity_div( actor=h.linked_user(activity.user_id), verb='updated their profile.', activity=activity diff --git a/ckan/templates/activity_streams/deleted_group.html b/ckan/templates/activity_streams/deleted_group.html index c7a21046bfd..66f45550f9b 100644 --- a/ckan/templates/activity_streams/deleted_group.html +++ b/ckan/templates/activity_streams/deleted_group.html @@ -6,10 +6,10 @@ py:strip="" > -${activity_stream_activity( +${activity_div( actor=h.linked_user(activity.user_id), verb='deleted', - object="the group "+h.group_link(activity.data.group), + object="the group "+activity.data.group.name, activity=activity )} diff --git a/ckan/templates/activity_streams/deleted_package.html b/ckan/templates/activity_streams/deleted_package.html index 0c56596dfb8..4413e645017 100644 --- a/ckan/templates/activity_streams/deleted_package.html +++ b/ckan/templates/activity_streams/deleted_package.html @@ -6,9 +6,9 @@ py:strip="" > -${activity_stream_activity( +${activity_div( actor=h.linked_user(activity.user_id), verb='deleted', - object="the dataset "+h.dataset_link(activity.data.package), + object="the dataset "+h.dataset_display_name(activity.data.package), activity=activity)} diff --git a/ckan/templates/activity_streams/deleted_package_extra.html b/ckan/templates/activity_streams/deleted_package_extra.html index 5f2bc972841..abc43eead81 100644 --- a/ckan/templates/activity_streams/deleted_package_extra.html +++ b/ckan/templates/activity_streams/deleted_package_extra.html @@ -6,7 +6,7 @@ py:strip="" > -${activity_stream_activity( +${activity_div( actor=h.linked_user(activity.user_id), verb='deleted', object='the extra "'+detail.data.package_extra.key+'"', diff --git a/ckan/templates/activity_streams/deleted_resource.html b/ckan/templates/activity_streams/deleted_resource.html index 29fb0a3bb2d..09e3233f816 100644 --- a/ckan/templates/activity_streams/deleted_resource.html +++ b/ckan/templates/activity_streams/deleted_resource.html @@ -6,11 +6,10 @@ py:strip="" > -${activity_stream_activity( +${activity_div( actor=h.linked_user(activity.user_id), verb='deleted', - object='the resource '+h.resource_link(detail.data.resource, - activity.data.package.id), + object='the resource '+h.resource_display_name(detail.data.resource), target='from the dataset '+h.dataset_link(activity.data.package), activity=activity )} diff --git a/ckan/templates/activity_streams/new_group.html b/ckan/templates/activity_streams/new_group.html index 0f5cb35645c..9ff83bab89a 100644 --- a/ckan/templates/activity_streams/new_group.html +++ b/ckan/templates/activity_streams/new_group.html @@ -6,7 +6,7 @@ py:strip="" > -${activity_stream_activity( +${activity_div( actor=h.linked_user(activity.user_id), verb='created', object="the group "+h.group_link(activity.data.group), diff --git a/ckan/templates/activity_streams/new_package.html b/ckan/templates/activity_streams/new_package.html index cce9cfa23af..b09f3ee4dc2 100644 --- a/ckan/templates/activity_streams/new_package.html +++ b/ckan/templates/activity_streams/new_package.html @@ -6,7 +6,7 @@ py:strip="" > -${activity_stream_activity( +${activity_div( actor=h.linked_user(activity.user_id), verb='created', object="the dataset "+h.dataset_link(activity.data.package), diff --git a/ckan/templates/activity_streams/new_package_extra.html b/ckan/templates/activity_streams/new_package_extra.html index 550f412847f..a811dd656ea 100644 --- a/ckan/templates/activity_streams/new_package_extra.html +++ b/ckan/templates/activity_streams/new_package_extra.html @@ -6,7 +6,7 @@ py:strip="" > -${activity_stream_activity( +${activity_div( actor=h.linked_user(activity.user_id), verb='added', object='the extra "'+detail.data.package_extra.key+'"', diff --git a/ckan/templates/activity_streams/new_resource.html b/ckan/templates/activity_streams/new_resource.html index 46e1e805920..a86f60138e9 100644 --- a/ckan/templates/activity_streams/new_resource.html +++ b/ckan/templates/activity_streams/new_resource.html @@ -6,7 +6,7 @@ py:strip="" > -${activity_stream_activity( +${activity_div( actor=h.linked_user(activity.user_id), verb='added', object='the resource '+h.resource_link(detail.data.resource, diff --git a/ckan/templates/activity_streams/new_user.html b/ckan/templates/activity_streams/new_user.html index aef2cc82289..e480e1d7149 100644 --- a/ckan/templates/activity_streams/new_user.html +++ b/ckan/templates/activity_streams/new_user.html @@ -6,7 +6,7 @@ py:strip="" > -${activity_stream_activity( +${activity_div( actor=h.linked_user(activity.user_id), verb='signed up.', activity=activity diff --git a/ckan/templates/activity_streams/removed_tag.html b/ckan/templates/activity_streams/removed_tag.html index 6376caf7861..9b2eef3d2f8 100644 --- a/ckan/templates/activity_streams/removed_tag.html +++ b/ckan/templates/activity_streams/removed_tag.html @@ -6,7 +6,7 @@ py:strip="" > -${activity_stream_activity( +${activity_div( actor=h.linked_user(activity.user_id), verb='removed', object="the tag "+h.tag_link(detail.data.tag), diff --git a/ckan/templates/package/history.html b/ckan/templates/package/history.html index c35f97f50ba..595835aa122 100644 --- a/ckan/templates/package/history.html +++ b/ckan/templates/package/history.html @@ -47,7 +47,7 @@

${rev['id'][:4]}… - ${h.render_datetime(rev['timestamp'])} + ${h.render_datetime(rev['timestamp'], with_hours=True)} ${h.linked_user(rev['author'])} ${rev['message']} diff --git a/ckan/templates/package/read.html b/ckan/templates/package/read.html index c5dad62261e..2682e9f7d4d 100644 --- a/ckan/templates/package/read.html +++ b/ckan/templates/package/read.html @@ -84,8 +84,8 @@

Related Datasets

-

This is an old revision of this dataset, as edited at ${h.render_datetime(c.pkg_revision_timestamp)}. It may differ significantly from the current revision.

-

This is the current revision of this dataset, as edited at ${h.render_datetime(c.pkg_revision_timestamp)}.

+

This is an old revision of this dataset, as edited at ${h.render_datetime(c.pkg_revision_timestamp, with_hours=True)}. It may differ significantly from the current revision.

+

This is the current revision of this dataset, as edited at ${h.render_datetime(c.pkg_revision_timestamp, with_hours=True)}.

diff --git a/ckan/templates/user/read.html b/ckan/templates/user/read.html index ed5fb1661b9..318b9f12321 100644 --- a/ckan/templates/user/read.html +++ b/ckan/templates/user/read.html @@ -31,8 +31,8 @@
Member since
-
${c.created_formatted}
- +
${h.render_datetime(c.user_dict['created'])}
+
About
${c.about_formatted}
@@ -69,7 +69,9 @@

Datasets

Public Activity

- ${c.user_activity_stream} +
+ ${c.user_activity_stream} +
diff --git a/ckan/tests/functional/api/base.py b/ckan/tests/functional/api/base.py index eb28a343b53..82165d0dcf4 100644 --- a/ckan/tests/functional/api/base.py +++ b/ckan/tests/functional/api/base.py @@ -278,13 +278,6 @@ def _ref_group(cls, group): assert cls.ref_group_by in ['id', 'name'] return getattr(group, cls.ref_group_by) - @classmethod - def _list_package_refs(cls, packages): - return [getattr(p, cls.ref_package_by) for p in packages] - - @classmethod - def _list_group_refs(cls, groups): - return [getattr(p, cls.ref_group_by) for p in groups] class Api1TestCase(Api1and2TestCase): diff --git a/ckan/tests/functional/test_activity.py b/ckan/tests/functional/test_activity.py index 796c44bd6d3..fcc9d317c07 100644 --- a/ckan/tests/functional/test_activity.py +++ b/ckan/tests/functional/test_activity.py @@ -201,5 +201,5 @@ def test_activity(self): # By now we've created >15 activities, but only the latest 15 should # appear on the page. result = self.app.get(offset, status=200) - assert result.body.count('
') \ + assert result.body.count('
') \ == 15 diff --git a/ckan/tests/functional/test_package.py b/ckan/tests/functional/test_package.py index 81c67d93a2d..e789b594a5c 100644 --- a/ckan/tests/functional/test_package.py +++ b/ckan/tests/functional/test_package.py @@ -503,7 +503,7 @@ def test_read_revision1(self): side_html = self.named_div('sidebar', res) print 'MAIN', main_html assert 'This is an old revision of this dataset' in main_html - assert 'at 2011-01-01 00:00' in main_html + assert 'at Jan 01, 2011, 00:00' in main_html self.check_named_element(main_html, 'a', 'href="/dataset/%s"' % self.pkg_name) print 'PKG', pkg_html assert 'title1' in res @@ -521,7 +521,7 @@ def test_read_revision2(self): side_html = self.named_div('sidebar', res) print 'MAIN', main_html assert 'This is an old revision of this dataset' in main_html - assert 'at 2011-01-02 00:00' in main_html + assert 'at Jan 02, 2011, 00:00' in main_html self.check_named_element(main_html, 'a', 'href="/dataset/%s"' % self.pkg_name) print 'PKG', pkg_html assert 'title2' in res @@ -540,7 +540,7 @@ def test_read_revision3(self): print 'MAIN', main_html assert 'This is an old revision of this dataset' not in main_html assert 'This is the current revision of this dataset' in main_html - assert 'at 2011-01-03 00:00' in main_html + assert 'at Jan 03, 2011, 00:00' in main_html self.check_named_element(main_html, 'a', 'href="/dataset/%s"' % self.pkg_name) print 'PKG', pkg_html assert 'title3' in res @@ -1016,6 +1016,33 @@ def test_edit_indexerror(self): plugins.unload('synchronous_search') SolrSettings.init(solr_url) + def test_edit_pkg_with_relationships(self): + # 1786 + try: + # add a relationship to a package + pkg = model.Package.by_name(self.editpkg_name) + anna = model.Package.by_name(u'annakarenina') + model.repo.new_revision() + pkg.add_relationship(u'depends_on', anna) + model.repo.commit_and_remove() + + # check relationship before the test + rels = model.Package.by_name(self.editpkg_name).get_relationships() + assert_equal(str(rels), '[<*PackageRelationship editpkgtest depends_on annakarenina>]') + + # edit the package + self.offset = url_for(controller='package', action='edit', id=self.editpkg_name) + self.res = self.app.get(self.offset) + fv = self.res.forms['dataset-edit'] + fv['title'] = u'New Title' + res = fv.submit('save') + + # check relationship still exists + rels = model.Package.by_name(self.editpkg_name).get_relationships() + assert_equal(str(rels), '[<*PackageRelationship editpkgtest depends_on annakarenina>]') + + finally: + self._reset_data() class TestNew(TestPackageForm): pkg_names = [] @@ -1421,7 +1448,6 @@ def test_4_history_revision_package_link(self): res = res.click(href=url) main_html = self.main_div(res) assert 'This is an old revision of this dataset' in main_html - assert 'at %s' % str(self.revision_timestamps[1])[:6] in main_html class TestMarkdownHtmlWhitelist(TestPackageForm): diff --git a/ckan/tests/lib/test_hash.py b/ckan/tests/lib/test_hash.py new file mode 100644 index 00000000000..e24dd19b2b3 --- /dev/null +++ b/ckan/tests/lib/test_hash.py @@ -0,0 +1,16 @@ +from nose.tools import assert_equals + +from ckan.lib.hash import get_message_hash, get_redirect + +class TestHash: + @classmethod + def setup_class(cls): + global secret + secret = '42' # so that these tests are repeatable + + def test_get_message_hash(self): + assert_equals(len(get_message_hash(u'/tag/country-uk')), len('6f58ff51b42e6b2d2e700abd1a14c9699e115c61')) + + def test_get_message_hash_unicode(self): + assert_equals(len(get_message_hash(u'/tag/biocombust\xedveis')), len('d748fa890eb6a964cd317e6ff62905fad645b43d')) + diff --git a/ckan/tests/lib/test_helpers.py b/ckan/tests/lib/test_helpers.py index da44820867b..2500c64b69e 100644 --- a/ckan/tests/lib/test_helpers.py +++ b/ckan/tests/lib/test_helpers.py @@ -22,11 +22,11 @@ def test_extract_markdown(self): def test_render_datetime(self): res = h.render_datetime(datetime.datetime(2008, 4, 13, 20, 40, 20, 123456)) - assert_equal(res, '2008-04-13 20:40') + assert_equal(res, 'Apr 13, 2008') def test_render_datetime_but_from_string(self): res = h.render_datetime('2008-04-13T20:40:20.123456') - assert_equal(res, '2008-04-13 20:40') + assert_equal(res, 'Apr 13, 2008') def test_render_datetime_blank(self): res = h.render_datetime(None) diff --git a/doc/apiv3.rst b/doc/apiv3.rst index 6843628ed7d..99b1b816f47 100644 --- a/doc/apiv3.rst +++ b/doc/apiv3.rst @@ -150,7 +150,7 @@ license_id "cc-by" ID of the licens extras [] tags ["government-spending"] List of tags associated with this dataset. groups ["spending", "country-uk"] List of groups this dataset is a member of. -relationships_as_subject [] List of relationships (edit this only using relationship specific command). The 'type' of the relationship is described in terms of this package being the subject and the related package being the object. +relationships_as_subject [] List of relationships. The 'type' of the relationship is described in terms of this package being the subject and the related package being the object. state active May be ``deleted`` or other custom states like ``pending``. revision_id "f645243a-7334-44e2-b87c-64231700a9a6" (Read-only) ID of the last revision for the core package object was (doesn't include tags, groups, extra fields, relationships). revision_timestamp "2010-12-21T15:26:17.345502" (Read-only) Time and date when the last revision for the core package object was (doesn't include tags, groups, extra fields, relationships). ISO format. UTC timezone assumed. diff --git a/doc/prepare-extensions.rst b/doc/prepare-extensions.rst index b0fa0856119..496c54c54d9 100644 --- a/doc/prepare-extensions.rst +++ b/doc/prepare-extensions.rst @@ -8,7 +8,9 @@ Firstly, you'll need to set up and enter a virtual Python environment, as follow :: - sudo apt-get install python-virtualenv mercurial + # install software we need (virtualenv and git to retrieve the source code) + sudo apt-get install python-virtualenv git-core + # create a python virtual env and activate virtualenv /home/ubuntu/pyenv . /home/ubuntu/pyenv/bin/activate diff --git a/requires/lucid_missing.txt b/requires/lucid_missing.txt index 89e2eb8cac7..a5ed111fdf0 100644 --- a/requires/lucid_missing.txt +++ b/requires/lucid_missing.txt @@ -15,7 +15,7 @@ solrpy==0.9.4 formalchemy==1.4.1 pairtree==0.7.1-T -ofs>=0.4.1 +ofs==0.4.1 apachemiddleware==0.1.1 licenses==0.6.1