We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Unnecessary purges on `User` model change
Currently right now any time the `User` model changes, we purge all the cache keys for that user's project.

This includes attribute changes that don't actually affect the project pages, like `last_login`, `password` etc.

We should filter out "purge-able" attribute changes and only issue purges when necessary. Said attributes include:
* `username`
* `name`
* `emails`

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `warehouse/packaging/__init__.py`
Content:
```
1 # Licensed under the Apache License, Version 2.0 (the "License");
2 # you may not use this file except in compliance with the License.
3 # You may obtain a copy of the License at
4 #
5 # http://www.apache.org/licenses/LICENSE-2.0
6 #
7 # Unless required by applicable law or agreed to in writing, software
8 # distributed under the License is distributed on an "AS IS" BASIS,
9 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 # See the License for the specific language governing permissions and
11 # limitations under the License.
12 
13 from celery.schedules import crontab
14 
15 from warehouse.accounts.models import User
16 from warehouse.cache.origin import key_factory
17 from warehouse.packaging.interfaces import IFileStorage
18 from warehouse.packaging.models import Project, Release
19 from warehouse.packaging.tasks import compute_trending
20 
21 
22 def includeme(config):
23     # Register whatever file storage backend has been configured for storing
24     # our package files.
25     storage_class = config.maybe_dotted(
26         config.registry.settings["files.backend"],
27     )
28     config.register_service_factory(storage_class.create_service, IFileStorage)
29 
30     # Register our origin cache keys
31     config.register_origin_cache_keys(
32         Project,
33         cache_keys=["project/{obj.normalized_name}"],
34         purge_keys=[
35             key_factory("project/{obj.normalized_name}"),
36             key_factory("user/{itr.username}", iterate_on='users'),
37             key_factory("all-projects"),
38         ],
39     )
40     config.register_origin_cache_keys(
41         Release,
42         cache_keys=["project/{obj.project.normalized_name}"],
43         purge_keys=[
44             key_factory("project/{obj.project.normalized_name}"),
45             key_factory("user/{itr.username}", iterate_on='project.users'),
46             key_factory("all-projects"),
47         ],
48     )
49     config.register_origin_cache_keys(
50         User,
51         cache_keys=["user/{obj.username}"],
52         purge_keys=[
53             key_factory("user/{obj.username}"),
54             key_factory("project/{itr.normalized_name}", iterate_on='projects')
55         ],
56     )
57 
58     # Add a periodic task to compute trending once a day, assuming we have
59     # been configured to be able to access BigQuery.
60     if config.get_settings().get("warehouse.trending_table"):
61         config.add_periodic_task(crontab(minute=0, hour=3), compute_trending)
62
```

