kinto 23.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. kinto/__init__.py +92 -0
  2. kinto/__main__.py +249 -0
  3. kinto/authorization.py +134 -0
  4. kinto/config/__init__.py +94 -0
  5. kinto/config/kinto.tpl +270 -0
  6. kinto/contribute.json +27 -0
  7. kinto/core/__init__.py +246 -0
  8. kinto/core/authentication.py +48 -0
  9. kinto/core/authorization.py +311 -0
  10. kinto/core/cache/__init__.py +131 -0
  11. kinto/core/cache/memcached.py +112 -0
  12. kinto/core/cache/memory.py +104 -0
  13. kinto/core/cache/postgresql/__init__.py +178 -0
  14. kinto/core/cache/postgresql/schema.sql +23 -0
  15. kinto/core/cache/testing.py +208 -0
  16. kinto/core/cornice/__init__.py +93 -0
  17. kinto/core/cornice/cors.py +144 -0
  18. kinto/core/cornice/errors.py +40 -0
  19. kinto/core/cornice/pyramidhook.py +373 -0
  20. kinto/core/cornice/renderer.py +89 -0
  21. kinto/core/cornice/resource.py +205 -0
  22. kinto/core/cornice/service.py +641 -0
  23. kinto/core/cornice/util.py +138 -0
  24. kinto/core/cornice/validators/__init__.py +94 -0
  25. kinto/core/cornice/validators/_colander.py +142 -0
  26. kinto/core/cornice/validators/_marshmallow.py +182 -0
  27. kinto/core/cornice_swagger/__init__.py +92 -0
  28. kinto/core/cornice_swagger/converters/__init__.py +21 -0
  29. kinto/core/cornice_swagger/converters/exceptions.py +6 -0
  30. kinto/core/cornice_swagger/converters/parameters.py +90 -0
  31. kinto/core/cornice_swagger/converters/schema.py +249 -0
  32. kinto/core/cornice_swagger/swagger.py +725 -0
  33. kinto/core/cornice_swagger/templates/index.html +73 -0
  34. kinto/core/cornice_swagger/templates/index_script_template.html +21 -0
  35. kinto/core/cornice_swagger/util.py +42 -0
  36. kinto/core/cornice_swagger/views.py +78 -0
  37. kinto/core/decorators.py +74 -0
  38. kinto/core/errors.py +216 -0
  39. kinto/core/events.py +301 -0
  40. kinto/core/initialization.py +738 -0
  41. kinto/core/listeners/__init__.py +9 -0
  42. kinto/core/metrics.py +94 -0
  43. kinto/core/openapi.py +115 -0
  44. kinto/core/permission/__init__.py +202 -0
  45. kinto/core/permission/memory.py +167 -0
  46. kinto/core/permission/postgresql/__init__.py +489 -0
  47. kinto/core/permission/postgresql/migrations/migration_001_002.sql +18 -0
  48. kinto/core/permission/postgresql/schema.sql +41 -0
  49. kinto/core/permission/testing.py +487 -0
  50. kinto/core/resource/__init__.py +1311 -0
  51. kinto/core/resource/model.py +412 -0
  52. kinto/core/resource/schema.py +502 -0
  53. kinto/core/resource/viewset.py +230 -0
  54. kinto/core/schema.py +119 -0
  55. kinto/core/scripts.py +50 -0
  56. kinto/core/statsd.py +1 -0
  57. kinto/core/storage/__init__.py +436 -0
  58. kinto/core/storage/exceptions.py +53 -0
  59. kinto/core/storage/generators.py +58 -0
  60. kinto/core/storage/memory.py +651 -0
  61. kinto/core/storage/postgresql/__init__.py +1131 -0
  62. kinto/core/storage/postgresql/client.py +120 -0
  63. kinto/core/storage/postgresql/migrations/migration_001_002.sql +10 -0
  64. kinto/core/storage/postgresql/migrations/migration_002_003.sql +33 -0
  65. kinto/core/storage/postgresql/migrations/migration_003_004.sql +18 -0
  66. kinto/core/storage/postgresql/migrations/migration_004_005.sql +20 -0
  67. kinto/core/storage/postgresql/migrations/migration_005_006.sql +11 -0
  68. kinto/core/storage/postgresql/migrations/migration_006_007.sql +74 -0
  69. kinto/core/storage/postgresql/migrations/migration_007_008.sql +66 -0
  70. kinto/core/storage/postgresql/migrations/migration_008_009.sql +41 -0
  71. kinto/core/storage/postgresql/migrations/migration_009_010.sql +98 -0
  72. kinto/core/storage/postgresql/migrations/migration_010_011.sql +14 -0
  73. kinto/core/storage/postgresql/migrations/migration_011_012.sql +9 -0
  74. kinto/core/storage/postgresql/migrations/migration_012_013.sql +71 -0
  75. kinto/core/storage/postgresql/migrations/migration_013_014.sql +14 -0
  76. kinto/core/storage/postgresql/migrations/migration_014_015.sql +95 -0
  77. kinto/core/storage/postgresql/migrations/migration_015_016.sql +4 -0
  78. kinto/core/storage/postgresql/migrations/migration_016_017.sql +81 -0
  79. kinto/core/storage/postgresql/migrations/migration_017_018.sql +25 -0
  80. kinto/core/storage/postgresql/migrations/migration_018_019.sql +8 -0
  81. kinto/core/storage/postgresql/migrations/migration_019_020.sql +7 -0
  82. kinto/core/storage/postgresql/migrations/migration_020_021.sql +68 -0
  83. kinto/core/storage/postgresql/migrations/migration_021_022.sql +62 -0
  84. kinto/core/storage/postgresql/migrations/migration_022_023.sql +5 -0
  85. kinto/core/storage/postgresql/migrations/migration_023_024.sql +6 -0
  86. kinto/core/storage/postgresql/migrations/migration_024_025.sql +6 -0
  87. kinto/core/storage/postgresql/migrator.py +98 -0
  88. kinto/core/storage/postgresql/pool.py +55 -0
  89. kinto/core/storage/postgresql/schema.sql +143 -0
  90. kinto/core/storage/testing.py +1857 -0
  91. kinto/core/storage/utils.py +37 -0
  92. kinto/core/testing.py +182 -0
  93. kinto/core/utils.py +553 -0
  94. kinto/core/views/__init__.py +0 -0
  95. kinto/core/views/batch.py +163 -0
  96. kinto/core/views/errors.py +145 -0
  97. kinto/core/views/heartbeat.py +106 -0
  98. kinto/core/views/hello.py +69 -0
  99. kinto/core/views/openapi.py +35 -0
  100. kinto/core/views/version.py +50 -0
  101. kinto/events.py +3 -0
  102. kinto/plugins/__init__.py +0 -0
  103. kinto/plugins/accounts/__init__.py +94 -0
  104. kinto/plugins/accounts/authentication.py +63 -0
  105. kinto/plugins/accounts/scripts.py +61 -0
  106. kinto/plugins/accounts/utils.py +13 -0
  107. kinto/plugins/accounts/views.py +136 -0
  108. kinto/plugins/admin/README.md +3 -0
  109. kinto/plugins/admin/VERSION +1 -0
  110. kinto/plugins/admin/__init__.py +40 -0
  111. kinto/plugins/admin/build/VERSION +1 -0
  112. kinto/plugins/admin/build/assets/index-CYFwtKtL.css +6 -0
  113. kinto/plugins/admin/build/assets/index-DJ0m93zA.js +149 -0
  114. kinto/plugins/admin/build/assets/logo-VBRiKSPX.png +0 -0
  115. kinto/plugins/admin/build/index.html +18 -0
  116. kinto/plugins/admin/public/help.html +25 -0
  117. kinto/plugins/admin/views.py +42 -0
  118. kinto/plugins/default_bucket/__init__.py +191 -0
  119. kinto/plugins/flush.py +28 -0
  120. kinto/plugins/history/__init__.py +65 -0
  121. kinto/plugins/history/listener.py +181 -0
  122. kinto/plugins/history/views.py +66 -0
  123. kinto/plugins/openid/__init__.py +131 -0
  124. kinto/plugins/openid/utils.py +14 -0
  125. kinto/plugins/openid/views.py +193 -0
  126. kinto/plugins/prometheus.py +300 -0
  127. kinto/plugins/statsd.py +85 -0
  128. kinto/schema_validation.py +135 -0
  129. kinto/views/__init__.py +34 -0
  130. kinto/views/admin.py +195 -0
  131. kinto/views/buckets.py +45 -0
  132. kinto/views/collections.py +58 -0
  133. kinto/views/contribute.py +39 -0
  134. kinto/views/groups.py +90 -0
  135. kinto/views/permissions.py +235 -0
  136. kinto/views/records.py +133 -0
  137. kinto-23.2.1.dist-info/METADATA +232 -0
  138. kinto-23.2.1.dist-info/RECORD +142 -0
  139. kinto-23.2.1.dist-info/WHEEL +5 -0
  140. kinto-23.2.1.dist-info/entry_points.txt +5 -0
  141. kinto-23.2.1.dist-info/licenses/LICENSE +13 -0
  142. kinto-23.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,178 @@