Path: `warehouse/cache/origin/__init__.py`
Content:
```
1 # Licensed under the Apache License, Version 2.0 (the "License");
2 # you may not use this file except in compliance with the License.
3 # You may obtain a copy of the License at
4 #
5 # http://www.apache.org/licenses/LICENSE-2.0
6 #
7 # Unless required by applicable law or agreed to in writing, software
8 # distributed under the License is distributed on an "AS IS" BASIS,
9 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 # See the License for the specific language governing permissions and
11 # limitations under the License.
12 
13 import collections
14 import functools
15 import operator
16 from itertools import chain
17 
18 from warehouse import db
19 from warehouse.cache.origin.interfaces import IOriginCache
20 
21 
22 @db.listens_for(db.Session, "after_flush")
23 def store_purge_keys(config, session, flush_context):
24     cache_keys = config.registry["cache_keys"]
25 
26     # We'll (ab)use the session.info dictionary to store a list of pending
27     # purges to the session.
28     purges = session.info.setdefault("warehouse.cache.origin.purges", set())
29 
30     # Go through each new, changed, and deleted object and attempt to store
31     # a cache key that we'll want to purge when the session has been committed.
32     for obj in (session.new | session.dirty | session.deleted):
33         try:
34             key_maker = cache_keys[obj.__class__]
35         except KeyError:
36             continue
37 
38         purges.update(key_maker(obj).purge)
39 
40 
41 @db.listens_for(db.Session, "after_commit")
42 def execute_purge(config, session):
43     purges = session.info.pop("warehouse.cache.origin.purges", set())
44 
45     try:
46         cacher_factory = config.find_service_factory(IOriginCache)
47     except ValueError:
48         return
49 
50     cacher = cacher_factory(None, config)
51     cacher.purge(purges)
52 
53 
54 def origin_cache(seconds, keys=None, stale_while_revalidate=None,
55                  stale_if_error=None):
56     if keys is None:
57         keys = []
58 
59     def inner(view):
60         @functools.wraps(view)
61         def wrapped(context, request):
62             cache_keys = request.registry["cache_keys"]
63 
64             context_keys = []
65             if context.__class__ in cache_keys:
66                 context_keys = cache_keys[context.__class__](context).cache
67 
68             try:
69                 cacher = request.find_service(IOriginCache)
70             except ValueError:
71                 pass
72             else:
73                 request.add_response_callback(
74                     functools.partial(
75                         cacher.cache,
76                         sorted(context_keys + keys),
77                         seconds=seconds,
78                         stale_while_revalidate=stale_while_revalidate,
79                         stale_if_error=stale_if_error,
80                     )
81                 )
82 
83             return view(context, request)
84         return wrapped
85 
86     return inner
87 
88 
89 CacheKeys = collections.namedtuple("CacheKeys", ["cache", "purge"])
90 
91 
92 def key_factory(keystring, iterate_on=None):
93 
94     def generate_key(obj):
95         if iterate_on:
96             for itr in operator.attrgetter(iterate_on)(obj):
97                 yield keystring.format(itr=itr, obj=obj)
98         else:
99             yield keystring.format(obj=obj)
100 
101     return generate_key
102 
103 
104 def key_maker_factory(cache_keys, purge_keys):
105     if cache_keys is None:
106         cache_keys = []
107 
108     if purge_keys is None:
109         purge_keys = []
110 
111     def key_maker(obj):
112         return CacheKeys(
113             # Note: this does not support setting the `cache` argument via
114             # multiple `key_factories` as we do with `purge` because there is
115             # a limit to how many surrogate keys we can attach to a single HTTP
116             # response, and being able to use use `iterate_on` would allow this
117             # size to be unbounded.
118             # ref: https://github.com/pypa/warehouse/pull/3189
119             cache=[k.format(obj=obj) for k in cache_keys],
120             purge=chain.from_iterable(key(obj) for key in purge_keys),
121         )
122 
123     return key_maker
124 
125 
126 def register_origin_cache_keys(config, klass, cache_keys=None,
127                                purge_keys=None):
128     key_makers = config.registry.setdefault("cache_keys", {})
129     key_makers[klass] = key_maker_factory(
130         cache_keys=cache_keys,
131         purge_keys=purge_keys,
132     )
133 
134 
135 def includeme(config):
136     if "origin_cache.backend" in config.registry.settings:
137         cache_class = config.maybe_dotted(
138             config.registry.settings["origin_cache.backend"],
139         )
140         config.register_service_factory(
141             cache_class.create_service,
142             IOriginCache,
143         )
144 
145     config.add_directive(
146         "register_origin_cache_keys",
147         register_origin_cache_keys,
148     )
149
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/warehouse/cache/origin/__init__.py b/warehouse/cache/origin/__init__.py
--- a/warehouse/cache/origin/__init__.py
+++ b/warehouse/cache/origin/__init__.py
@@ -15,6 +15,8 @@
 import operator
 from itertools import chain
 
+from sqlalchemy.orm.session import Session
+
 from warehouse import db
 from warehouse.cache.origin.interfaces import IOriginCache
 
@@ -132,6 +134,19 @@
     )
 
 
+def receive_set(attribute, config, target):
+    cache_keys = config.registry["cache_keys"]
+    session = Session.object_session(target)
+    if session:
+        purges = session.info.setdefault(
+            "warehouse.cache.origin.purges",
+            set()
+        )
+        key_maker = cache_keys[attribute]
+        keys = key_maker(target).purge
+        purges.update(list(keys))
+
+
 def includeme(config):
     if "origin_cache.backend" in config.registry.settings:
         cache_class = config.maybe_dotted(
diff --git a/warehouse/packaging/__init__.py b/warehouse/packaging/__init__.py
--- a/warehouse/packaging/__init__.py
+++ b/warehouse/packaging/__init__.py
@@ -11,14 +11,25 @@
 # limitations under the License.
 
 from celery.schedules import crontab
+from warehouse import db
 
-from warehouse.accounts.models import User
-from warehouse.cache.origin import key_factory
+from warehouse.accounts.models import User, Email
+from warehouse.cache.origin import key_factory, receive_set
 from warehouse.packaging.interfaces import IFileStorage
 from warehouse.packaging.models import Project, Release
 from warehouse.packaging.tasks import compute_trending
 
 
+@db.listens_for(User.name, 'set')
+def user_name_receive_set(config, target, value, oldvalue, initiator):
+    receive_set(User.name, config, target)
+
+
+@db.listens_for(Email.primary, 'set')
+def email_primary_receive_set(config, target, value, oldvalue, initiator):
+    receive_set(Email.primary, config, target)
+
+
 def includeme(config):
     # Register whatever file storage backend has been configured for storing
     # our package files.
@@ -49,11 +60,24 @@
     config.register_origin_cache_keys(
         User,
         cache_keys=["user/{obj.username}"],
+    )
+    config.register_origin_cache_keys(
+        User.name,
         purge_keys=[
             key_factory("user/{obj.username}"),
             key_factory("project/{itr.normalized_name}", iterate_on='projects')
         ],
     )
+    config.register_origin_cache_keys(
+        Email.primary,
+        purge_keys=[
+            key_factory("user/{obj.user.username}"),
+            key_factory(
+                "project/{itr.normalized_name}",
+                iterate_on='user.projects',
+            )
+        ],
+    )
 
     # Add a periodic task to compute trending once a day, assuming we have
     # been configured to be able to access BigQuery.

{"golden_diff": "diff --git a/warehouse/cache/origin/__init__.py b/warehouse/cache/origin/__init__.py\n--- a/warehouse/cache/origin/__init__.py\n+++ b/warehouse/cache/origin/__init__.py\n@@ -15,6 +15,8 @@\n import operator\n from itertools import chain\n \n+from sqlalchemy.orm.session import Session\n+\n from warehouse import db\n from warehouse.cache.origin.interfaces import IOriginCache\n \n@@ -132,6 +134,19 @@\n     )\n \n \n+def receive_set(attribute, config, target):\n+    cache_keys = config.registry[\"cache_keys\"]\n+    session = Session.object_session(target)\n+    if session:\n+        purges = session.info.setdefault(\n+            \"warehouse.cache.origin.purges\",\n+            set()\n+        )\n+        key_maker = cache_keys[attribute]\n+        keys = key_maker(target).purge\n+        purges.update(list(keys))\n+\n+\n def includeme(config):\n     if \"origin_cache.backend\" in config.registry.settings:\n         cache_class = config.maybe_dotted(\ndiff --git a/warehouse/packaging/__init__.py b/warehouse/packaging/__init__.py\n--- a/warehouse/packaging/__init__.py\n+++ b/warehouse/packaging/__init__.py\n@@ -11,14 +11,25 @@\n # limitations under the License.\n \n from celery.schedules import crontab\n+from warehouse import db\n \n-from warehouse.accounts.models import User\n-from warehouse.cache.origin import key_factory\n+from warehouse.accounts.models import User, Email\n+from warehouse.cache.origin import key_factory, receive_set\n from warehouse.packaging.interfaces import IFileStorage\n from warehouse.packaging.models import Project, Release\n from warehouse.packaging.tasks import compute_trending\n \n \n+@db.listens_for(User.name, 'set')\n+def user_name_receive_set(config, target, value, oldvalue, initiator):\n+    receive_set(User.name, config, target)\n+\n+\n+@db.listens_for(Email.primary, 'set')\n+def email_primary_receive_set(config, target, value, oldvalue, initiator):\n+    receive_set(Email.primary, config, target)\n+\n+\n def includeme(config):\n     # Register whatever file storage backend has been configured for storing\n     # our package files.\n@@ -49,11 +60,24 @@\n     config.register_origin_cache_keys(\n         User,\n         cache_keys=[\"user/{obj.username}\"],\n+    )\n+    config.register_origin_cache_keys(\n+        User.name,\n         purge_keys=[\n             key_factory(\"user/{obj.username}\"),\n             key_factory(\"project/{itr.normalized_name}\", iterate_on='projects')\n         ],\n     )\n+    config.register_origin_cache_keys(\n+        Email.primary,\n+        purge_keys=[\n+            key_factory(\"user/{obj.user.username}\"),\n+            key_factory(\n+                \"project/{itr.normalized_name}\",\n+                iterate_on='user.projects',\n+            )\n+        ],\n+    )\n \n     # Add a periodic task to compute trending once a day, assuming we have\n     # been configured to be able to access BigQuery.\n", "issue": "Unnecessary purges on `User` model change\nCurrently right now any time the `User` model changes, we purge all the cache keys for that user's project.\r\n\r\nThis includes attribute changes that don't actually affect the project pages, like `last_login`, `password` etc.\r\n\r\nWe should filter out \"purge-able\" attribute changes and only issue purges when necessary. Said attributes include:\r\n* `username`\r\n* `name`\r\n* `emails`\n", "before_files": [{"content": "# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nfrom celery.schedules import crontab\n\nfrom warehouse.accounts.models import User\nfrom warehouse.cache.origin import key_factory\nfrom warehouse.packaging.interfaces import IFileStorage\nfrom warehouse.packaging.models import Project, Release\nfrom warehouse.packaging.tasks import compute_trending\n\n\ndef includeme(config):\n    # Register whatever file storage backend has been configured for storing\n    # our package files.\n    storage_class = config.maybe_dotted(\n        config.registry.settings[\"files.backend\"],\n    )\n    config.register_service_factory(storage_class.create_service, IFileStorage)\n\n    # Register our origin cache keys\n    config.register_origin_cache_keys(\n        Project,\n        cache_keys=[\"project/{obj.normalized_name}\"],\n        purge_keys=[\n            key_factory(\"project/{obj.normalized_name}\"),\n            key_factory(\"user/{itr.username}\", iterate_on='users'),\n            key_factory(\"all-projects\"),\n        ],\n    )\n    config.register_origin_cache_keys(\n        Release,\n        cache_keys=[\"project/{obj.project.normalized_name}\"],\n        purge_keys=[\n            key_factory(\"project/{obj.project.normalized_name}\"),\n            key_factory(\"user/{itr.username}\", iterate_on='project.users'),\n            key_factory(\"all-projects\"),\n        ],\n    )\n    config.register_origin_cache_keys(\n        User,\n        cache_keys=[\"user/{obj.username}\"],\n        purge_keys=[\n            key_factory(\"user/{obj.username}\"),\n            key_factory(\"project/{itr.normalized_name}\", iterate_on='projects')\n        ],\n    )\n\n    # Add a periodic task to compute trending once a day, assuming we have\n    # been configured to be able to access BigQuery.\n    if config.get_settings().get(\"warehouse.trending_table\"):\n        config.add_periodic_task(crontab(minute=0, hour=3), compute_trending)\n", "path": "warehouse/packaging/__init__.py"}, {"content": "# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport collections\nimport functools\nimport operator\nfrom itertools import chain\n\nfrom warehouse import db\nfrom warehouse.cache.origin.interfaces import IOriginCache\n\n\n@db.listens_for(db.Session, \"after_flush\")\ndef store_purge_keys(config, session, flush_context):\n    cache_keys = config.registry[\"cache_keys\"]\n\n    # We'll (ab)use the session.info dictionary to store a list of pending\n    # purges to the session.\n    purges = session.info.setdefault(\"warehouse.cache.origin.purges\", set())\n\n    # Go through each new, changed, and deleted object and attempt to store\n    # a cache key that we'll want to purge when the session has been committed.\n    for obj in (session.new | session.dirty | session.deleted):\n        try:\n            key_maker = cache_keys[obj.__class__]\n        except KeyError:\n            continue\n\n        purges.update(key_maker(obj).purge)\n\n\n@db.listens_for(db.Session, \"after_commit\")\ndef execute_purge(config, session):\n    purges = session.info.pop(\"warehouse.cache.origin.purges\", set())\n\n    try:\n        cacher_factory = config.find_service_factory(IOriginCache)\n    except ValueError:\n        return\n\n    cacher = cacher_factory(None, config)\n    cacher.purge(purges)\n\n\ndef origin_cache(seconds, keys=None, stale_while_revalidate=None,\n                 stale_if_error=None):\n    if keys is None:\n        keys = []\n\n    def inner(view):\n        @functools.wraps(view)\n        def wrapped(context, request):\n            cache_keys = request.registry[\"cache_keys\"]\n\n            context_keys = []\n            if context.__class__ in cache_keys:\n                context_keys = cache_keys[context.__class__](context).cache\n\n            try:\n                cacher = request.find_service(IOriginCache)\n            except ValueError:\n                pass\n            else:\n                request.add_response_callback(\n                    functools.partial(\n                        cacher.cache,\n                        sorted(context_keys + keys),\n                        seconds=seconds,\n                        stale_while_revalidate=stale_while_revalidate,\n                        stale_if_error=stale_if_error,\n                    )\n                )\n\n            return view(context, request)\n        return wrapped\n\n    return inner\n\n\nCacheKeys = collections.namedtuple(\"CacheKeys\", [\"cache\", \"purge\"])\n\n\ndef key_factory(keystring, iterate_on=None):\n\n    def generate_key(obj):\n        if iterate_on:\n            for itr in operator.attrgetter(iterate_on)(obj):\n                yield keystring.format(itr=itr, obj=obj)\n        else:\n            yield keystring.format(obj=obj)\n\n    return generate_key\n\n\ndef key_maker_factory(cache_keys, purge_keys):\n    if cache_keys is None:\n        cache_keys = []\n\n    if purge_keys is None:\n        purge_keys = []\n\n    def key_maker(obj):\n        return CacheKeys(\n            # Note: this does not support setting the `cache` argument via\n            # multiple `key_factories` as we do with `purge` because there is\n            # a limit to how many surrogate keys we can attach to a single HTTP\n            # response, and being able to use use `iterate_on` would allow this\n            # size to be unbounded.\n            # ref: https://github.com/pypa/warehouse/pull/3189\n            cache=[k.format(obj=obj) for k in cache_keys],\n            purge=chain.from_iterable(key(obj) for key in purge_keys),\n        )\n\n    return key_maker\n\n\ndef register_origin_cache_keys(config, klass, cache_keys=None,\n                               purge_keys=None):\n    key_makers = config.registry.setdefault(\"cache_keys\", {})\n    key_makers[klass] = key_maker_factory(\n        cache_keys=cache_keys,\n        purge_keys=purge_keys,\n    )\n\n\ndef includeme(config):\n    if \"origin_cache.backend\" in config.registry.settings:\n        cache_class = config.maybe_dotted(\n            config.registry.settings[\"origin_cache.backend\"],\n        )\n        config.register_service_factory(\n            cache_class.create_service,\n            IOriginCache,\n        )\n\n    config.add_directive(\n        \"register_origin_cache_keys\",\n        register_origin_cache_keys,\n    )\n", "path": "warehouse/cache/origin/__init__.py"}], "after_files": [{"content": "# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nfrom celery.schedules import crontab\nfrom warehouse import db\n\nfrom warehouse.accounts.models import User, Email\nfrom warehouse.cache.origin import key_factory, receive_set\nfrom warehouse.packaging.interfaces import IFileStorage\nfrom warehouse.packaging.models import Project, Release\nfrom warehouse.packaging.tasks import compute_trending\n\n\n@db.listens_for(User.name, 'set')\ndef user_name_receive_set(config, target, value, oldvalue, initiator):\n    receive_set(User.name, config, target)\n\n\n@db.listens_for(Email.primary, 'set')\ndef email_primary_receive_set(config, target, value, oldvalue, initiator):\n    receive_set(Email.primary, config, target)\n\n\ndef includeme(config):\n    # Register whatever file storage backend has been configured for storing\n    # our package files.\n    storage_class = config.maybe_dotted(\n        config.registry.settings[\"files.backend\"],\n    )\n    config.register_service_factory(storage_class.create_service, IFileStorage)\n\n    # Register our origin cache keys\n    config.register_origin_cache_keys(\n        Project,\n        cache_keys=[\"project/{obj.normalized_name}\"],\n        purge_keys=[\n            key_factory(\"project/{obj.normalized_name}\"),\n            key_factory(\"user/{itr.username}\", iterate_on='users'),\n            key_factory(\"all-projects\"),\n        ],\n    )\n    config.register_origin_cache_keys(\n        Release,\n        cache_keys=[\"project/{obj.project.normalized_name}\"],\n        purge_keys=[\n            key_factory(\"project/{obj.project.normalized_name}\"),\n            key_factory(\"user/{itr.username}\", iterate_on='project.users'),\n            key_factory(\"all-projects\"),\n        ],\n    )\n    config.register_origin_cache_keys(\n        User,\n        cache_keys=[\"user/{obj.username}\"],\n    )\n    config.register_origin_cache_keys(\n        User.name,\n        purge_keys=[\n            key_factory(\"user/{obj.username}\"),\n            key_factory(\"project/{itr.normalized_name}\", iterate_on='projects')\n        ],\n    )\n    config.register_origin_cache_keys(\n        Email.primary,\n        purge_keys=[\n            key_factory(\"user/{obj.user.username}\"),\n            key_factory(\n                \"project/{itr.normalized_name}\",\n                iterate_on='user.projects',\n            )\n        ],\n    )\n\n    # Add a periodic task to compute trending once a day, assuming we have\n    # been configured to be able to access BigQuery.\n    if config.get_settings().get(\"warehouse.trending_table\"):\n        config.add_periodic_task(crontab(minute=0, hour=3), compute_trending)\n", "path": "warehouse/packaging/__init__.py"}, {"content": "# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport collections\nimport functools\nimport operator\nfrom itertools import chain\n\nfrom sqlalchemy.orm.session import Session\n\nfrom warehouse import db\nfrom warehouse.cache.origin.interfaces import IOriginCache\n\n\n@db.listens_for(db.Session, \"after_flush\")\ndef store_purge_keys(config, session, flush_context):\n    cache_keys = config.registry[\"cache_keys\"]\n\n    # We'll (ab)use the session.info dictionary to store a list of pending\n    # purges to the session.\n    purges = session.info.setdefault(\"warehouse.cache.origin.purges\", set())\n\n    # Go through each new, changed, and deleted object and attempt to store\n    # a cache key that we'll want to purge when the session has been committed.\n    for obj in (session.new | session.dirty | session.deleted):\n        try:\n            key_maker = cache_keys[obj.__class__]\n        except KeyError:\n            continue\n\n        purges.update(key_maker(obj).purge)\n\n\n@db.listens_for(db.Session, \"after_commit\")\ndef execute_purge(config, session):\n    purges = session.info.pop(\"warehouse.cache.origin.purges\", set())\n\n    try:\n        cacher_factory = config.find_service_factory(IOriginCache)\n    except ValueError:\n        return\n\n    cacher = cacher_factory(None, config)\n    cacher.purge(purges)\n\n\ndef origin_cache(seconds, keys=None, stale_while_revalidate=None,\n                 stale_if_error=None):\n    if keys is None:\n        keys = []\n\n    def inner(view):\n        @functools.wraps(view)\n        def wrapped(context, request):\n            cache_keys = request.registry[\"cache_keys\"]\n\n            context_keys = []\n            if context.__class__ in cache_keys:\n                context_keys = cache_keys[context.__class__](context).cache\n\n            try:\n                cacher = request.find_service(IOriginCache)\n            except ValueError:\n                pass\n            else:\n                request.add_response_callback(\n                    functools.partial(\n                        cacher.cache,\n                        sorted(context_keys + keys),\n                        seconds=seconds,\n                        stale_while_revalidate=stale_while_revalidate,\n                        stale_if_error=stale_if_error,\n                    )\n                )\n\n            return view(context, request)\n        return wrapped\n\n    return inner\n\n\nCacheKeys = collections.namedtuple(\"CacheKeys\", [\"cache\", \"purge\"])\n\n\ndef key_factory(keystring, iterate_on=None):\n\n    def generate_key(obj):\n        if iterate_on:\n            for itr in operator.attrgetter(iterate_on)(obj):\n                yield keystring.format(itr=itr, obj=obj)\n        else:\n            yield keystring.format(obj=obj)\n\n    return generate_key\n\n\ndef key_maker_factory(cache_keys, purge_keys):\n    if cache_keys is None:\n        cache_keys = []\n\n    if purge_keys is None:\n        purge_keys = []\n\n    def key_maker(obj):\n        return CacheKeys(\n            # Note: this does not support setting the `cache` argument via\n            # multiple `key_factories` as we do with `purge` because there is\n            # a limit to how many surrogate keys we can attach to a single HTTP\n            # response, and being able to use use `iterate_on` would allow this\n            # size to be unbounded.\n            # ref: https://github.com/pypa/warehouse/pull/3189\n            cache=[k.format(obj=obj) for k in cache_keys],\n            purge=chain.from_iterable(key(obj) for key in purge_keys),\n        )\n\n    return key_maker\n\n\ndef register_origin_cache_keys(config, klass, cache_keys=None,\n                               purge_keys=None):\n    key_makers = config.registry.setdefault(\"cache_keys\", {})\n    key_makers[klass] = key_maker_factory(\n        cache_keys=cache_keys,\n        purge_keys=purge_keys,\n    )\n\n\ndef receive_set(attribute, config, target):\n    cache_keys = config.registry[\"cache_keys\"]\n    session = Session.object_session(target)\n    if session:\n        purges = session.info.setdefault(\n            \"warehouse.cache.origin.purges\",\n            set()\n        )\n        key_maker = cache_keys[attribute]\n        keys = key_maker(target).purge\n        purges.update(list(keys))\n\n\ndef includeme(config):\n    if \"origin_cache.backend\" in config.registry.settings:\n        cache_class = config.maybe_dotted(\n            config.registry.settings[\"origin_cache.backend\"],\n        )\n        config.register_service_factory(\n            cache_class.create_service,\n            IOriginCache,\n        )\n\n    config.add_directive(\n        \"register_origin_cache_keys\",\n        register_origin_cache_keys,\n    )\n", "path": "warehouse/cache/origin/__init__.py"}]}