1
+ import logging
2
+ import os
3
+
4
+ from kinto.core.cache import CacheBase
5
+ from kinto.core.storage.postgresql.client import create_from_config
6
+ from kinto.core.utils import json
7
+ from kinto.core.utils import sqlalchemy as sa
8
+
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class Cache(CacheBase):
14
+ """Cache backend using PostgreSQL.
15
+
16
+ Enable in configuration::
17
+
18
+ kinto.cache_backend = kinto.core.cache.postgresql
19
+
20
+ Database location URI can be customized::
21
+
22
+ kinto.cache_url = postgresql://user:pass@db.server.lan:5432/dbname
23
+
24
+ Alternatively, username and password could also rely on system user ident
25
+ or even specified in :file:`~/.pgpass` (*see PostgreSQL documentation*).
26
+
27
+ .. note::
28
+
29
+ Some tables and indices are created when ``kinto migrate`` is run.
30
+ This requires some privileges on the database, or some error will
31
+ be raised.
32
+
33
+ **Alternatively**, the schema can be initialized outside the
34
+ python application, using the SQL file located in
35
+ :file:`kinto/core/cache/postgresql/schema.sql`. This allows to
36
+ distinguish schema manipulation privileges from schema usage.
37
+
38
+
39
+ A connection pool is enabled by default::
40
+
41
+ kinto.cache_pool_size = 10
42
+ kinto.cache_maxoverflow = 10
43
+ kinto.cache_max_backlog = -1
44
+ kinto.cache_pool_recycle = -1
45
+ kinto.cache_pool_timeout = 30
46
+ kinto.cache_poolclass =
47
+ kinto.core.storage.postgresql.pool.QueuePoolWithMaxBacklog
48
+
49
+ The ``max_backlog`` limits the number of threads that can be in the queue
50
+ waiting for a connection. Once this limit has been reached, any further
51
+ attempts to acquire a connection will be rejected immediately, instead of
52
+ locking up all threads by keeping them waiting in the queue.
53
+
54
+ See `dedicated section in SQLAlchemy documentation
55
+ <http://docs.sqlalchemy.org/en/rel_1_0/core/engines.html>`_
56
+ for default values and behaviour.
57
+
58
+ .. note::
59
+
60
+ Using a `dedicated connection pool <http://pgpool.net>`_ is still
61
+ recommended to allow load balancing, replication or limit the number
62
+ of connections used in a multi-process deployment.
63
+
64
+ :noindex:
65
+ """ # NOQA
66
+
67
+ def __init__(self, client, *args, **kwargs):
68
+ super().__init__(*args, **kwargs)
69
+ self.client = client
70
+
71
+ def initialize_schema(self, dry_run=False):
72
+ # Check if cache table exists.
73
+ query = """
74
+ SELECT 1
75
+ FROM information_schema.tables
76
+ WHERE table_name = 'cache';
77
+ """
78
+ with self.client.connect(readonly=True) as conn:
79
+ result = conn.execute(sa.text(query))
80
+ if result.rowcount > 0:
81
+ logger.info("PostgreSQL cache schema is up-to-date.")
82
+ return
83
+
84
+ # Create schema
85
+ here = os.path.dirname(__file__)
86
+ sql_file = os.path.join(here, "schema.sql")
87
+
88
+ if dry_run:
89
+ logger.info(f"Create cache schema from '{sql_file}'")
90
+ return
91
+
92
+ # Since called outside request, force commit.
93
+ with open(sql_file) as f:
94
+ schema = f.read()
95
+ with self.client.connect(force_commit=True) as conn:
96
+ conn.execute(sa.text(schema))
97
+ logger.info("Created PostgreSQL cache tables")
98
+
99
+ def flush(self):
100
+ query = """
101
+ DELETE FROM cache;
102
+ """
103
+ # Since called outside request (e.g. tests), force commit.
104
+ with self.client.connect(force_commit=True) as conn:
105
+ conn.execute(sa.text(query))
106
+ logger.debug("Flushed PostgreSQL cache tables")
107
+
108
+ def ttl(self, key):
109
+ query = """
110
+ SELECT EXTRACT(SECOND FROM (ttl - now())) AS ttl
111
+ FROM cache
112
+ WHERE key = :key
113
+ AND ttl IS NOT NULL;
114
+ """
115
+ with self.client.connect(readonly=True) as conn:
116
+ result = conn.execute(sa.text(query), dict(key=self.prefix + key))
117
+ if result.rowcount > 0:
118
+ return result.fetchone().ttl
119
+ return -1
120
+
121
+ def expire(self, key, ttl):
122
+ query = """
123
+ UPDATE cache SET ttl = sec2ttl(:ttl) WHERE key = :key;
124
+ """
125
+ with self.client.connect() as conn:
126
+ conn.execute(sa.text(query), dict(ttl=ttl, key=self.prefix + key))
127
+
128
+ def set(self, key, value, ttl):
129
+ if isinstance(value, bytes):
130
+ raise TypeError("a string-like object is required, not 'bytes'")
131
+
132
+ query = """
133
+ INSERT INTO cache (key, value, ttl)
134
+ VALUES (:key, :value, sec2ttl(:ttl))
135
+ ON CONFLICT (key) DO UPDATE
136
+ SET value = :value,
137
+ ttl = sec2ttl(:ttl);
138
+ """
139
+ value = json.dumps(value)
140
+ with self.client.connect() as conn:
141
+ conn.execute(sa.text(query), dict(key=self.prefix + key, value=value, ttl=ttl))
142
+
143
+ def get(self, key):
144
+ purge = """
145
+ DELETE FROM cache c
146
+ USING (
147
+ SELECT key
148
+ FROM cache
149
+ WHERE ttl IS NOT NULL AND now() > ttl
150
+ ORDER BY key ASC
151
+ FOR UPDATE
152
+ ) del
153
+ WHERE del.key = c.key;"""
154
+ query = "SELECT value FROM cache WHERE key = :key AND now() < ttl;"
155
+ with self.client.connect() as conn:
156
+ conn.execute(sa.text(purge))
157
+ result = conn.execute(sa.text(query), dict(key=self.prefix + key))
158
+ if result.rowcount > 0:
159
+ self.metrics_backend.count_hit()
160
+ value = result.fetchone().value
161
+ return json.loads(value)
162
+ self.metrics_backend.count_miss()
163
+ return None
164
+
165
+ def delete(self, key):
166
+ query = "DELETE FROM cache WHERE key = :key RETURNING value;"
167
+ with self.client.connect() as conn:
168
+ result = conn.execute(sa.text(query), dict(key=self.prefix + key))
169
+ if result.rowcount > 0:
170
+ value = result.fetchone().value
171
+ return json.loads(value)
172
+ return None
173
+
174
+
175
+ def load_from_config(config):
176
+ settings = config.get_settings()
177
+ client = create_from_config(config, prefix="cache_", with_transaction=False)
178
+ return Cache(client=client, cache_prefix=settings["cache_prefix"])
@@ -0,0 +1,23 @@
1
+ --
2
+ -- Automated script, we do not need NOTICE and WARNING
3
+ --
4
+ SET client_min_messages TO ERROR;
5
+
6
+ CREATE TABLE IF NOT EXISTS cache (
7
+ key VARCHAR(256) PRIMARY KEY,
8
+ value TEXT NOT NULL,
9
+ ttl TIMESTAMP DEFAULT NULL
10
+ );
11
+
12
+ CREATE INDEX IF NOT EXISTS idx_cache_ttl ON cache(ttl);
13
+
14
+
15
+ CREATE OR REPLACE FUNCTION sec2ttl(seconds FLOAT)
16
+ RETURNS TIMESTAMP AS $$
17
+ BEGIN
18
+ IF seconds IS NULL THEN
19
+ RETURN NULL;
20
+ END IF;
21
+ RETURN now() + (seconds || ' SECOND')::INTERVAL;
22
+ END;
23
+ $$ LANGUAGE plpgsql;
@@ -0,0 +1,208 @@
1
+ import time
2
+ from unittest import mock
3
+
4
+ import pytest
5
+ from pyramid import testing
6
+
7
+ from kinto.core.cache import heartbeat
8
+ from kinto.core.storage import exceptions
9
+
10
+
11
+ class CacheTest:
12
+ backend = None
13
+ settings = {}
14
+
15
+ def setUp(self):
16
+ super().setUp()
17
+ self.cache = self.backend.load_from_config(self._get_config())
18
+ self.cache.initialize_schema()
19
+ self.request = None
20
+ self.client_error_patcher = None
21
+
22
+ def _get_config(self, settings=None):
23
+ """Mock Pyramid config object."""
24
+ if settings is None:
25
+ settings = self.settings
26
+ config = testing.setUp()
27
+ config.add_settings(settings)
28
+ return config
29
+
30
+ def tearDown(self):
31
+ mock.patch.stopall()
32
+ super().tearDown()
33
+ self.cache.flush()
34
+
35
+ def get_backend_prefix(self, prefix):
36
+ settings_prefix = {**self.settings}
37
+ settings_prefix["cache_prefix"] = prefix
38
+ config_prefix = self._get_config(settings=settings_prefix)
39
+
40
+ # initiating cache backend with prefix:
41
+ backend_prefix = self.backend.load_from_config(config_prefix)
42
+
43
+ return backend_prefix
44
+
45
+ def test_backend_error_is_raised_anywhere(self):
46
+ self.client_error_patcher.start()
47
+ calls = [
48
+ (self.cache.flush,),
49
+ (self.cache.ttl, ""),
50
+ (self.cache.expire, "", 0),
51
+ (self.cache.get, ""),
52
+ (self.cache.set, "", "", 42),
53
+ (self.cache.delete, ""),
54
+ ]
55
+ for call in calls:
56
+ self.assertRaises(exceptions.BackendError, *call)
57
+
58
+ def test_initialize_schema_is_idempotent(self):
59
+ self.cache.initialize_schema()
60
+ self.cache.initialize_schema() # not raising.
61
+
62
+ def test_ping_returns_false_if_unavailable(self):
63
+ self.client_error_patcher.start()
64
+ ping = heartbeat(self.cache)
65
+ self.assertFalse(ping(self.request))
66
+ with mock.patch("kinto.core.cache.random.SystemRandom.random", return_value=0.6):
67
+ self.assertFalse(ping(self.request))
68
+ with mock.patch("kinto.core.cache.random.SystemRandom.random", return_value=0.4):
69
+ self.assertFalse(ping(self.request))
70
+
71
+ def test_ping_returns_true_if_available(self):
72
+ ping = heartbeat(self.cache)
73
+ with mock.patch("kinto.core.cache.random.random", return_value=0.6):
74
+ self.assertTrue(ping(self.request))
75
+ with mock.patch("kinto.core.cache.random.random", return_value=0.4):
76
+ self.assertTrue(ping(self.request))
77
+
78
+ def test_ping_logs_error_if_unavailable(self):
79
+ self.client_error_patcher.start()
80
+ ping = heartbeat(self.cache)
81
+
82
+ with mock.patch("kinto.core.cache.logger.exception") as exc_handler:
83
+ self.assertFalse(ping(self.request))
84
+
85
+ self.assertTrue(exc_handler.called)
86
+
87
+ def test_set_adds_the_object(self):
88
+ stored = "toto"
89
+ self.cache.set("foobar", stored, 42)
90
+ retrieved = self.cache.get("foobar")
91
+ self.assertEqual(retrieved, stored)
92
+
93
+ def test_values_remains_python_dict(self):
94
+ def setget(k, v):
95
+ self.cache.set(k, v, 42)
96
+ return (self.cache.get(k), v)
97
+
98
+ self.assertEqual(*setget("foobar", 3))
99
+ self.assertEqual(*setget("foobar", ["a"]))
100
+ self.assertEqual(*setget("foobar", {"b": [1, 2]}))
101
+ self.assertEqual(*setget("foobar", 3.14))
102
+
103
+ def test_bytes_cannot_be_stored_in_the_cache(self):
104
+ with pytest.raises(TypeError):
105
+ self.cache.set("test", b"foo", 42)
106
+
107
+ def test_delete_removes_the_object(self):
108
+ self.cache.set("foobar", "toto", 42)
109
+ returned = self.cache.delete("foobar")
110
+ self.assertEqual(returned, "toto")
111
+ missing = self.cache.get("foobar")
112
+ self.assertIsNone(missing)
113
+
114
+ def test_delete_does_not_fail_if_object_is_unknown(self):
115
+ returned = self.cache.delete("foobar")
116
+ self.assertIsNone(returned)
117
+
118
+ def test_expire_expires_the_value(self):
119
+ self.cache.set("foobar", "toto", 42)
120
+ self.cache.expire("foobar", 0.01)
121
+ time.sleep(0.02)
122
+ retrieved = self.cache.get("foobar")
123
+ self.assertIsNone(retrieved)
124
+
125
+ def test_set_with_ttl_expires_the_value(self):
126
+ self.cache.set("foobar", "toto", 0.01)
127
+ time.sleep(0.02)
128
+ retrieved = self.cache.get("foobar")
129
+ self.assertIsNone(retrieved)
130
+
131
+ def test_ttl_return_the_time_to_live(self):
132
+ self.cache.set("foobar", "toto", 42)
133
+ self.cache.expire("foobar", 10)
134
+ ttl = self.cache.ttl("foobar")
135
+ self.assertGreater(ttl, 0)
136
+ self.assertLessEqual(ttl, 10)
137
+
138
+ def test_ttl_return_none_if_unknown(self):
139
+ ttl = self.cache.ttl("unknown")
140
+ self.assertTrue(ttl < 0)
141
+
142
+ def test_cache_prefix_is_set(self):
143
+ backend_prefix = self.get_backend_prefix(prefix="prefix_")
144
+
145
+ # Set the value
146
+ backend_prefix.set("key", "foo", 42)
147
+
148
+ # Validate that it was set with the prefix.
149
+ obtained = self.cache.get("prefix_key")
150
+ self.assertEqual(obtained, "foo")
151
+
152
+ def test_cache_when_prefix_is_not_set(self):
153
+ backend_prefix = self.get_backend_prefix(prefix="")
154
+
155
+ # Set a value
156
+ backend_prefix.set("key", "foo", 42)
157
+
158
+ # Validate that it was set with no prefix
159
+ obtained = self.cache.get("key")
160
+ self.assertEqual(obtained, "foo")
161
+
162
+ def test_prefix_value_use_to_get_data(self):
163
+ backend_prefix = self.get_backend_prefix(prefix="prefix_")
164
+
165
+ # Set the value with the prefix
166
+ self.cache.set("prefix_key", "foo", 42)
167
+
168
+ # Validate that the prefix was added
169
+ obtained = backend_prefix.get("key")
170
+ self.assertEqual(obtained, "foo")
171
+
172
+ def test_prefix_value_use_to_delete_data(self):
173
+ backend_prefix = self.get_backend_prefix(prefix="prefix_")
174
+ # Set the value
175
+ self.cache.set("prefix_key", "foo", 42)
176
+
177
+ # Delete the value
178
+ backend_prefix.delete("key")
179
+
180
+ # Validate that the value was deleted
181
+ obtained = self.cache.get("prefix_key")
182
+ self.assertEqual(obtained, None)
183
+
184
+ def test_prefix_value_used_with_ttl(self):
185
+ backend_prefix = self.get_backend_prefix(prefix="prefix_")
186
+
187
+ self.cache.set("prefix_key", "foo", 10)
188
+
189
+ # Validate that the ttl add the prefix to the key.
190
+ obtained = backend_prefix.ttl("key")
191
+ self.assertLessEqual(obtained, 10)
192
+ self.assertGreater(obtained, 9)
193
+
194
+ def test_prefix_value_used_with_expire(self):
195
+ backend_prefix = self.get_backend_prefix(prefix="prefix_")
196
+
197
+ self.cache.set("prefix_foobar", "toto", 10)
198
+
199
+ # expiring the ttl of key
200
+ backend_prefix.expire("foobar", 0)
201
+
202
+ # Make sure the TTL was set accordingly.
203
+ ttl = self.cache.ttl("prefix_foobar")
204
+ self.assertLessEqual(ttl, 0)
205
+
206
+ # The object should have expired
207
+ retrieved = self.cache.get("prefix_foobar")
208
+ self.assertIsNone(retrieved)
@@ -0,0 +1,93 @@
1
+ # This Source Code Form is subject to the terms of the Mozilla Public
2
+ # License, v. 2.0. If a copy of the MPL was not distributed with this file,
3
+ # You can obtain one at http://mozilla.org/MPL/2.0/.
4
+ import logging
5
+ from functools import partial
6
+
7
+ from pyramid.events import NewRequest
8
+ from pyramid.httpexceptions import HTTPForbidden, HTTPNotFound
9
+ from pyramid.security import NO_PERMISSION_REQUIRED
10
+ from pyramid.settings import asbool, aslist
11
+
12
+ from kinto.core.cornice.errors import Errors # NOQA
13
+ from kinto.core.cornice.pyramidhook import (
14
+ handle_exceptions,
15
+ register_resource_views,
16
+ register_service_views,
17
+ wrap_request,
18
+ )
19
+ from kinto.core.cornice.renderer import CorniceRenderer
20
+ from kinto.core.cornice.service import Service # NOQA
21
+ from kinto.core.cornice.util import ContentTypePredicate, current_service
22
+
23
+
24
+ logger = logging.getLogger("cornice")
25
+
26
+
27
+ def set_localizer_for_languages(event, available_languages, default_locale_name):
28
+ """
29
+ Sets the current locale based on the incoming Accept-Language header, if
30
+ present, and sets a localizer attribute on the request object based on
31
+ the current locale.
32
+
33
+ To be used as an event handler, this function needs to be partially applied
34
+ with the available_languages and default_locale_name arguments. The
35
+ resulting function will be an event handler which takes an event object as
36
+ its only argument.
37
+ """
38
+ request = event.request
39
+ if request.accept_language:
40
+ accepted = request.accept_language.lookup(available_languages, default=default_locale_name)
41
+ request._LOCALE_ = accepted
42
+
43
+
44
+ def setup_localization(config):
45
+ """
46
+ Setup localization based on the available_languages and
47
+ pyramid.default_locale_name settings.
48
+
49
+ These settings are named after suggestions from the "Internationalization
50
+ and Localization" section of the Pyramid documentation.
51
+ """
52
+ try:
53
+ config.add_translation_dirs("colander:locale/")
54
+ settings = config.get_settings()
55
+ available_languages = aslist(settings["available_languages"])
56
+ default_locale_name = settings.get("pyramid.default_locale_name", "en")
57
+ set_localizer = partial(
58
+ set_localizer_for_languages,
59
+ available_languages=available_languages,
60
+ default_locale_name=default_locale_name,
61
+ )
62
+ config.add_subscriber(set_localizer, NewRequest)
63
+ except ImportError: # pragma: no cover
64
+ # add_translation_dirs raises an ImportError if colander is not
65
+ # installed
66
+ pass
67
+
68
+
69
+ def includeme(config):
70
+ """Include the Cornice definitions"""
71
+ # attributes required to maintain services
72
+ config.registry.cornice_services = {}
73
+
74
+ settings = config.get_settings()
75
+
76
+ # localization request subscriber must be set before first call
77
+ # for request.localizer (in wrap_request)
78
+ if settings.get("available_languages"):
79
+ setup_localization(config)
80
+
81
+ config.add_directive("add_cornice_service", register_service_views)
82
+ config.add_directive("add_cornice_resource", register_resource_views)
83
+ config.add_subscriber(wrap_request, NewRequest)
84
+ config.add_renderer("cornicejson", CorniceRenderer())
85
+ config.add_view_predicate("content_type", ContentTypePredicate)
86
+ config.add_request_method(current_service, reify=True)
87
+
88
+ if asbool(settings.get("handle_exceptions", True)):
89
+ config.add_view(handle_exceptions, context=Exception, permission=NO_PERMISSION_REQUIRED)
90
+ config.add_view(handle_exceptions, context=HTTPNotFound, permission=NO_PERMISSION_REQUIRED)
91
+ config.add_view(
92
+ handle_exceptions, context=HTTPForbidden, permission=NO_PERMISSION_REQUIRED
93
+ )
@@ -0,0 +1,144 @@
1
+ # This Source Code Form is subject to the terms of the Mozilla Public
2
+ # License, v. 2.0. If a copy of the MPL was not distributed with this file,
3
+ # You can obtain one at http://mozilla.org/MPL/2.0/.
4
+ import fnmatch
5
+ import functools
6
+
7
+ from pyramid.settings import asbool
8
+
9
+
10
+ CORS_PARAMETERS = (
11
+ "cors_headers",
12
+ "cors_enabled",
13
+ "cors_origins",
14
+ "cors_credentials",
15
+ "cors_max_age",
16
+ "cors_expose_all_headers",
17
+ )
18
+
19
+
20
+ def get_cors_preflight_view(service):
21
+ """Return a view for the OPTION method.
22
+
23
+ Checks that the User-Agent is authorized to do a request to the server, and
24
+ to this particular service, and add the various checks that are specified
25
+ in http://www.w3.org/TR/cors/#resource-processing-model.
26
+ """
27
+
28
+ def _preflight_view(request):
29
+ response = request.response
30
+ origin = request.headers.get("Origin")
31
+ supported_headers = service.cors_supported_headers_for()
32
+
33
+ if not origin:
34
+ request.errors.add("header", "Origin", "this header is mandatory")
35
+
36
+ requested_method = request.headers.get("Access-Control-Request-Method")
37
+ if not requested_method:
38
+ request.errors.add(
39
+ "header", "Access-Control-Request-Method", "this header is mandatory"
40
+ )
41
+
42
+ if not (requested_method and origin):
43
+ return
44
+
45
+ requested_headers = request.headers.get("Access-Control-Request-Headers", ())
46
+
47
+ if requested_headers:
48
+ requested_headers = map(str.strip, requested_headers.split(","))
49
+
50
+ if requested_method not in service.cors_supported_methods:
51
+ request.errors.add("header", "Access-Control-Request-Method", "Method not allowed")
52
+
53
+ if not service.cors_expose_all_headers:
54
+ for h in requested_headers:
55
+ if h.lower() not in [s.lower() for s in supported_headers]:
56
+ request.errors.add(
57
+ "header", "Access-Control-Request-Headers", 'Header "%s" not allowed' % h
58
+ )
59
+
60
+ supported_headers = set(supported_headers) | set(requested_headers)
61
+
62
+ response.headers["Access-Control-Allow-Headers"] = ",".join(supported_headers)
63
+
64
+ response.headers["Access-Control-Allow-Methods"] = ",".join(service.cors_supported_methods)
65
+
66
+ max_age = service.cors_max_age_for(requested_method)
67
+ if max_age is not None:
68
+ response.headers["Access-Control-Max-Age"] = str(max_age)
69
+
70
+ return None
71
+
72
+ return _preflight_view
73
+
74
+
75
+ def _get_method(request):
76
+ """Return what's supposed to be the method for CORS operations.
77
+ (e.g if the verb is options, look at the A-C-Request-Method header,
78
+ otherwise return the HTTP verb).
79
+ """
80
+ if request.method == "OPTIONS":
81
+ method = request.headers.get("Access-Control-Request-Method", request.method)
82
+ else:
83
+ method = request.method
84
+ return method
85
+
86
+
87
+ def ensure_origin(service, request, response=None, **kwargs):
88
+ """Ensure that the origin header is set and allowed."""
89
+ response = response or request.response
90
+
91
+ # Don't check this twice.
92
+ if not request.info.get("cors_checked", False):
93
+ method = _get_method(request)
94
+
95
+ origin = request.headers.get("Origin")
96
+
97
+ if not origin:
98
+ always_cors = asbool(request.registry.settings.get("cornice.always_cors"))
99
+ # With this setting, if the service origins has "*", then
100
+ # always return CORS headers.
101
+ origins = getattr(service, "cors_origins", [])
102
+ if always_cors and "*" in origins:
103
+ origin = "*"
104
+
105
+ if origin:
106
+ if not any([fnmatch.fnmatchcase(origin, o) for o in service.cors_origins_for(method)]):
107
+ request.errors.add("header", "Origin", "%s not allowed" % origin)
108
+ elif service.cors_support_credentials_for(method):
109
+ response.headers["Access-Control-Allow-Origin"] = origin
110
+ else:
111
+ if any([o == "*" for o in service.cors_origins_for(method)]):
112
+ response.headers["Access-Control-Allow-Origin"] = "*"
113
+ else:
114
+ response.headers["Access-Control-Allow-Origin"] = origin
115
+ request.info["cors_checked"] = True
116
+ return response
117
+
118
+
119
+ def get_cors_validator(service):
120
+ return functools.partial(ensure_origin, service)
121
+
122
+
123
+ def apply_cors_post_request(service, request, response):
124
+ """Handles CORS-related post-request things.
125
+
126
+ Add some response headers, such as the Expose-Headers and the
127
+ Allow-Credentials ones.
128
+ """
129
+ response = ensure_origin(service, request, response)
130
+ method = _get_method(request)
131
+
132
+ if (
133
+ service.cors_support_credentials_for(method)
134
+ and "Access-Control-Allow-Credentials" not in response.headers
135
+ ):
136
+ response.headers["Access-Control-Allow-Credentials"] = "true"
137
+
138
+ if request.method != "OPTIONS":
139
+ # Which headers are exposed?
140
+ supported_headers = service.cors_supported_headers_for(request.method)
141
+ if supported_headers:
142
+ response.headers["Access-Control-Expose-Headers"] = ", ".join(supported_headers)
143
+
144
+ return response
@@ -0,0 +1,40 @@
1
+ # This Source Code Form is subject to the terms of the Mozilla Public
2
+ # License, v. 2.0. If a copy of the MPL was not distributed with this file,
3
+ # You can obtain one at http://mozilla.org/MPL/2.0/.
4
+ import json
5
+
6
+ from pyramid.i18n import TranslationString
7
+
8
+
9
+ class Errors(list):
10
+ """Holds Request errors"""
11
+
12
+ def __init__(self, status=400, localizer=None):
13
+ self.status = status
14
+ self.localizer = localizer
15
+ super(Errors, self).__init__()
16
+
17
+ def add(self, location, name=None, description=None, **kw):
18
+ """Registers a new error."""
19
+ allowed = ("body", "querystring", "url", "header", "path", "cookies", "method")
20
+ if location != "" and location not in allowed:
21
+ raise ValueError("%r not in %s" % (location, allowed))
22
+
23
+ if isinstance(description, TranslationString) and self.localizer:
24
+ description = self.localizer.translate(description)
25
+
26
+ self.append(dict(location=location, name=name, description=description, **kw))
27
+
28
+ @classmethod
29
+ def from_json(cls, string):
30
+ """Transforms a json string into an `Errors` instance"""
31
+ obj = json.loads(string.decode())
32
+ return Errors.from_list(obj.get("errors", []))
33
+
34
+ @classmethod
35
+ def from_list(cls, obj):
36
+ """Transforms a python list into an `Errors` instance"""
37
+ errors = Errors()
38
+ for error in obj:
39
+ errors.add(**error)
40
+ return errors