1 line
No EOL
1 MiB
1 line
No EOL
1 MiB
{"github_data": {"url": "https://api.github.com/repos/Tautulli/Tautulli/compare/940c2ae6cd064817512aa7646386a31fa9f465fa...78864d7a97ab94d433f5eca205897f2b10be455b", "html_url": "https://github.com/Tautulli/Tautulli/compare/940c2ae6cd064817512aa7646386a31fa9f465fa...78864d7a97ab94d433f5eca205897f2b10be455b", "permalink_url": "https://github.com/Tautulli/Tautulli/compare/Tautulli:940c2ae...Tautulli:78864d7", "diff_url": "https://github.com/Tautulli/Tautulli/compare/940c2ae6cd064817512aa7646386a31fa9f465fa...78864d7a97ab94d433f5eca205897f2b10be455b.diff", "patch_url": "https://github.com/Tautulli/Tautulli/compare/940c2ae6cd064817512aa7646386a31fa9f465fa...78864d7a97ab94d433f5eca205897f2b10be455b.patch", "base_commit": {"sha": "940c2ae6cd064817512aa7646386a31fa9f465fa", "node_id": "C_kwDOAducuNoAKDk0MGMyYWU2Y2QwNjQ4MTc1MTJhYTc2NDYzODZhMzFmYTlmNDY1ZmE", "commit": {"author": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-10-12T23:55:38Z"}, "committer": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-10-12T23:55:38Z"}, "message": "v2.14.6", "tree": {"sha": "4cdce13025cf16b86b6242c818df2501f3d84d5c", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/4cdce13025cf16b86b6242c818df2501f3d84d5c"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/940c2ae6cd064817512aa7646386a31fa9f465fa", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\niQIzBAABCAAdFiEEFPKZYCM7sDX3KZzmsfH5gHGEaXoFAmcLDH4ACgkQsfH5gHGE\naXp/Yw/9EYlGn4AbfdoIGSoVTjVN75m7/5nUScbyAbZkqG87GovCKSbUKChtRVP2\n5csMym9KszLZyQw5dskGeUfDcxmzZ84lttrJNBu47p0Wh0ZNRAq/mcDRlfgtBv6B\nudUQwXPnSuZIhU9LUz0yJm8/Bm5GPaQZyUPf5PjQrjjQeFL5IEP49UGsmeUQbXeO\nkc7d49Uor/tifeKSNblSzLwihqjQZJkc0JJG3dPNz8vMiDScerK6cygMDW1NYxiU\nq9hPXaoOk/QDqPmSkwuHaYB1UXXT0Ib+Quy6kv0lNoRsJ+2xYp+uPeiBr6WEV+A7\nYCwySlWfTC396q4QriAaBAdAmGrD7hzPTV5dSUUn0ydOP+xArtgR+rLMri+ARhPV\nz29zsYZz6/8k1FQ6MO80YEntqD3S0xXyDXsP2vRYVILG4+0hRXY1gE3RsqLSNqSh\njugwboYUp5igpfVZO0igNSJTLEze6vVaM0Et03KcWL7CCKrP5gtpAOXSPe/z+/fQ\nrMkhkIW9kz+J29OowW+/dMBnWRFR3WCDFDJG/Rgi/DlFAVWTd+LJ/iLE9X0I99dP\nV58s94gyfQZdgPcsFiRRi3wD6kNTU67LyBApGhwiAQeoJPvPCxaljn1hd2Tlpm0r\nn8G2QaLxFVWn8XxW8tulJcpjcNXiUbP0hm07zccDCcT3YjGYRs4=\n=z3c0\n-----END PGP SIGNATURE-----", "payload": "tree 4cdce13025cf16b86b6242c818df2501f3d84d5c\nparent 1cdfd5f30a6c4b080f4bab3225267b70123639a4\nauthor JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1728777338 -0700\ncommitter JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1728777338 -0700\n\nv2.14.6\n", "verified_at": "2024-11-12T16:04:51Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/940c2ae6cd064817512aa7646386a31fa9f465fa", "html_url": "https://github.com/Tautulli/Tautulli/commit/940c2ae6cd064817512aa7646386a31fa9f465fa", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/940c2ae6cd064817512aa7646386a31fa9f465fa/comments", "author": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "committer": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "1cdfd5f30a6c4b080f4bab3225267b70123639a4", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/1cdfd5f30a6c4b080f4bab3225267b70123639a4", "html_url": "https://github.com/Tautulli/Tautulli/commit/1cdfd5f30a6c4b080f4bab3225267b70123639a4"}]}, "merge_base_commit": {"sha": "940c2ae6cd064817512aa7646386a31fa9f465fa", "node_id": "C_kwDOAducuNoAKDk0MGMyYWU2Y2QwNjQ4MTc1MTJhYTc2NDYzODZhMzFmYTlmNDY1ZmE", "commit": {"author": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-10-12T23:55:38Z"}, "committer": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-10-12T23:55:38Z"}, "message": "v2.14.6", "tree": {"sha": "4cdce13025cf16b86b6242c818df2501f3d84d5c", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/4cdce13025cf16b86b6242c818df2501f3d84d5c"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/940c2ae6cd064817512aa7646386a31fa9f465fa", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\niQIzBAABCAAdFiEEFPKZYCM7sDX3KZzmsfH5gHGEaXoFAmcLDH4ACgkQsfH5gHGE\naXp/Yw/9EYlGn4AbfdoIGSoVTjVN75m7/5nUScbyAbZkqG87GovCKSbUKChtRVP2\n5csMym9KszLZyQw5dskGeUfDcxmzZ84lttrJNBu47p0Wh0ZNRAq/mcDRlfgtBv6B\nudUQwXPnSuZIhU9LUz0yJm8/Bm5GPaQZyUPf5PjQrjjQeFL5IEP49UGsmeUQbXeO\nkc7d49Uor/tifeKSNblSzLwihqjQZJkc0JJG3dPNz8vMiDScerK6cygMDW1NYxiU\nq9hPXaoOk/QDqPmSkwuHaYB1UXXT0Ib+Quy6kv0lNoRsJ+2xYp+uPeiBr6WEV+A7\nYCwySlWfTC396q4QriAaBAdAmGrD7hzPTV5dSUUn0ydOP+xArtgR+rLMri+ARhPV\nz29zsYZz6/8k1FQ6MO80YEntqD3S0xXyDXsP2vRYVILG4+0hRXY1gE3RsqLSNqSh\njugwboYUp5igpfVZO0igNSJTLEze6vVaM0Et03KcWL7CCKrP5gtpAOXSPe/z+/fQ\nrMkhkIW9kz+J29OowW+/dMBnWRFR3WCDFDJG/Rgi/DlFAVWTd+LJ/iLE9X0I99dP\nV58s94gyfQZdgPcsFiRRi3wD6kNTU67LyBApGhwiAQeoJPvPCxaljn1hd2Tlpm0r\nn8G2QaLxFVWn8XxW8tulJcpjcNXiUbP0hm07zccDCcT3YjGYRs4=\n=z3c0\n-----END PGP SIGNATURE-----", "payload": "tree 4cdce13025cf16b86b6242c818df2501f3d84d5c\nparent 1cdfd5f30a6c4b080f4bab3225267b70123639a4\nauthor JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1728777338 -0700\ncommitter JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1728777338 -0700\n\nv2.14.6\n", "verified_at": "2024-11-12T16:04:51Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/940c2ae6cd064817512aa7646386a31fa9f465fa", "html_url": "https://github.com/Tautulli/Tautulli/commit/940c2ae6cd064817512aa7646386a31fa9f465fa", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/940c2ae6cd064817512aa7646386a31fa9f465fa/comments", "author": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "committer": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "1cdfd5f30a6c4b080f4bab3225267b70123639a4", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/1cdfd5f30a6c4b080f4bab3225267b70123639a4", "html_url": "https://github.com/Tautulli/Tautulli/commit/1cdfd5f30a6c4b080f4bab3225267b70123639a4"}]}, "status": "ahead", "ahead_by": 37, "behind_by": 0, "total_commits": 37, "commits": [{"sha": "2d3271376be809e4760b258fa7381dabfa701c43", "node_id": "C_kwDOAducuNoAKDJkMzI3MTM3NmJlODA5ZTQ3NjBiMjU4ZmE3MzgxZGFiZmE3MDFjNDM", "commit": {"author": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-11-02T18:13:37Z"}, "committer": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-11-02T18:13:37Z"}, "message": "Remove unused cache_image function\n\nFixes #2426", "tree": {"sha": "0502463e0c44f3da33235485d9367f15f91a6d7e", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/0502463e0c44f3da33235485d9367f15f91a6d7e"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/2d3271376be809e4760b258fa7381dabfa701c43", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\niQIzBAABCAAdFiEEFPKZYCM7sDX3KZzmsfH5gHGEaXoFAmcma9IACgkQsfH5gHGE\naXo1eQ//TrKBxEuDMoe0OYHU581t6s7OJA8heFOUf2boqzxYOyb2e0x8UWSFusDj\n/Y8I4OmlsbdS9a12VAqhJFmewaQpBWlMy5ZdN48rzTza5ARRD8Mt54Yu7lwAl1fE\nLdMalo8tctTsWpHmn7ShMB4XS7Cpnmj19xNaLESbqcLPqypi08FrEoRUPl/wj+Uj\nVfCusYHzMVQbjnFJY4HnF5gZXXAlRBsMHwXNETw7sf2QWvI7h3oSLw0jaRaw1abm\n/EVYBzHKsUiJ36y/9WIFPkFL6V1nR4VlO2JKLojqcpRjKRRSWLJ5ta9VM6Hrg3sb\n6M9qS85j4BQIgB5xv6Za5DWkrocT2sg8YbFnkYHmKbkugYLCZcEBK13LoZxwqMTZ\n2NBDJSez3gdN4Uz4XblkDVswWya69z6ZF/H4WORKBm4CZB4PYdcIY/+Zh8jJ8dDp\nsCyfu5LA0lUy3OMGC8qnaMPnPBVGdDSTkeYE5I5gXIuzv7r6J+VfEz935cqskZqG\n7nUzOHeZLw5CGmnTe8lwbQ/lGdJjTp5iTwI3PJoNOpEZp4e6J9CMi1V2bN0VABBa\nVMOxjNra09Ve0DQfDczswwSWAYB0pvUTy00dj8HJ8xYwiLAYLDGKufKIbuUE8g5A\nHmsUttDQGhuk56cUYMpH+8sVEEo0kL4DgP0FWWd/mMdLQCVlmnI=\n=DnUm\n-----END PGP SIGNATURE-----", "payload": "tree 0502463e0c44f3da33235485d9367f15f91a6d7e\nparent 940c2ae6cd064817512aa7646386a31fa9f465fa\nauthor JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1730571217 -0700\ncommitter JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1730571217 -0700\n\nRemove unused cache_image function\n\nFixes #2426\n", "verified_at": "2024-11-12T16:05:58Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/2d3271376be809e4760b258fa7381dabfa701c43", "html_url": "https://github.com/Tautulli/Tautulli/commit/2d3271376be809e4760b258fa7381dabfa701c43", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/2d3271376be809e4760b258fa7381dabfa701c43/comments", "author": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "committer": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "940c2ae6cd064817512aa7646386a31fa9f465fa", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/940c2ae6cd064817512aa7646386a31fa9f465fa", "html_url": "https://github.com/Tautulli/Tautulli/commit/940c2ae6cd064817512aa7646386a31fa9f465fa"}]}, {"sha": "2f3d24a0e75576694f364255817f0ad8d5c50e1d", "node_id": "C_kwDOAducuNoAKDJmM2QyNGEwZTc1NTc2Njk0ZjM2NDI1NTgxN2YwYWQ4ZDVjNTBlMWQ", "commit": {"author": {"name": "dependabot[bot]", "email": "49699333+dependabot[bot]@users.noreply.github.com", "date": "2024-11-16T22:47:38Z"}, "committer": {"name": "GitHub", "email": "noreply@github.com", "date": "2024-11-16T22:47:38Z"}, "message": "Bump simplejson from 3.19.2 to 3.19.3 (#2379)\n\n* Bump simplejson from 3.19.2 to 3.19.3\r\n\r\nBumps [simplejson](https://github.com/simplejson/simplejson) from 3.19.2 to 3.19.3.\r\n- [Release notes](https://github.com/simplejson/simplejson/releases)\r\n- [Changelog](https://github.com/simplejson/simplejson/blob/master/CHANGES.txt)\r\n- [Commits](https://github.com/simplejson/simplejson/compare/v3.19.2...v3.19.3)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: simplejson\r\n dependency-type: direct:production\r\n update-type: version-update:semver-patch\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update simplejson==3.19.3\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "tree": {"sha": "6507225d3afee083c596c478f123f4a3736e39fe", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/6507225d3afee083c596c478f123f4a3736e39fe"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/2f3d24a0e75576694f364255817f0ad8d5c50e1d", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJnOSEKCRC1aQ7uu5UhlAAAfdEQAKtqgSpX7SV+UyPOjd/f/wHa\nRuPHdjlcVGV6vGr8F6GeWp/QtjRtTiLhZvDKNQeNFMO2kwTya7KLkgvQF+HTdIzS\nhrLo5Z3GU7/+y4w0u7xPPqlDlCRaK1NPpM3/YFetOIrWE4V2JfK6XI6FkEbdV5vs\nPdPp5Mnmxy3SCj2XDyqQH+EOGCGyZ9MkPxkqTT5SJ9cZBPbI5QP/EIBGPqlnJNqP\n4eaEYH4e8k8jDeOGphO+mfJY/URmtrk3M3avPzErc0z9F8at+Dt7OECc7jf9vNzZ\nqvithYbpLywRx985ZMY8u4/uzefMrc9lcOvv7A4Qu7j/IPPC/wNFyEZM1WZxoKuS\nVCOQZJgRhH7+C8wXIBDo1cMbSfpqTvvO0a4o9w688y42RcPlT1mK+aU6UhV5aQIf\nVjxv6TlkxHY2jsB0qc2JOah4jEsfTQYU+9NA6YycgsXTwTRbd9qGxEAym9tj08rl\nXqUwwgn5K1jlm/C973VVqkf4n1BrwWaSs+p/WfaIr2ot8vbtWPPqTUk1GZas0yaQ\nFF8bWYGBADGbQrisZNNwbQR86cS7+L2gQX0BqIFzRxOiTq7dZFq4lZG3hE0rjn9o\nOj+tlX8vRuGyz5fsrOmTIEjELL51+aeg0scKig0IDQ71r/zCoQGn8N9CvWfZrMMU\n0Wi02e+86cnCy2BaQBc6\n=QBPp\n-----END PGP SIGNATURE-----\n", "payload": "tree 6507225d3afee083c596c478f123f4a3736e39fe\nparent 2d3271376be809e4760b258fa7381dabfa701c43\nauthor dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1731797258 -0800\ncommitter GitHub <noreply@github.com> 1731797258 -0800\n\nBump simplejson from 3.19.2 to 3.19.3 (#2379)\n\n* Bump simplejson from 3.19.2 to 3.19.3\r\n\r\nBumps [simplejson](https://github.com/simplejson/simplejson) from 3.19.2 to 3.19.3.\r\n- [Release notes](https://github.com/simplejson/simplejson/releases)\r\n- [Changelog](https://github.com/simplejson/simplejson/blob/master/CHANGES.txt)\r\n- [Commits](https://github.com/simplejson/simplejson/compare/v3.19.2...v3.19.3)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: simplejson\r\n dependency-type: direct:production\r\n update-type: version-update:semver-patch\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update simplejson==3.19.3\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "verified_at": "2024-11-16T22:48:08Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/2f3d24a0e75576694f364255817f0ad8d5c50e1d", "html_url": "https://github.com/Tautulli/Tautulli/commit/2f3d24a0e75576694f364255817f0ad8d5c50e1d", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/2f3d24a0e75576694f364255817f0ad8d5c50e1d/comments", "author": {"login": "dependabot[bot]", "id": 49699333, "node_id": "MDM6Qm90NDk2OTkzMzM=", "avatar_url": "https://avatars.githubusercontent.com/in/29110?v=4", "gravatar_id": "", "url": "https://api.github.com/users/dependabot%5Bbot%5D", "html_url": "https://github.com/apps/dependabot", "followers_url": "https://api.github.com/users/dependabot%5Bbot%5D/followers", "following_url": "https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}", "gists_url": "https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}", "starred_url": "https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/dependabot%5Bbot%5D/subscriptions", "organizations_url": "https://api.github.com/users/dependabot%5Bbot%5D/orgs", "repos_url": "https://api.github.com/users/dependabot%5Bbot%5D/repos", "events_url": "https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}", "received_events_url": "https://api.github.com/users/dependabot%5Bbot%5D/received_events", "type": "Bot", "user_view_type": "public", "site_admin": false}, "committer": {"login": "web-flow", "id": 19864447, "node_id": "MDQ6VXNlcjE5ODY0NDQ3", "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", "gravatar_id": "", "url": "https://api.github.com/users/web-flow", "html_url": "https://github.com/web-flow", "followers_url": "https://api.github.com/users/web-flow/followers", "following_url": "https://api.github.com/users/web-flow/following{/other_user}", "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", "organizations_url": "https://api.github.com/users/web-flow/orgs", "repos_url": "https://api.github.com/users/web-flow/repos", "events_url": "https://api.github.com/users/web-flow/events{/privacy}", "received_events_url": "https://api.github.com/users/web-flow/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "2d3271376be809e4760b258fa7381dabfa701c43", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/2d3271376be809e4760b258fa7381dabfa701c43", "html_url": "https://github.com/Tautulli/Tautulli/commit/2d3271376be809e4760b258fa7381dabfa701c43"}]}, {"sha": "d3f7eef84f1e8f88b93f4d86a41a4c39d093a4de", "node_id": "C_kwDOAducuNoAKGQzZjdlZWY4NGYxZThmODhiOTNmNGQ4NmE0MWE0YzM5ZDA5M2E0ZGU", "commit": {"author": {"name": "dependabot[bot]", "email": "49699333+dependabot[bot]@users.noreply.github.com", "date": "2024-11-16T22:47:48Z"}, "committer": {"name": "GitHub", "email": "noreply@github.com", "date": "2024-11-16T22:47:48Z"}, "message": "Bump pyparsing from 3.1.2 to 3.1.4 (#2388)\n\n* Bump pyparsing from 3.1.2 to 3.1.4\r\n\r\nBumps [pyparsing](https://github.com/pyparsing/pyparsing) from 3.1.2 to 3.1.4.\r\n- [Release notes](https://github.com/pyparsing/pyparsing/releases)\r\n- [Changelog](https://github.com/pyparsing/pyparsing/blob/master/CHANGES)\r\n- [Commits](https://github.com/pyparsing/pyparsing/compare/pyparsing_3.1.2...3.1.4)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: pyparsing\r\n dependency-type: direct:production\r\n update-type: version-update:semver-patch\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update pyparsing==3.1.4\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "tree": {"sha": "b3a2d73a5de6903c8781ccd093dce41fcddd6bea", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/b3a2d73a5de6903c8781ccd093dce41fcddd6bea"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/d3f7eef84f1e8f88b93f4d86a41a4c39d093a4de", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJnOSEUCRC1aQ7uu5UhlAAAUtoQAFDjp/5d59aeMmuK3W69xcs3\n7xHIvQhvbOUrPGSJxjF7hvaJo1Q1V3+IGtlo3OCtS137pHERtTXWZDLdqo/XCLDv\n9e5JVvEs0EEmyHJFPnSpXshKzYdXmCFViYCyErMgN2CbEAjj8nrw6KWFFzQlVdyk\nIWv4BCmdnKSCsPrv14ieLT4+OTFFpzIgFy2aH7x9UydWBEWlMXDLMqHPvgApG6L2\nOiFCG7uohm4AIX9uQf90uGklqtGLE4VQiFRwfzXVaSE7Rqm/AXf8ADrrK19rmYj3\nTa9PCjPMgcDR2bsu+fYrpz83f6WhhB8eKUrkkgaRYYRWjgeA/3XgTghM8DcY6Eoh\nXjiwFKteVLUkcHlKLOz+n+OHv+PV7grTxDn92AphokdnQUfaJ/9w6HmyDNXbQrxz\nTf39xj//rUm9AV4hdN8WJneECBhUyInzQBp1EzsTJPD4aFKP5SYyyXinWqYDTxhZ\n2mjtiq+9zkkcMB+b9o92oqFZwhL/8Ru/hxIZpyhzfdJb4HcTixMSXn2NpUFZkdGt\nA/zpxW0k39rMHQVKl/mPZqqVhk4RjJiriF/Ndk+IsPT+c66NbfKgoGj46Ep1Ir0m\nEKsi1mC4BFU/9o9GGbE9Nztj3jLr5DmnTo6ivQ9vGHGYk4ZdRGvjwmqTBpuwgtSq\njtpI/CY1iC3ct0T2J4yV\n=tNge\n-----END PGP SIGNATURE-----\n", "payload": "tree b3a2d73a5de6903c8781ccd093dce41fcddd6bea\nparent 2f3d24a0e75576694f364255817f0ad8d5c50e1d\nauthor dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1731797268 -0800\ncommitter GitHub <noreply@github.com> 1731797268 -0800\n\nBump pyparsing from 3.1.2 to 3.1.4 (#2388)\n\n* Bump pyparsing from 3.1.2 to 3.1.4\r\n\r\nBumps [pyparsing](https://github.com/pyparsing/pyparsing) from 3.1.2 to 3.1.4.\r\n- [Release notes](https://github.com/pyparsing/pyparsing/releases)\r\n- [Changelog](https://github.com/pyparsing/pyparsing/blob/master/CHANGES)\r\n- [Commits](https://github.com/pyparsing/pyparsing/compare/pyparsing_3.1.2...3.1.4)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: pyparsing\r\n dependency-type: direct:production\r\n update-type: version-update:semver-patch\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update pyparsing==3.1.4\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "verified_at": "2024-11-16T22:48:08Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/d3f7eef84f1e8f88b93f4d86a41a4c39d093a4de", "html_url": "https://github.com/Tautulli/Tautulli/commit/d3f7eef84f1e8f88b93f4d86a41a4c39d093a4de", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/d3f7eef84f1e8f88b93f4d86a41a4c39d093a4de/comments", "author": {"login": "dependabot[bot]", "id": 49699333, "node_id": "MDM6Qm90NDk2OTkzMzM=", "avatar_url": "https://avatars.githubusercontent.com/in/29110?v=4", "gravatar_id": "", "url": "https://api.github.com/users/dependabot%5Bbot%5D", "html_url": "https://github.com/apps/dependabot", "followers_url": "https://api.github.com/users/dependabot%5Bbot%5D/followers", "following_url": "https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}", "gists_url": "https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}", "starred_url": "https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/dependabot%5Bbot%5D/subscriptions", "organizations_url": "https://api.github.com/users/dependabot%5Bbot%5D/orgs", "repos_url": "https://api.github.com/users/dependabot%5Bbot%5D/repos", "events_url": "https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}", "received_events_url": "https://api.github.com/users/dependabot%5Bbot%5D/received_events", "type": "Bot", "user_view_type": "public", "site_admin": false}, "committer": {"login": "web-flow", "id": 19864447, "node_id": "MDQ6VXNlcjE5ODY0NDQ3", "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", "gravatar_id": "", "url": "https://api.github.com/users/web-flow", "html_url": "https://github.com/web-flow", "followers_url": "https://api.github.com/users/web-flow/followers", "following_url": "https://api.github.com/users/web-flow/following{/other_user}", "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", "organizations_url": "https://api.github.com/users/web-flow/orgs", "repos_url": "https://api.github.com/users/web-flow/repos", "events_url": "https://api.github.com/users/web-flow/events{/privacy}", "received_events_url": "https://api.github.com/users/web-flow/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "2f3d24a0e75576694f364255817f0ad8d5c50e1d", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/2f3d24a0e75576694f364255817f0ad8d5c50e1d", "html_url": "https://github.com/Tautulli/Tautulli/commit/2f3d24a0e75576694f364255817f0ad8d5c50e1d"}]}, {"sha": "f3a2c02e960d3a101205302628a22e1615163e48", "node_id": "C_kwDOAducuNoAKGYzYTJjMDJlOTYwZDNhMTAxMjA1MzAyNjI4YTIyZTE2MTUxNjNlNDg", "commit": {"author": {"name": "dependabot[bot]", "email": "49699333+dependabot[bot]@users.noreply.github.com", "date": "2024-11-16T22:47:58Z"}, "committer": {"name": "GitHub", "email": "noreply@github.com", "date": "2024-11-16T22:47:58Z"}, "message": "Bump certifi from 2024.7.4 to 2024.8.30 (#2391)\n\n* Bump certifi from 2024.7.4 to 2024.8.30\r\n\r\nBumps [certifi](https://github.com/certifi/python-certifi) from 2024.7.4 to 2024.8.30.\r\n- [Commits](https://github.com/certifi/python-certifi/compare/2024.07.04...2024.08.30)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: certifi\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update certifi==2024.8.30\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "tree": {"sha": "d9d7d3acd5e19e54fb99570cb1399e82d7f7c81a", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/d9d7d3acd5e19e54fb99570cb1399e82d7f7c81a"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/f3a2c02e960d3a101205302628a22e1615163e48", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJnOSEeCRC1aQ7uu5UhlAAA/YIQAINpLsTXtYYd8Ebi0deWN7Hk\nfDxb9aYgb41yD+Ig2CxQ8lD2D4B51wfqT7hG9g601rNi7Sqmq6LbN5ROwwtMaL30\nARYc45f8n5Wnk8dVDuocZct2347m3ZBEL2/iPxcOa/SOIULPHJrALKds/88adjYT\nSCHN2J+5C/eax+AWvGQIVoefOiZp495t6HyyCGiMOiQROQW14ohE8l7fsCbhx/NE\nOtrqnX3axxDC6D0QQBKQ0X3m0S40cTFCNhg1QSeRf0MoWhts6KR+gWFxzk+HkhNS\n/Ci45ow6DKUrrJpWrTQ5qN3wf1G3nLUiy8+GcNZK0M8i3aZ511gykhSGUT6A1HyF\nCEAAbvrtyxUeno/aRAWE428NRLnkgwz9oW0Oz86mIhX7hvNxBH66LwN4U3oXztqH\nec8Lkz4oLJhiphrPNL3l4Pkfr2GtYFaZES6qwVBx+RX4vPO9rUTKNetJXytn/3uF\nMBV70LMiJEanQjARcbMXoOCNvXiujLY5awxU86M9+ErCCfBCuVs2eAIie9nbD5Cf\nCHaSc+PT2OWHgT61MjSZi84kjgKDE2uqe0T8Bs6+GBK2ZGZm9gKx85JHaWsTOuvn\nIo+VrgMi31iBdyVOne6CSHSxOBO57s4DQMeDgKSZyv02BHx1QDsoICLCCLy7F8xm\nyaDQlEdsDQ7CiRogZ3Et\n=L/AE\n-----END PGP SIGNATURE-----\n", "payload": "tree d9d7d3acd5e19e54fb99570cb1399e82d7f7c81a\nparent d3f7eef84f1e8f88b93f4d86a41a4c39d093a4de\nauthor dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1731797278 -0800\ncommitter GitHub <noreply@github.com> 1731797278 -0800\n\nBump certifi from 2024.7.4 to 2024.8.30 (#2391)\n\n* Bump certifi from 2024.7.4 to 2024.8.30\r\n\r\nBumps [certifi](https://github.com/certifi/python-certifi) from 2024.7.4 to 2024.8.30.\r\n- [Commits](https://github.com/certifi/python-certifi/compare/2024.07.04...2024.08.30)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: certifi\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update certifi==2024.8.30\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "verified_at": "2024-11-16T22:48:08Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/f3a2c02e960d3a101205302628a22e1615163e48", "html_url": "https://github.com/Tautulli/Tautulli/commit/f3a2c02e960d3a101205302628a22e1615163e48", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/f3a2c02e960d3a101205302628a22e1615163e48/comments", "author": {"login": "dependabot[bot]", "id": 49699333, "node_id": "MDM6Qm90NDk2OTkzMzM=", "avatar_url": "https://avatars.githubusercontent.com/in/29110?v=4", "gravatar_id": "", "url": "https://api.github.com/users/dependabot%5Bbot%5D", "html_url": "https://github.com/apps/dependabot", "followers_url": "https://api.github.com/users/dependabot%5Bbot%5D/followers", "following_url": "https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}", "gists_url": "https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}", "starred_url": "https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/dependabot%5Bbot%5D/subscriptions", "organizations_url": "https://api.github.com/users/dependabot%5Bbot%5D/orgs", "repos_url": "https://api.github.com/users/dependabot%5Bbot%5D/repos", "events_url": "https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}", "received_events_url": "https://api.github.com/users/dependabot%5Bbot%5D/received_events", "type": "Bot", "user_view_type": "public", "site_admin": false}, "committer": {"login": "web-flow", "id": 19864447, "node_id": "MDQ6VXNlcjE5ODY0NDQ3", "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", "gravatar_id": "", "url": "https://api.github.com/users/web-flow", "html_url": "https://github.com/web-flow", "followers_url": "https://api.github.com/users/web-flow/followers", "following_url": "https://api.github.com/users/web-flow/following{/other_user}", "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", "organizations_url": "https://api.github.com/users/web-flow/orgs", "repos_url": "https://api.github.com/users/web-flow/repos", "events_url": "https://api.github.com/users/web-flow/events{/privacy}", "received_events_url": "https://api.github.com/users/web-flow/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "d3f7eef84f1e8f88b93f4d86a41a4c39d093a4de", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/d3f7eef84f1e8f88b93f4d86a41a4c39d093a4de", "html_url": "https://github.com/Tautulli/Tautulli/commit/d3f7eef84f1e8f88b93f4d86a41a4c39d093a4de"}]}, {"sha": "01589cb8b0bf0c6578da1d8792d8ba14e60546c0", "node_id": "C_kwDOAducuNoAKDAxNTg5Y2I4YjBiZjBjNjU3OGRhMWQ4NzkyZDhiYTE0ZTYwNTQ2YzA", "commit": {"author": {"name": "dependabot[bot]", "email": "49699333+dependabot[bot]@users.noreply.github.com", "date": "2024-11-16T22:48:10Z"}, "committer": {"name": "GitHub", "email": "noreply@github.com", "date": "2024-11-16T22:48:10Z"}, "message": "Bump importlib-resources from 6.4.0 to 6.4.5 (#2394)\n\n* Bump importlib-resources from 6.4.0 to 6.4.5\r\n\r\nBumps [importlib-resources](https://github.com/python/importlib_resources) from 6.4.0 to 6.4.5.\r\n- [Release notes](https://github.com/python/importlib_resources/releases)\r\n- [Changelog](https://github.com/python/importlib_resources/blob/main/NEWS.rst)\r\n- [Commits](https://github.com/python/importlib_resources/compare/v6.4.0...v6.4.5)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: importlib-resources\r\n dependency-type: direct:production\r\n update-type: version-update:semver-patch\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update importlib-resources==6.4.5\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "tree": {"sha": "c1a962fb921ff433ded63ada8bebab7993b1900b", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/c1a962fb921ff433ded63ada8bebab7993b1900b"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/01589cb8b0bf0c6578da1d8792d8ba14e60546c0", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJnOSEqCRC1aQ7uu5UhlAAAKGUQAHKqaPasA2nV+kcj0+VjF9M9\n8DRAPJ/O28ZqJeHojtrtUlAc6tQ60PMxy0STWSM3gfLcsolIbsTK6rEHzRAi4lXA\nAHgO0mZSQSYDMTJP4fw5K4NFm6xTvvhzQqfe4DXzdGCslU5wUabYd/jmohx0gZCN\nH6PSKlyT419FpJNq8nsP+v1psSGADx77xrbhFQE2Q9oVTGGpWTID5HY/z4LoovpS\nn8u7Zj5/IHfXEHZ50gq9THPiW15+suRAnaAfYQyq98gSI3tMLJRiYxPsmAY8UrLT\nI+ZOEkz6+rmqXBgaxY+fNMA+k0UIW9Sa4v5lAkKNy+wXFSmUB/sh2DwDy/AqRV/H\nDnOTdz/z5x6ILz/ugxkEPlWxMvSYfm8V68r+gDeircD8kDkyOXWqVMgDskeduwCe\nDMXqNr/cFzKTNjRWdjiVKgPPELGmRgP33JwbzyPJ3yiQIPKRCqeLNgZwnqfSK5rF\n2UboWPX4JeCgLyuu5rPRMxb7SOYjptjFBQubRcEo/seVh6NxhFFCGHObDzA/M/Xl\niBtehWzocQPkwKtDu7vurJVG6WEpWu8FyhZFwh1QqdJLsAFj8Nsezq7dr2HQD07a\nrTQPl5hA013ve2TgzybSKbpt6k1sZo1dIRc+Ktl2QYqh/qL8A6Xw/7L8O233N4SP\njiIBsEvTpGQr6w2v9+I5\n=JGq7\n-----END PGP SIGNATURE-----\n", "payload": "tree c1a962fb921ff433ded63ada8bebab7993b1900b\nparent f3a2c02e960d3a101205302628a22e1615163e48\nauthor dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1731797290 -0800\ncommitter GitHub <noreply@github.com> 1731797290 -0800\n\nBump importlib-resources from 6.4.0 to 6.4.5 (#2394)\n\n* Bump importlib-resources from 6.4.0 to 6.4.5\r\n\r\nBumps [importlib-resources](https://github.com/python/importlib_resources) from 6.4.0 to 6.4.5.\r\n- [Release notes](https://github.com/python/importlib_resources/releases)\r\n- [Changelog](https://github.com/python/importlib_resources/blob/main/NEWS.rst)\r\n- [Commits](https://github.com/python/importlib_resources/compare/v6.4.0...v6.4.5)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: importlib-resources\r\n dependency-type: direct:production\r\n update-type: version-update:semver-patch\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update importlib-resources==6.4.5\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "verified_at": "2024-11-16T22:48:21Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/01589cb8b0bf0c6578da1d8792d8ba14e60546c0", "html_url": "https://github.com/Tautulli/Tautulli/commit/01589cb8b0bf0c6578da1d8792d8ba14e60546c0", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/01589cb8b0bf0c6578da1d8792d8ba14e60546c0/comments", "author": {"login": "dependabot[bot]", "id": 49699333, "node_id": "MDM6Qm90NDk2OTkzMzM=", "avatar_url": "https://avatars.githubusercontent.com/in/29110?v=4", "gravatar_id": "", "url": "https://api.github.com/users/dependabot%5Bbot%5D", "html_url": "https://github.com/apps/dependabot", "followers_url": "https://api.github.com/users/dependabot%5Bbot%5D/followers", "following_url": "https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}", "gists_url": "https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}", "starred_url": "https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/dependabot%5Bbot%5D/subscriptions", "organizations_url": "https://api.github.com/users/dependabot%5Bbot%5D/orgs", "repos_url": "https://api.github.com/users/dependabot%5Bbot%5D/repos", "events_url": "https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}", "received_events_url": "https://api.github.com/users/dependabot%5Bbot%5D/received_events", "type": "Bot", "user_view_type": "public", "site_admin": false}, "committer": {"login": "web-flow", "id": 19864447, "node_id": "MDQ6VXNlcjE5ODY0NDQ3", "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", "gravatar_id": "", "url": "https://api.github.com/users/web-flow", "html_url": "https://github.com/web-flow", "followers_url": "https://api.github.com/users/web-flow/followers", "following_url": "https://api.github.com/users/web-flow/following{/other_user}", "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", "organizations_url": "https://api.github.com/users/web-flow/orgs", "repos_url": "https://api.github.com/users/web-flow/repos", "events_url": "https://api.github.com/users/web-flow/events{/privacy}", "received_events_url": "https://api.github.com/users/web-flow/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "f3a2c02e960d3a101205302628a22e1615163e48", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/f3a2c02e960d3a101205302628a22e1615163e48", "html_url": "https://github.com/Tautulli/Tautulli/commit/f3a2c02e960d3a101205302628a22e1615163e48"}]}, {"sha": "e69852fa0e3c8d0d34dc982f712069ac4360ecbf", "node_id": "C_kwDOAducuNoAKGU2OTg1MmZhMGUzYzhkMGQzNGRjOTgyZjcxMjA2OWFjNDM2MGVjYmY", "commit": {"author": {"name": "dependabot[bot]", "email": "49699333+dependabot[bot]@users.noreply.github.com", "date": "2024-11-16T22:48:53Z"}, "committer": {"name": "GitHub", "email": "noreply@github.com", "date": "2024-11-16T22:48:53Z"}, "message": "Bump importlib-metadata from 8.2.0 to 8.5.0 (#2397)\n\n* Bump importlib-metadata from 8.2.0 to 8.5.0\r\n\r\nBumps [importlib-metadata](https://github.com/python/importlib_metadata) from 8.2.0 to 8.5.0.\r\n- [Release notes](https://github.com/python/importlib_metadata/releases)\r\n- [Changelog](https://github.com/python/importlib_metadata/blob/main/NEWS.rst)\r\n- [Commits](https://github.com/python/importlib_metadata/compare/v8.2.0...v8.5.0)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: importlib-metadata\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update importlib-metadata==8.5.0\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "tree": {"sha": "5be65f8537e29ad6c1a2acb28c688607949969ff", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/5be65f8537e29ad6c1a2acb28c688607949969ff"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/e69852fa0e3c8d0d34dc982f712069ac4360ecbf", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJnOSFVCRC1aQ7uu5UhlAAAIE0QABYAA4D4vQ/hyraf98eUHknv\nME5qWQAmTQUMM5I0XfbvsFt11FWIXto5VvdPhh/8pwAwQGp6SBpGgNINW7gNioLU\nxZyA8GcpdN1Gb96kjW0cKPVOlJb4eFMsKx+mBC5PbHqDHVYVE/OzN7ydwQ9raav0\nEAcTEpl/bOH21ulwYLnVB0cUWkFQfbu0PY4w9erwVvp8Ny8KOVmd9RtgViyHh9i8\nPg1I5ZeMGru+6U5pg4Wjj7cR9MZcOQQumWhiWFo0cIP07veuP7hDfIsi7Pp4+zq2\nZpE44qr1PpBO0fEyuBe6oxOH8AGakCSDZtQeH1eYPmHQJkotC09dm29/q1JynMA4\nxkjzMcmSQ/WQFOI5zuM/hvqQfbDuYVu9FvBZDBMN8KH9/FdaXfR12tJnsElk+n2a\n6zMEa1ZFIWRGhSenEyok82l7D5jCs7Xao9eLuufH4fNlUW7UG24eSZB611FydeuR\n8cu3oIzUeWlV9aWcho3aYNF49oc3aOpXG/iCQq/vDF6kjPkVTm/xYbBjols7Jr7/\nzqAoXuNvoOVJ6K5mABtr+2udHkoK60zWmsfmFHT3flYa+ehBjKmu+Ln85DAYJ4ra\n3tvGemGwkWhvM35KLuigfT9hgEvQ8iyqKdyH6pKTzL78kSN/CN1iL1zO+SC5uPeI\nfMn6LEwhQqvnuCe0tYwF\n=iteZ\n-----END PGP SIGNATURE-----\n", "payload": "tree 5be65f8537e29ad6c1a2acb28c688607949969ff\nparent 01589cb8b0bf0c6578da1d8792d8ba14e60546c0\nauthor dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1731797333 -0800\ncommitter GitHub <noreply@github.com> 1731797333 -0800\n\nBump importlib-metadata from 8.2.0 to 8.5.0 (#2397)\n\n* Bump importlib-metadata from 8.2.0 to 8.5.0\r\n\r\nBumps [importlib-metadata](https://github.com/python/importlib_metadata) from 8.2.0 to 8.5.0.\r\n- [Release notes](https://github.com/python/importlib_metadata/releases)\r\n- [Changelog](https://github.com/python/importlib_metadata/blob/main/NEWS.rst)\r\n- [Commits](https://github.com/python/importlib_metadata/compare/v8.2.0...v8.5.0)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: importlib-metadata\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update importlib-metadata==8.5.0\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "verified_at": "2024-11-16T22:49:05Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/e69852fa0e3c8d0d34dc982f712069ac4360ecbf", "html_url": "https://github.com/Tautulli/Tautulli/commit/e69852fa0e3c8d0d34dc982f712069ac4360ecbf", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/e69852fa0e3c8d0d34dc982f712069ac4360ecbf/comments", "author": {"login": "dependabot[bot]", "id": 49699333, "node_id": "MDM6Qm90NDk2OTkzMzM=", "avatar_url": "https://avatars.githubusercontent.com/in/29110?v=4", "gravatar_id": "", "url": "https://api.github.com/users/dependabot%5Bbot%5D", "html_url": "https://github.com/apps/dependabot", "followers_url": "https://api.github.com/users/dependabot%5Bbot%5D/followers", "following_url": "https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}", "gists_url": "https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}", "starred_url": "https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/dependabot%5Bbot%5D/subscriptions", "organizations_url": "https://api.github.com/users/dependabot%5Bbot%5D/orgs", "repos_url": "https://api.github.com/users/dependabot%5Bbot%5D/repos", "events_url": "https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}", "received_events_url": "https://api.github.com/users/dependabot%5Bbot%5D/received_events", "type": "Bot", "user_view_type": "public", "site_admin": false}, "committer": {"login": "web-flow", "id": 19864447, "node_id": "MDQ6VXNlcjE5ODY0NDQ3", "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", "gravatar_id": "", "url": "https://api.github.com/users/web-flow", "html_url": "https://github.com/web-flow", "followers_url": "https://api.github.com/users/web-flow/followers", "following_url": "https://api.github.com/users/web-flow/following{/other_user}", "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", "organizations_url": "https://api.github.com/users/web-flow/orgs", "repos_url": "https://api.github.com/users/web-flow/repos", "events_url": "https://api.github.com/users/web-flow/events{/privacy}", "received_events_url": "https://api.github.com/users/web-flow/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "01589cb8b0bf0c6578da1d8792d8ba14e60546c0", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/01589cb8b0bf0c6578da1d8792d8ba14e60546c0", "html_url": "https://github.com/Tautulli/Tautulli/commit/01589cb8b0bf0c6578da1d8792d8ba14e60546c0"}]}, {"sha": "48b1c7b522408cfb858f5a0639043aa7a43113bc", "node_id": "C_kwDOAducuNoAKDQ4YjFjN2I1MjI0MDhjZmI4NThmNWEwNjM5MDQzYWE3YTQzMTEzYmM", "commit": {"author": {"name": "dependabot[bot]", "email": "49699333+dependabot[bot]@users.noreply.github.com", "date": "2024-11-16T22:49:08Z"}, "committer": {"name": "GitHub", "email": "noreply@github.com", "date": "2024-11-16T22:49:08Z"}, "message": "Bump pytz from 2024.1 to 2024.2 (#2398)\n\n* Bump pytz from 2024.1 to 2024.2\r\n\r\nBumps [pytz](https://github.com/stub42/pytz) from 2024.1 to 2024.2.\r\n- [Release notes](https://github.com/stub42/pytz/releases)\r\n- [Commits](https://github.com/stub42/pytz/compare/release_2024.1...release_2024.2)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: pytz\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update pytz==2024.2\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "tree": {"sha": "c3244efa2ab3658f6ae76e08b8ef4ae713ba1d8a", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/c3244efa2ab3658f6ae76e08b8ef4ae713ba1d8a"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/48b1c7b522408cfb858f5a0639043aa7a43113bc", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJnOSFkCRC1aQ7uu5UhlAAAv6IQAH3o0YRm1jGmsT23XYTtK9uR\nPEQAeEX35f0CNaeNmIpdiQqAaSBEAEl1XtDpDR90dtMlmKTb4h/b6nrNq9uY60z8\nroIAYHmGg2VfV8eZndeeOVrnMfo2TlqtU4TZK8l3jBKkIYzng/FIcT3NQPOsDNoZ\nT7+SWDupOAYVQACgS6QnSray3YeRh4ZOkTezKvQbBjMwT97s414/19LV0Cu8zkgm\n+qkA5etuGk/JIhI2hKoy7JfQg4/pNyzWcYXwd6QArKggpkIPrplJcDGg3E76gGf1\nepVjDMHFDdz/um2MoAJEi7Nqn6fQqDqkkKIfMacgXmP7FypRt4v6I0U1atcsad/q\nGFXcoPbKEOasd1umLoY9yZQHZLFE9spJqw555N+nUY0GZdaoIa8cKo4A9u7vlXE7\niZCaZXFdXvk56GyTSo0CvMc3q2bUd5c+ABP2Cm5g+1VK+zOJjMEWRwucWsw4vqcL\nX9sDxzjpImkI+lwmeetM28/A7F5iiSBU8T2GXCItCdXq1uvlXVS5IlTg3xUr8T0Z\nFDxvMo3ndjSZ7MS3MeBzqJCcMtg20H864RJjBH/demxcrELhrMncjNP/kVGSYg80\ndO2/IMIl8Gdo6fjbkVnzSd29ynH8tat5TBtUJcqrvAusarZSGISsTeovATwzET9w\naNVz9eLY2mqK7DeYpi4s\n=T7Sx\n-----END PGP SIGNATURE-----\n", "payload": "tree c3244efa2ab3658f6ae76e08b8ef4ae713ba1d8a\nparent e69852fa0e3c8d0d34dc982f712069ac4360ecbf\nauthor dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1731797348 -0800\ncommitter GitHub <noreply@github.com> 1731797348 -0800\n\nBump pytz from 2024.1 to 2024.2 (#2398)\n\n* Bump pytz from 2024.1 to 2024.2\r\n\r\nBumps [pytz](https://github.com/stub42/pytz) from 2024.1 to 2024.2.\r\n- [Release notes](https://github.com/stub42/pytz/releases)\r\n- [Commits](https://github.com/stub42/pytz/compare/release_2024.1...release_2024.2)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: pytz\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update pytz==2024.2\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "verified_at": "2024-11-16T22:49:22Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/48b1c7b522408cfb858f5a0639043aa7a43113bc", "html_url": "https://github.com/Tautulli/Tautulli/commit/48b1c7b522408cfb858f5a0639043aa7a43113bc", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/48b1c7b522408cfb858f5a0639043aa7a43113bc/comments", "author": {"login": "dependabot[bot]", "id": 49699333, "node_id": "MDM6Qm90NDk2OTkzMzM=", "avatar_url": "https://avatars.githubusercontent.com/in/29110?v=4", "gravatar_id": "", "url": "https://api.github.com/users/dependabot%5Bbot%5D", "html_url": "https://github.com/apps/dependabot", "followers_url": "https://api.github.com/users/dependabot%5Bbot%5D/followers", "following_url": "https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}", "gists_url": "https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}", "starred_url": "https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/dependabot%5Bbot%5D/subscriptions", "organizations_url": "https://api.github.com/users/dependabot%5Bbot%5D/orgs", "repos_url": "https://api.github.com/users/dependabot%5Bbot%5D/repos", "events_url": "https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}", "received_events_url": "https://api.github.com/users/dependabot%5Bbot%5D/received_events", "type": "Bot", "user_view_type": "public", "site_admin": false}, "committer": {"login": "web-flow", "id": 19864447, "node_id": "MDQ6VXNlcjE5ODY0NDQ3", "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", "gravatar_id": "", "url": "https://api.github.com/users/web-flow", "html_url": "https://github.com/web-flow", "followers_url": "https://api.github.com/users/web-flow/followers", "following_url": "https://api.github.com/users/web-flow/following{/other_user}", "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", "organizations_url": "https://api.github.com/users/web-flow/orgs", "repos_url": "https://api.github.com/users/web-flow/repos", "events_url": "https://api.github.com/users/web-flow/events{/privacy}", "received_events_url": "https://api.github.com/users/web-flow/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "e69852fa0e3c8d0d34dc982f712069ac4360ecbf", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/e69852fa0e3c8d0d34dc982f712069ac4360ecbf", "html_url": "https://github.com/Tautulli/Tautulli/commit/e69852fa0e3c8d0d34dc982f712069ac4360ecbf"}]}, {"sha": "bf07912711bef3274a3393e89855238f77c6565d", "node_id": "C_kwDOAducuNoAKGJmMDc5MTI3MTFiZWYzMjc0YTMzOTNlODk4NTUyMzhmNzdjNjU2NWQ", "commit": {"author": {"name": "dependabot[bot]", "email": "49699333+dependabot[bot]@users.noreply.github.com", "date": "2024-11-16T22:50:57Z"}, "committer": {"name": "GitHub", "email": "noreply@github.com", "date": "2024-11-16T22:50:57Z"}, "message": "Bump idna from 3.7 to 3.10 (#2400)\n\n* Bump idna from 3.7 to 3.10\r\n\r\nBumps [idna](https://github.com/kjd/idna) from 3.7 to 3.10.\r\n- [Release notes](https://github.com/kjd/idna/releases)\r\n- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.rst)\r\n- [Commits](https://github.com/kjd/idna/compare/v3.7...v3.10)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: idna\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update idna==3.10\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "tree": {"sha": "345375d5dac100925967a823af60e6fbaab6ebf4", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/345375d5dac100925967a823af60e6fbaab6ebf4"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/bf07912711bef3274a3393e89855238f77c6565d", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJnOSHRCRC1aQ7uu5UhlAAAiFkQAJX1RIbHmpaDVOXG0ju0yG5W\nJAkQ07rNTBB4Dm4UN1FE4GmEDSIhM+2cZRw+nm83YNZYwCGLVFw+3pzvqM2kCY9m\ngNZmt2tWCjCRx8KDSXHycJnmE3jBXlRrnzBuX3lCy51Eg3JY9gpxmuIotuCHWuMP\ne/ZYS3GHGa8A0cvrlXjqgKd9O7ZOI3PsVNBJHny+WEI0WrozpABRbLAGoXmBLJ1K\nF9zr03NG55I4hQhDmOkPm+f/Dw/JdXUBNnlXOHl/m0Gx+kZ5gkmU5UlC6uf4IAPp\nSi+YX1xMuHbKtDaL2+5inI6f6ALdiWBEi+he/k2mKjdTFdAxeFVddkjFhSGpXlMd\nDXLcTjmBbs2ghO32Xev5Bt2QIoyFNoDbKT/Ga93SRkWPp5T81dlq4fEOOQMKpiu/\n4yD40P22zCvzcX5e43bmKzDjN64tApORVvvbBr5KVvwE52FX02rZ49yc4LN9LlwS\n1n6zAD1hfOtDPIJKxDCT9UOUGCpI1jg3Yttv79tSjC5+ED/5I7tDlIfWbPc1xoF5\nb9IQ4yMr+mcIVaKxsML49xYu/r/2m3DWVZxS6mMzBd9PULTsfsW72z9QErzOiKnY\nYPLNI2VTz+LlHYy9t9gyOHy+GBfKUeN4O7MdYERCwgsibpZ6FOYVhwwZ9W+Z3BsF\nRoenv+33vq6/oyIbYQ+g\n=aFLP\n-----END PGP SIGNATURE-----\n", "payload": "tree 345375d5dac100925967a823af60e6fbaab6ebf4\nparent 48b1c7b522408cfb858f5a0639043aa7a43113bc\nauthor dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1731797457 -0800\ncommitter GitHub <noreply@github.com> 1731797457 -0800\n\nBump idna from 3.7 to 3.10 (#2400)\n\n* Bump idna from 3.7 to 3.10\r\n\r\nBumps [idna](https://github.com/kjd/idna) from 3.7 to 3.10.\r\n- [Release notes](https://github.com/kjd/idna/releases)\r\n- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.rst)\r\n- [Commits](https://github.com/kjd/idna/compare/v3.7...v3.10)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: idna\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update idna==3.10\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "verified_at": "2024-11-16T22:51:00Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/bf07912711bef3274a3393e89855238f77c6565d", "html_url": "https://github.com/Tautulli/Tautulli/commit/bf07912711bef3274a3393e89855238f77c6565d", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/bf07912711bef3274a3393e89855238f77c6565d/comments", "author": {"login": "dependabot[bot]", "id": 49699333, "node_id": "MDM6Qm90NDk2OTkzMzM=", "avatar_url": "https://avatars.githubusercontent.com/in/29110?v=4", "gravatar_id": "", "url": "https://api.github.com/users/dependabot%5Bbot%5D", "html_url": "https://github.com/apps/dependabot", "followers_url": "https://api.github.com/users/dependabot%5Bbot%5D/followers", "following_url": "https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}", "gists_url": "https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}", "starred_url": "https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/dependabot%5Bbot%5D/subscriptions", "organizations_url": "https://api.github.com/users/dependabot%5Bbot%5D/orgs", "repos_url": "https://api.github.com/users/dependabot%5Bbot%5D/repos", "events_url": "https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}", "received_events_url": "https://api.github.com/users/dependabot%5Bbot%5D/received_events", "type": "Bot", "user_view_type": "public", "site_admin": false}, "committer": {"login": "web-flow", "id": 19864447, "node_id": "MDQ6VXNlcjE5ODY0NDQ3", "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", "gravatar_id": "", "url": "https://api.github.com/users/web-flow", "html_url": "https://github.com/web-flow", "followers_url": "https://api.github.com/users/web-flow/followers", "following_url": "https://api.github.com/users/web-flow/following{/other_user}", "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", "organizations_url": "https://api.github.com/users/web-flow/orgs", "repos_url": "https://api.github.com/users/web-flow/repos", "events_url": "https://api.github.com/users/web-flow/events{/privacy}", "received_events_url": "https://api.github.com/users/web-flow/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "48b1c7b522408cfb858f5a0639043aa7a43113bc", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/48b1c7b522408cfb858f5a0639043aa7a43113bc", "html_url": "https://github.com/Tautulli/Tautulli/commit/48b1c7b522408cfb858f5a0639043aa7a43113bc"}]}, {"sha": "025e8bcf58b320805baca8d74e2ab9afd4c04281", "node_id": "C_kwDOAducuNoAKDAyNWU4YmNmNThiMzIwODA1YmFjYThkNzRlMmFiOWFmZDRjMDQyODE", "commit": {"author": {"name": "dependabot[bot]", "email": "49699333+dependabot[bot]@users.noreply.github.com", "date": "2024-11-16T22:51:10Z"}, "committer": {"name": "GitHub", "email": "noreply@github.com", "date": "2024-11-16T22:51:10Z"}, "message": "Bump platformdirs from 4.2.2 to 4.3.6 (#2403)\n\n* Bump platformdirs from 4.2.2 to 4.3.6\r\n\r\nBumps [platformdirs](https://github.com/tox-dev/platformdirs) from 4.2.2 to 4.3.6.\r\n- [Release notes](https://github.com/tox-dev/platformdirs/releases)\r\n- [Changelog](https://github.com/tox-dev/platformdirs/blob/main/CHANGES.rst)\r\n- [Commits](https://github.com/tox-dev/platformdirs/compare/4.2.2...4.3.6)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: platformdirs\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update platformdirs==4.3.6\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "tree": {"sha": "c01ed8faeef6491482ee694ac18594aa24277edc", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/c01ed8faeef6491482ee694ac18594aa24277edc"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/025e8bcf58b320805baca8d74e2ab9afd4c04281", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJnOSHeCRC1aQ7uu5UhlAAAZ48QAEIf2W4MtAgV5cUzF3D2bcct\nobp/vmADShZC6cyy9Zyp45ZAxTyraPq5l99/edfi2fqx6QNBOFqUuAy8rksaG+1x\nS/lu5khgGqKAmbJijUxJYvIs3TR8xmBUQbFOTBoqFUW6nCA/37XORY0r+VWvgMlX\ng19yQBajXDX3FXLryfHpSHpB2SiwFm7zHpHVJ5yb4yZIWXSDKJV4AlFlPLZ7OBpO\nbNx/pz5Xjyi60C/Mbr9oFDbJ0S0HwAEAgCPdMGn5lQgGG3sITtq6e+IltrIudEoq\noaWpggmz7m/n9/yqC2gbQfCs3gkOy41W86EXtkDl3w48Ol9ODm9/xXNhrqwPrS8n\nYILi+MzWsok9PV2Rx7OvD+p5OF4xR/DSTvpSRpzq8VarWv6ftAPuXO7Hr9rrcZW2\nBsW9UG8qkGg3UDytfX4KAA+gbf9tHA/Cd+51i0kVLgj+q/5GU87ApxPFyY8+meKj\nIarnneawUMx2+vVWzu73865nRdHGIKuC7oOBBFZcshvwTTfFLFgRkzukAqPHmyyz\nNOzIRiCJR5VQcLIW/plteliETt8eIzi6p1ogllw4oP8/vQWnp2SOrbc1p9HnJMQe\n9I5LN30EnYXoSrjJJEnVvl9rfLkUqfq8suJMCXjHVjleLcOz8HyMBif8HFZFl35x\nm2r2JAEtY+B2qXSX/Wge\n=Pb3a\n-----END PGP SIGNATURE-----\n", "payload": "tree c01ed8faeef6491482ee694ac18594aa24277edc\nparent bf07912711bef3274a3393e89855238f77c6565d\nauthor dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1731797470 -0800\ncommitter GitHub <noreply@github.com> 1731797470 -0800\n\nBump platformdirs from 4.2.2 to 4.3.6 (#2403)\n\n* Bump platformdirs from 4.2.2 to 4.3.6\r\n\r\nBumps [platformdirs](https://github.com/tox-dev/platformdirs) from 4.2.2 to 4.3.6.\r\n- [Release notes](https://github.com/tox-dev/platformdirs/releases)\r\n- [Changelog](https://github.com/tox-dev/platformdirs/blob/main/CHANGES.rst)\r\n- [Commits](https://github.com/tox-dev/platformdirs/compare/4.2.2...4.3.6)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: platformdirs\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update platformdirs==4.3.6\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "verified_at": "2024-11-16T22:51:27Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/025e8bcf58b320805baca8d74e2ab9afd4c04281", "html_url": "https://github.com/Tautulli/Tautulli/commit/025e8bcf58b320805baca8d74e2ab9afd4c04281", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/025e8bcf58b320805baca8d74e2ab9afd4c04281/comments", "author": {"login": "dependabot[bot]", "id": 49699333, "node_id": "MDM6Qm90NDk2OTkzMzM=", "avatar_url": "https://avatars.githubusercontent.com/in/29110?v=4", "gravatar_id": "", "url": "https://api.github.com/users/dependabot%5Bbot%5D", "html_url": "https://github.com/apps/dependabot", "followers_url": "https://api.github.com/users/dependabot%5Bbot%5D/followers", "following_url": "https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}", "gists_url": "https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}", "starred_url": "https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/dependabot%5Bbot%5D/subscriptions", "organizations_url": "https://api.github.com/users/dependabot%5Bbot%5D/orgs", "repos_url": "https://api.github.com/users/dependabot%5Bbot%5D/repos", "events_url": "https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}", "received_events_url": "https://api.github.com/users/dependabot%5Bbot%5D/received_events", "type": "Bot", "user_view_type": "public", "site_admin": false}, "committer": {"login": "web-flow", "id": 19864447, "node_id": "MDQ6VXNlcjE5ODY0NDQ3", "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", "gravatar_id": "", "url": "https://api.github.com/users/web-flow", "html_url": "https://github.com/web-flow", "followers_url": "https://api.github.com/users/web-flow/followers", "following_url": "https://api.github.com/users/web-flow/following{/other_user}", "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", "organizations_url": "https://api.github.com/users/web-flow/orgs", "repos_url": "https://api.github.com/users/web-flow/repos", "events_url": "https://api.github.com/users/web-flow/events{/privacy}", "received_events_url": "https://api.github.com/users/web-flow/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "bf07912711bef3274a3393e89855238f77c6565d", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/bf07912711bef3274a3393e89855238f77c6565d", "html_url": "https://github.com/Tautulli/Tautulli/commit/bf07912711bef3274a3393e89855238f77c6565d"}]}, {"sha": "fc2c7cc871bbd169e1fa73736136e76ec7c11844", "node_id": "C_kwDOAducuNoAKGZjMmM3Y2M4NzFiYmQxNjllMWZhNzM3MzYxMzZlNzZlYzdjMTE4NDQ", "commit": {"author": {"name": "dependabot[bot]", "email": "49699333+dependabot[bot]@users.noreply.github.com", "date": "2024-11-16T22:51:22Z"}, "committer": {"name": "GitHub", "email": "noreply@github.com", "date": "2024-11-16T22:51:22Z"}, "message": "Bump tzdata from 2024.1 to 2024.2 (#2409)\n\n* Bump tzdata from 2024.1 to 2024.2\r\n\r\nBumps [tzdata](https://github.com/python/tzdata) from 2024.1 to 2024.2.\r\n- [Release notes](https://github.com/python/tzdata/releases)\r\n- [Changelog](https://github.com/python/tzdata/blob/master/NEWS.md)\r\n- [Commits](https://github.com/python/tzdata/compare/2024.1...2024.2)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: tzdata\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update tzdata==2024.2\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "tree": {"sha": "658b0182525418b4ff75789a3e83d9cfeb5d8e5a", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/658b0182525418b4ff75789a3e83d9cfeb5d8e5a"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/fc2c7cc871bbd169e1fa73736136e76ec7c11844", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJnOSHqCRC1aQ7uu5UhlAAAGNMQAJXQyjHxF6/pv03yM8ROSuSK\nOOFeFr5RlhhXwoUR2lbbu9hiZTFqbsMA5/sZIJLKLHVrHnkUuQzp6jfMal4eqT6K\njHOPfp4F3D1fvZKwsA00h3EDZiQCLjMJjOZ4V29rkZa5qDKuuRMRIGSsSfe+rXFq\n6c4HsDgG2fxaIhS/ndJ4N0K9wtZ9TXdiCtlo6p2ZlFGs82B0e0pgL/KqezHOH+RE\nm4/iaeuRQS1cr8jgGd6QwbmYfp1tujaRQG1ao490DKl9Fj5CV7atTOxG9J6zu8Ms\nzzOpR8X7b59bwnOczmeX4ipuBhpHJoUZEypS1jx1c1mZDLuidtybynei6Rawdrie\nvU3l47cHUWNnK8s5OGDk/X8928ritR0OcJAjRvEPoDFOrpkwWxb5I4H84ulUTb/m\n9nUNU6BGT6Y93kkh8BIuGwiP/pFA/otcraxdQneMbqItI9PPbiZE1rwcaZ9pXqRM\nc4SZ4yZvDyRa5HAsXQwIZu6de8xz0wxp9CxXZKkkb6yiq7DtjrDlGTGtdrXzGXaZ\nn0Klzdtj7TuGSYDxLnIXsj2i6xxG+w6P+KUdz+Sq4VmsMBGYQsOAKDgv9mCMTTnF\nLvxmT6IVNvzIQh7VLdwIQk0Kt/4/KezTnQgY7AfSx89irEtntR2FIGr/k14RrJf4\nG+w6bKvVboxh9eLR12JY\n=Gp54\n-----END PGP SIGNATURE-----\n", "payload": "tree 658b0182525418b4ff75789a3e83d9cfeb5d8e5a\nparent 025e8bcf58b320805baca8d74e2ab9afd4c04281\nauthor dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1731797482 -0800\ncommitter GitHub <noreply@github.com> 1731797482 -0800\n\nBump tzdata from 2024.1 to 2024.2 (#2409)\n\n* Bump tzdata from 2024.1 to 2024.2\r\n\r\nBumps [tzdata](https://github.com/python/tzdata) from 2024.1 to 2024.2.\r\n- [Release notes](https://github.com/python/tzdata/releases)\r\n- [Changelog](https://github.com/python/tzdata/blob/master/NEWS.md)\r\n- [Commits](https://github.com/python/tzdata/compare/2024.1...2024.2)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: tzdata\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update tzdata==2024.2\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "verified_at": "2024-11-16T22:51:27Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/fc2c7cc871bbd169e1fa73736136e76ec7c11844", "html_url": "https://github.com/Tautulli/Tautulli/commit/fc2c7cc871bbd169e1fa73736136e76ec7c11844", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/fc2c7cc871bbd169e1fa73736136e76ec7c11844/comments", "author": {"login": "dependabot[bot]", "id": 49699333, "node_id": "MDM6Qm90NDk2OTkzMzM=", "avatar_url": "https://avatars.githubusercontent.com/in/29110?v=4", "gravatar_id": "", "url": "https://api.github.com/users/dependabot%5Bbot%5D", "html_url": "https://github.com/apps/dependabot", "followers_url": "https://api.github.com/users/dependabot%5Bbot%5D/followers", "following_url": "https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}", "gists_url": "https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}", "starred_url": "https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/dependabot%5Bbot%5D/subscriptions", "organizations_url": "https://api.github.com/users/dependabot%5Bbot%5D/orgs", "repos_url": "https://api.github.com/users/dependabot%5Bbot%5D/repos", "events_url": "https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}", "received_events_url": "https://api.github.com/users/dependabot%5Bbot%5D/received_events", "type": "Bot", "user_view_type": "public", "site_admin": false}, "committer": {"login": "web-flow", "id": 19864447, "node_id": "MDQ6VXNlcjE5ODY0NDQ3", "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", "gravatar_id": "", "url": "https://api.github.com/users/web-flow", "html_url": "https://github.com/web-flow", "followers_url": "https://api.github.com/users/web-flow/followers", "following_url": "https://api.github.com/users/web-flow/following{/other_user}", "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", "organizations_url": "https://api.github.com/users/web-flow/orgs", "repos_url": "https://api.github.com/users/web-flow/repos", "events_url": "https://api.github.com/users/web-flow/events{/privacy}", "received_events_url": "https://api.github.com/users/web-flow/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "025e8bcf58b320805baca8d74e2ab9afd4c04281", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/025e8bcf58b320805baca8d74e2ab9afd4c04281", "html_url": "https://github.com/Tautulli/Tautulli/commit/025e8bcf58b320805baca8d74e2ab9afd4c04281"}]}, {"sha": "86abd130b00669cdfaf19e58e252461d799fde8a", "node_id": "C_kwDOAducuNoAKDg2YWJkMTMwYjAwNjY5Y2RmYWYxOWU1OGUyNTI0NjFkNzk5ZmRlOGE", "commit": {"author": {"name": "dependabot[bot]", "email": "49699333+dependabot[bot]@users.noreply.github.com", "date": "2024-11-16T22:51:34Z"}, "committer": {"name": "GitHub", "email": "noreply@github.com", "date": "2024-11-16T22:51:34Z"}, "message": "Bump profilehooks from 1.12.0 to 1.13.0 (#2414)\n\n* Bump profilehooks from 1.12.0 to 1.13.0\r\n\r\nBumps [profilehooks](https://github.com/mgedmin/profilehooks) from 1.12.0 to 1.13.0.\r\n- [Changelog](https://github.com/mgedmin/profilehooks/blob/master/CHANGES.rst)\r\n- [Commits](https://github.com/mgedmin/profilehooks/compare/1.12.0...1.13.0)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: profilehooks\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update profilehooks==1.13.0\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "tree": {"sha": "f0694c106464a783be22ea9baa6c99369323c41b", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/f0694c106464a783be22ea9baa6c99369323c41b"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/86abd130b00669cdfaf19e58e252461d799fde8a", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJnOSH2CRC1aQ7uu5UhlAAA0ooQAHRhnlkZMbs6r8Vt0D3eUH6S\nf5IbqH8EBT4C3pHwB8WPdMOuBZnVHKV1H38RFa3SmtudTj27dkDJ1xKfzZE8V/Gw\nWzJ2+3s2YPIdrnddV3Pu8H+ndrFKTG9NpiPDc39vYySa0lAkbxEStMBFiv6xwxya\n3gc57TLHoQBhgfo4CwkMKBrUCBcse7P8jMXBAOTLLBlSAlPpFols/dmG9pV8Ez3Q\nlvwyK9Vh8MHOtxCzBj7p4lWkJyHZiAsZ3cYCLcyu6bgS9eWNXakYUyMJkq4mGn/q\nhrKpNO4HtOgD9Og2GZ4NyqFfgHtVQHk+9jYkEIAs54TiOSU32zME2YmDUnxRAjbO\noR35Bvtf/DA/XZYmsyiH2lReRoeQRdRVlPRRACozZ0FZoIVBbrlwMG0eDR2ifVht\nI3xuuNNnFafXV3RZbMD4QPeLAHn2QvLajiYwJbyeDDfiJLE1dyumiOV1in2jlWP2\nt8IRpVZKqSEKByeWYaLYgcfauYEfc6fmb/gju23rlElBpytyQaFIXjRWdINxI2L0\nPbptMbvN9126OkdGjv/c37VSWJt1KMb9Nr0CSjxzhefFgYOPEUdrsi94nqURChp6\nOIYWaW4B7ful2fFmvsWo+k7t/o234+AHAv7qLBsP7j8f9EievHFbSn4kv73I+Xmu\nsFP5QVeH7fabAgU018/V\n=mahs\n-----END PGP SIGNATURE-----\n", "payload": "tree f0694c106464a783be22ea9baa6c99369323c41b\nparent fc2c7cc871bbd169e1fa73736136e76ec7c11844\nauthor dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1731797494 -0800\ncommitter GitHub <noreply@github.com> 1731797494 -0800\n\nBump profilehooks from 1.12.0 to 1.13.0 (#2414)\n\n* Bump profilehooks from 1.12.0 to 1.13.0\r\n\r\nBumps [profilehooks](https://github.com/mgedmin/profilehooks) from 1.12.0 to 1.13.0.\r\n- [Changelog](https://github.com/mgedmin/profilehooks/blob/master/CHANGES.rst)\r\n- [Commits](https://github.com/mgedmin/profilehooks/compare/1.12.0...1.13.0)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: profilehooks\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update profilehooks==1.13.0\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "verified_at": "2024-11-16T22:51:35Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/86abd130b00669cdfaf19e58e252461d799fde8a", "html_url": "https://github.com/Tautulli/Tautulli/commit/86abd130b00669cdfaf19e58e252461d799fde8a", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/86abd130b00669cdfaf19e58e252461d799fde8a/comments", "author": {"login": "dependabot[bot]", "id": 49699333, "node_id": "MDM6Qm90NDk2OTkzMzM=", "avatar_url": "https://avatars.githubusercontent.com/in/29110?v=4", "gravatar_id": "", "url": "https://api.github.com/users/dependabot%5Bbot%5D", "html_url": "https://github.com/apps/dependabot", "followers_url": "https://api.github.com/users/dependabot%5Bbot%5D/followers", "following_url": "https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}", "gists_url": "https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}", "starred_url": "https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/dependabot%5Bbot%5D/subscriptions", "organizations_url": "https://api.github.com/users/dependabot%5Bbot%5D/orgs", "repos_url": "https://api.github.com/users/dependabot%5Bbot%5D/repos", "events_url": "https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}", "received_events_url": "https://api.github.com/users/dependabot%5Bbot%5D/received_events", "type": "Bot", "user_view_type": "public", "site_admin": false}, "committer": {"login": "web-flow", "id": 19864447, "node_id": "MDQ6VXNlcjE5ODY0NDQ3", "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", "gravatar_id": "", "url": "https://api.github.com/users/web-flow", "html_url": "https://github.com/web-flow", "followers_url": "https://api.github.com/users/web-flow/followers", "following_url": "https://api.github.com/users/web-flow/following{/other_user}", "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", "organizations_url": "https://api.github.com/users/web-flow/orgs", "repos_url": "https://api.github.com/users/web-flow/repos", "events_url": "https://api.github.com/users/web-flow/events{/privacy}", "received_events_url": "https://api.github.com/users/web-flow/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "fc2c7cc871bbd169e1fa73736136e76ec7c11844", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/fc2c7cc871bbd169e1fa73736136e76ec7c11844", "html_url": "https://github.com/Tautulli/Tautulli/commit/fc2c7cc871bbd169e1fa73736136e76ec7c11844"}]}, {"sha": "af752e0accd02b162cd628420b52acb97c680774", "node_id": "C_kwDOAducuNoAKGFmNzUyZTBhY2NkMDJiMTYyY2Q2Mjg0MjBiNTJhY2I5N2M2ODA3NzQ", "commit": {"author": {"name": "dependabot[bot]", "email": "49699333+dependabot[bot]@users.noreply.github.com", "date": "2024-11-16T22:51:45Z"}, "committer": {"name": "GitHub", "email": "noreply@github.com", "date": "2024-11-16T22:51:45Z"}, "message": "Bump xmltodict from 0.13.0 to 0.14.2 (#2418)\n\n* Bump xmltodict from 0.13.0 to 0.14.2\r\n\r\nBumps [xmltodict](https://github.com/martinblech/xmltodict) from 0.13.0 to 0.14.2.\r\n- [Changelog](https://github.com/martinblech/xmltodict/blob/master/CHANGELOG.md)\r\n- [Commits](https://github.com/martinblech/xmltodict/compare/v0.13.0...v0.14.2)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: xmltodict\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update xmltodict==0.14.2\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "tree": {"sha": "2c9f41ff4e1294384a1168098c7eb44fda0b3670", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/2c9f41ff4e1294384a1168098c7eb44fda0b3670"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/af752e0accd02b162cd628420b52acb97c680774", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJnOSIBCRC1aQ7uu5UhlAAAaB8QAGs2hpN26DfOn+E96kxT0unA\naO+BjSL0JnnCDqr0yEJCOXD1WjhhR7tK1/TS85R5+4nTTir2Geyh5pUlF+bcB3G5\nf1vUNzwy1q4erm7dKfYG5WO4x4X/Gjz7HBF4dpMku4uqjaBBUBr4WlPMV77oJjXj\nWViQ859GEuXQuYXh7WcE9IcCppcO9NYNobyOc9Sa07PZ4tkWUuBn1g5CrgBw75jY\n3Gt0oxnoSkD9waRVj10IzHk84MYRBE6Gq7SYhiZkQ32tCDAXuj1+5lAqq97B/rks\nl7TtcTkqF7sRb9MQ5I3jxxuJtEOaaUmeDFOX+QQSd6BO3gOD0O7QUxcilaOT+KjN\nwO6IzxUZLwlt8ktEJH48ymC4D9hQzMHxV6b8ef3NhFH7qdAZo/isXv/YTWnI1K2T\nsxKqMliP/bn0wQ55te6OomxALsuYoOZDxsjdDSe/2SNqORxYShzB7vmOofvibHl8\nA0OTMGAPaXO+6sZeSoNCDSGs5hZOdRtPDJvzNFjhBYb1b60PC9P1cojLdaYRx/X7\nCnLri2x7YXAP7bDjKzURwnEnJjiTWB8PtwY47mFoWiyEYQzHnMtAa3gyoU7Cvh5l\nylydaxjHN1y7EkcsmryJFxJ/3Hig+ohxVE+00VFn0ZT61fERck/BiOKsYJ2Tgk9s\neQHyRF9kGXR070FxBDlR\n=f4SY\n-----END PGP SIGNATURE-----\n", "payload": "tree 2c9f41ff4e1294384a1168098c7eb44fda0b3670\nparent 86abd130b00669cdfaf19e58e252461d799fde8a\nauthor dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1731797505 -0800\ncommitter GitHub <noreply@github.com> 1731797505 -0800\n\nBump xmltodict from 0.13.0 to 0.14.2 (#2418)\n\n* Bump xmltodict from 0.13.0 to 0.14.2\r\n\r\nBumps [xmltodict](https://github.com/martinblech/xmltodict) from 0.13.0 to 0.14.2.\r\n- [Changelog](https://github.com/martinblech/xmltodict/blob/master/CHANGELOG.md)\r\n- [Commits](https://github.com/martinblech/xmltodict/compare/v0.13.0...v0.14.2)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: xmltodict\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update xmltodict==0.14.2\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "verified_at": "2024-11-16T22:51:57Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/af752e0accd02b162cd628420b52acb97c680774", "html_url": "https://github.com/Tautulli/Tautulli/commit/af752e0accd02b162cd628420b52acb97c680774", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/af752e0accd02b162cd628420b52acb97c680774/comments", "author": {"login": "dependabot[bot]", "id": 49699333, "node_id": "MDM6Qm90NDk2OTkzMzM=", "avatar_url": "https://avatars.githubusercontent.com/in/29110?v=4", "gravatar_id": "", "url": "https://api.github.com/users/dependabot%5Bbot%5D", "html_url": "https://github.com/apps/dependabot", "followers_url": "https://api.github.com/users/dependabot%5Bbot%5D/followers", "following_url": "https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}", "gists_url": "https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}", "starred_url": "https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/dependabot%5Bbot%5D/subscriptions", "organizations_url": "https://api.github.com/users/dependabot%5Bbot%5D/orgs", "repos_url": "https://api.github.com/users/dependabot%5Bbot%5D/repos", "events_url": "https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}", "received_events_url": "https://api.github.com/users/dependabot%5Bbot%5D/received_events", "type": "Bot", "user_view_type": "public", "site_admin": false}, "committer": {"login": "web-flow", "id": 19864447, "node_id": "MDQ6VXNlcjE5ODY0NDQ3", "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", "gravatar_id": "", "url": "https://api.github.com/users/web-flow", "html_url": "https://github.com/web-flow", "followers_url": "https://api.github.com/users/web-flow/followers", "following_url": "https://api.github.com/users/web-flow/following{/other_user}", "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", "organizations_url": "https://api.github.com/users/web-flow/orgs", "repos_url": "https://api.github.com/users/web-flow/repos", "events_url": "https://api.github.com/users/web-flow/events{/privacy}", "received_events_url": "https://api.github.com/users/web-flow/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "86abd130b00669cdfaf19e58e252461d799fde8a", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/86abd130b00669cdfaf19e58e252461d799fde8a", "html_url": "https://github.com/Tautulli/Tautulli/commit/86abd130b00669cdfaf19e58e252461d799fde8a"}]}, {"sha": "9289ead9967b84f5cee1ee5d22554490f39e23fc", "node_id": "C_kwDOAducuNoAKDkyODllYWQ5OTY3Yjg0ZjVjZWUxZWU1ZDIyNTU0NDkwZjM5ZTIzZmM", "commit": {"author": {"name": "dependabot[bot]", "email": "49699333+dependabot[bot]@users.noreply.github.com", "date": "2024-11-16T22:51:58Z"}, "committer": {"name": "GitHub", "email": "noreply@github.com", "date": "2024-11-16T22:51:58Z"}, "message": "Bump mako from 1.3.5 to 1.3.6 (#2423)\n\n* Bump mako from 1.3.5 to 1.3.6\r\n\r\nBumps [mako](https://github.com/sqlalchemy/mako) from 1.3.5 to 1.3.6.\r\n- [Release notes](https://github.com/sqlalchemy/mako/releases)\r\n- [Changelog](https://github.com/sqlalchemy/mako/blob/main/CHANGES)\r\n- [Commits](https://github.com/sqlalchemy/mako/commits)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: mako\r\n dependency-type: direct:production\r\n update-type: version-update:semver-patch\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update mako==1.3.6\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "tree": {"sha": "3e3994ebde0366502d5e4582e1dc2b5c3a2eb25f", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/3e3994ebde0366502d5e4582e1dc2b5c3a2eb25f"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/9289ead9967b84f5cee1ee5d22554490f39e23fc", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJnOSIOCRC1aQ7uu5UhlAAAeMkQAH5cU4YzwdoUVuuixDxHceNN\n7nROIRtw3epP0z0KxvRLwMa6AXF6RM3z+TmRe7xfUNTpicnPDv7tpHvOdhLAn5Vv\nFOVMSvC3jadAIdFWt4ZUsZJl8ukMxVVZi5vN1at+TUGv4dik2LoyIJXLJkX++YIM\noERRB5jd6BcSt2OH0odHbiCveT6neW+yawlsAMRLQkHOf4Wr4thwBft+DizIWGti\nFyDgUOOzK002VgxTA6EZ1WDfvLHc6/tfGXCEByo8Zlm2x0cjPYL5coCFLXtiIDko\nb/tDxA+gH7mFHd51XTM2O0j1esZiCkWjtYYGIK1ytLNOvZg8dh3DlsV+pJpb3Owt\nzMGzsTcG2WRDhM9440UYYD5ccdfsxVlMD3UPOQmCApjzOKyj48etASlYv5PupLgf\nqcRtN4LuelLrSnBkL7oFFxOsDeENbyB6d5JAMnVWdQFNWxDi6CDFrpQM+Hpz6ue8\nObHAbDPrl6g7BsVUDPjH/twENn8YBRqW79crlTHRbuczlSVxV09SsJLixZxVk5/c\n87PSbUc4m7X2UaSwoYxJV+oo4EUy4Uuy6Z24NFNefoAVu31M9/BY9zwMcM1Q4EWZ\nMT6wDokgVjF04o45IpE5gxBk9s3EANV7WCJJrj8QnRJlp+O/4DIrHrhgtEa+HKbc\nCyaQDhphx98TMjyYGznU\n=dqlT\n-----END PGP SIGNATURE-----\n", "payload": "tree 3e3994ebde0366502d5e4582e1dc2b5c3a2eb25f\nparent af752e0accd02b162cd628420b52acb97c680774\nauthor dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1731797518 -0800\ncommitter GitHub <noreply@github.com> 1731797518 -0800\n\nBump mako from 1.3.5 to 1.3.6 (#2423)\n\n* Bump mako from 1.3.5 to 1.3.6\r\n\r\nBumps [mako](https://github.com/sqlalchemy/mako) from 1.3.5 to 1.3.6.\r\n- [Release notes](https://github.com/sqlalchemy/mako/releases)\r\n- [Changelog](https://github.com/sqlalchemy/mako/blob/main/CHANGES)\r\n- [Commits](https://github.com/sqlalchemy/mako/commits)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: mako\r\n dependency-type: direct:production\r\n update-type: version-update:semver-patch\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update mako==1.3.6\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "verified_at": "2024-11-16T22:51:58Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/9289ead9967b84f5cee1ee5d22554490f39e23fc", "html_url": "https://github.com/Tautulli/Tautulli/commit/9289ead9967b84f5cee1ee5d22554490f39e23fc", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/9289ead9967b84f5cee1ee5d22554490f39e23fc/comments", "author": {"login": "dependabot[bot]", "id": 49699333, "node_id": "MDM6Qm90NDk2OTkzMzM=", "avatar_url": "https://avatars.githubusercontent.com/in/29110?v=4", "gravatar_id": "", "url": "https://api.github.com/users/dependabot%5Bbot%5D", "html_url": "https://github.com/apps/dependabot", "followers_url": "https://api.github.com/users/dependabot%5Bbot%5D/followers", "following_url": "https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}", "gists_url": "https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}", "starred_url": "https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/dependabot%5Bbot%5D/subscriptions", "organizations_url": "https://api.github.com/users/dependabot%5Bbot%5D/orgs", "repos_url": "https://api.github.com/users/dependabot%5Bbot%5D/repos", "events_url": "https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}", "received_events_url": "https://api.github.com/users/dependabot%5Bbot%5D/received_events", "type": "Bot", "user_view_type": "public", "site_admin": false}, "committer": {"login": "web-flow", "id": 19864447, "node_id": "MDQ6VXNlcjE5ODY0NDQ3", "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", "gravatar_id": "", "url": "https://api.github.com/users/web-flow", "html_url": "https://github.com/web-flow", "followers_url": "https://api.github.com/users/web-flow/followers", "following_url": "https://api.github.com/users/web-flow/following{/other_user}", "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", "organizations_url": "https://api.github.com/users/web-flow/orgs", "repos_url": "https://api.github.com/users/web-flow/repos", "events_url": "https://api.github.com/users/web-flow/events{/privacy}", "received_events_url": "https://api.github.com/users/web-flow/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "af752e0accd02b162cd628420b52acb97c680774", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/af752e0accd02b162cd628420b52acb97c680774", "html_url": "https://github.com/Tautulli/Tautulli/commit/af752e0accd02b162cd628420b52acb97c680774"}]}, {"sha": "d9a87f9726f8d717e174343915610a56291e399d", "node_id": "C_kwDOAducuNoAKGQ5YTg3Zjk3MjZmOGQ3MTdlMTc0MzQzOTE1NjEwYTU2MjkxZTM5OWQ", "commit": {"author": {"name": "dependabot[bot]", "email": "49699333+dependabot[bot]@users.noreply.github.com", "date": "2024-11-16T22:52:11Z"}, "committer": {"name": "GitHub", "email": "noreply@github.com", "date": "2024-11-16T22:52:11Z"}, "message": "Bump packaging from 24.1 to 24.2 (#2428)\n\n* Bump packaging from 24.1 to 24.2\r\n\r\nBumps [packaging](https://github.com/pypa/packaging) from 24.1 to 24.2.\r\n- [Release notes](https://github.com/pypa/packaging/releases)\r\n- [Changelog](https://github.com/pypa/packaging/blob/main/CHANGELOG.rst)\r\n- [Commits](https://github.com/pypa/packaging/compare/24.1...24.2)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: packaging\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update packaging==24.2\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "tree": {"sha": "bdb60c579ed25b2de1f31a0819254bfa9d54f66d", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/bdb60c579ed25b2de1f31a0819254bfa9d54f66d"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/d9a87f9726f8d717e174343915610a56291e399d", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJnOSIbCRC1aQ7uu5UhlAAAueUQAFR0zXmZXeoCM814A4//YLTi\njLvJSfb2OTOxUKL4iRbeF/Lq7q9fLc2dEmcD96f4O3895grIrlgBmWVSq8qIgMvE\n1w2MO9Mmzix4ayzIYN2uL/0QXtA3nJjHoJDv8yDw7ktJeNf82oWxkLLWj0mq0NmU\nKh1SljflGchjhDWgqxx8zR7aEDuCYH2gezgWksZcztOLDacR6/7xmgo3OKTeWKuT\nzjXKIAnWN3JnLr92vdddot1B88TtkUC8hHj8e2jm2gPh8hfqqv44pgsSpWKw97MC\nQjnqtEoqRHrAVnY6iRZB3YXropTbhQFoOKLgoQ5J1Fnb4Q4VtWdVh4hx2FIV1oKu\ns3LTXT+Y4IP8xCuXveyKliAMcZ7ilu+H5dW7iihuR4eodXwdxuA799sjrrmzR6gi\n4h6x2+8L636n9ecb2n1VrBeVnt0mK1t5D+MC/dmv4wX7U6FX7TayYd+ep80r3Gon\nzJGw2g8oH+hm/v2puoFOkMDvE56CoGuYYJfMKPZXqXXUH5XBXKl8PvYpew+zT9H4\n+R7D1RBpXTCB8oeYCzpp3lI/l7dH6yIU5UnE4Veyx01BJ6kbFnwVlfUxNc0QlG+n\nldXAVGXeL7honJykJvri6C3Z/0tpW7k9KiGZE0fuG5lMs0iaqN8YuLhsEfG9PJnq\nkeSzWsZ2Pp7XiCq88lZh\n=7XMS\n-----END PGP SIGNATURE-----\n", "payload": "tree bdb60c579ed25b2de1f31a0819254bfa9d54f66d\nparent 9289ead9967b84f5cee1ee5d22554490f39e23fc\nauthor dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1731797531 -0800\ncommitter GitHub <noreply@github.com> 1731797531 -0800\n\nBump packaging from 24.1 to 24.2 (#2428)\n\n* Bump packaging from 24.1 to 24.2\r\n\r\nBumps [packaging](https://github.com/pypa/packaging) from 24.1 to 24.2.\r\n- [Release notes](https://github.com/pypa/packaging/releases)\r\n- [Changelog](https://github.com/pypa/packaging/blob/main/CHANGELOG.rst)\r\n- [Commits](https://github.com/pypa/packaging/compare/24.1...24.2)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: packaging\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update packaging==24.2\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "verified_at": "2024-11-16T22:52:19Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/d9a87f9726f8d717e174343915610a56291e399d", "html_url": "https://github.com/Tautulli/Tautulli/commit/d9a87f9726f8d717e174343915610a56291e399d", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/d9a87f9726f8d717e174343915610a56291e399d/comments", "author": {"login": "dependabot[bot]", "id": 49699333, "node_id": "MDM6Qm90NDk2OTkzMzM=", "avatar_url": "https://avatars.githubusercontent.com/in/29110?v=4", "gravatar_id": "", "url": "https://api.github.com/users/dependabot%5Bbot%5D", "html_url": "https://github.com/apps/dependabot", "followers_url": "https://api.github.com/users/dependabot%5Bbot%5D/followers", "following_url": "https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}", "gists_url": "https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}", "starred_url": "https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/dependabot%5Bbot%5D/subscriptions", "organizations_url": "https://api.github.com/users/dependabot%5Bbot%5D/orgs", "repos_url": "https://api.github.com/users/dependabot%5Bbot%5D/repos", "events_url": "https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}", "received_events_url": "https://api.github.com/users/dependabot%5Bbot%5D/received_events", "type": "Bot", "user_view_type": "public", "site_admin": false}, "committer": {"login": "web-flow", "id": 19864447, "node_id": "MDQ6VXNlcjE5ODY0NDQ3", "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", "gravatar_id": "", "url": "https://api.github.com/users/web-flow", "html_url": "https://github.com/web-flow", "followers_url": "https://api.github.com/users/web-flow/followers", "following_url": "https://api.github.com/users/web-flow/following{/other_user}", "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", "organizations_url": "https://api.github.com/users/web-flow/orgs", "repos_url": "https://api.github.com/users/web-flow/repos", "events_url": "https://api.github.com/users/web-flow/events{/privacy}", "received_events_url": "https://api.github.com/users/web-flow/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "9289ead9967b84f5cee1ee5d22554490f39e23fc", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/9289ead9967b84f5cee1ee5d22554490f39e23fc", "html_url": "https://github.com/Tautulli/Tautulli/commit/9289ead9967b84f5cee1ee5d22554490f39e23fc"}]}, {"sha": "84be60cb3608009c56f54f85b777a6c26a0d3856", "node_id": "C_kwDOAducuNoAKDg0YmU2MGNiMzYwODAwOWM1NmY1NGY4NWI3NzdhNmMyNmEwZDM4NTY", "commit": {"author": {"name": "dependabot[bot]", "email": "49699333+dependabot[bot]@users.noreply.github.com", "date": "2024-11-16T22:53:33Z"}, "committer": {"name": "GitHub", "email": "noreply@github.com", "date": "2024-11-16T22:53:33Z"}, "message": "Bump pywin32 from 306 to 308 (#2417)\n\nBumps [pywin32](https://github.com/mhammond/pywin32) from 306 to 308.\r\n- [Release notes](https://github.com/mhammond/pywin32/releases)\r\n- [Changelog](https://github.com/mhammond/pywin32/blob/main/CHANGES.txt)\r\n- [Commits](https://github.com/mhammond/pywin32/commits)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: pywin32\r\n dependency-type: direct:production\r\n update-type: version-update:semver-major\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\n\r\n[skip ci]", "tree": {"sha": "776840c0bf39cad6aa36268d362fe69b6be5c85d", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/776840c0bf39cad6aa36268d362fe69b6be5c85d"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/84be60cb3608009c56f54f85b777a6c26a0d3856", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJnOSJtCRC1aQ7uu5UhlAAAXp8QAD0SfAYNkTs0PRqHGSsbDkjH\nVOH6RFX0FftFyXXSTIKXrYbNp+L4249LqPwjvTe6Mb6v3YDpCQl68+M0uMMdyh9A\nJdsiYrpWTHqUK9kw4HSWfeUUXxbcbygK10pUhsa69nGwozwKouUGAPILKPSGK+w5\ngzxK3ViGRUlFdlNTxosqwDw+f5053QueXZIimO4QyObgKAwK6GkZ1WdYlCfKbvsW\n/8e7KKr69h+srnTvN2TloOTjaiianLvuwZtENjX5NZHm8VRgsLCtr1L2yhOSmEoz\niw6rjJ6Y7CrLaMzzw1qYwM2Z4p4iXMjqGtydaVQ+83AIkArvUm98wTtE4Hjb+UpO\nE3TQFyWsYcaTOR0juqBJrVUlSeg1gzLmKk1SVFPT5yiu58yyNRmNRmJU98ArGfu7\nIk9rSZolScggXvOk5oCrJ39C9QDJyfsEL43l5Y+U1q1Wsj+zu1gSfoQ2VhoAED4I\nxyv/Jn+e01XAgCT1hRw/AfoyMv9Ca9oxNsXEqa0wmrxJNGiCTo9FpSVPwJNO/p5z\n52A9FCjfr5XN6ZjRdTo65vwdkTUxb1wkCnMd94RnWyixgXzHeqDFHjSasyP/o3im\nqtAOR9XBTg+Xd7jVfJ7Z9qO6i9Ulnn035sWFGIbQhGagYZpkvFDpc32s+Pe1PbzN\n/WsaFO9RvNVcdPolrHJC\n=9Aso\n-----END PGP SIGNATURE-----\n", "payload": "tree 776840c0bf39cad6aa36268d362fe69b6be5c85d\nparent d9a87f9726f8d717e174343915610a56291e399d\nauthor dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1731797613 -0800\ncommitter GitHub <noreply@github.com> 1731797613 -0800\n\nBump pywin32 from 306 to 308 (#2417)\n\nBumps [pywin32](https://github.com/mhammond/pywin32) from 306 to 308.\r\n- [Release notes](https://github.com/mhammond/pywin32/releases)\r\n- [Changelog](https://github.com/mhammond/pywin32/blob/main/CHANGES.txt)\r\n- [Commits](https://github.com/mhammond/pywin32/commits)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: pywin32\r\n dependency-type: direct:production\r\n update-type: version-update:semver-major\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\n\r\n[skip ci]", "verified_at": "2024-11-16T22:53:54Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/84be60cb3608009c56f54f85b777a6c26a0d3856", "html_url": "https://github.com/Tautulli/Tautulli/commit/84be60cb3608009c56f54f85b777a6c26a0d3856", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/84be60cb3608009c56f54f85b777a6c26a0d3856/comments", "author": {"login": "dependabot[bot]", "id": 49699333, "node_id": "MDM6Qm90NDk2OTkzMzM=", "avatar_url": "https://avatars.githubusercontent.com/in/29110?v=4", "gravatar_id": "", "url": "https://api.github.com/users/dependabot%5Bbot%5D", "html_url": "https://github.com/apps/dependabot", "followers_url": "https://api.github.com/users/dependabot%5Bbot%5D/followers", "following_url": "https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}", "gists_url": "https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}", "starred_url": "https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/dependabot%5Bbot%5D/subscriptions", "organizations_url": "https://api.github.com/users/dependabot%5Bbot%5D/orgs", "repos_url": "https://api.github.com/users/dependabot%5Bbot%5D/repos", "events_url": "https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}", "received_events_url": "https://api.github.com/users/dependabot%5Bbot%5D/received_events", "type": "Bot", "user_view_type": "public", "site_admin": false}, "committer": {"login": "web-flow", "id": 19864447, "node_id": "MDQ6VXNlcjE5ODY0NDQ3", "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", "gravatar_id": "", "url": "https://api.github.com/users/web-flow", "html_url": "https://github.com/web-flow", "followers_url": "https://api.github.com/users/web-flow/followers", "following_url": "https://api.github.com/users/web-flow/following{/other_user}", "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", "organizations_url": "https://api.github.com/users/web-flow/orgs", "repos_url": "https://api.github.com/users/web-flow/repos", "events_url": "https://api.github.com/users/web-flow/events{/privacy}", "received_events_url": "https://api.github.com/users/web-flow/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "d9a87f9726f8d717e174343915610a56291e399d", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/d9a87f9726f8d717e174343915610a56291e399d", "html_url": "https://github.com/Tautulli/Tautulli/commit/d9a87f9726f8d717e174343915610a56291e399d"}]}, {"sha": "599e52de6a037855b2534e970441c74d9793980e", "node_id": "C_kwDOAducuNoAKDU5OWU1MmRlNmEwMzc4NTViMjUzNGU5NzA0NDFjNzRkOTc5Mzk4MGU", "commit": {"author": {"name": "dependabot[bot]", "email": "49699333+dependabot[bot]@users.noreply.github.com", "date": "2024-11-16T22:53:44Z"}, "committer": {"name": "GitHub", "email": "noreply@github.com", "date": "2024-11-16T22:53:44Z"}, "message": "Bump cryptography from 43.0.0 to 43.0.3 (#2421)\n\nBumps [cryptography](https://github.com/pyca/cryptography) from 43.0.0 to 43.0.3.\r\n- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)\r\n- [Commits](https://github.com/pyca/cryptography/compare/43.0.0...43.0.3)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: cryptography\r\n dependency-type: direct:production\r\n update-type: version-update:semver-patch\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\n\r\n[skip ci]", "tree": {"sha": "6d27b07353873d66c24db210826c411a095cd276", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/6d27b07353873d66c24db210826c411a095cd276"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/599e52de6a037855b2534e970441c74d9793980e", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJnOSJ4CRC1aQ7uu5UhlAAAra8QAFvtXo2YSkcSyo1pvlAHMxgm\nNRguFAha7vIcFGGZ5/XmUOWq/E8WHh55Nzcz/hyuCtWax46X2+FkVL8aImXNoO8g\ncA4VCzoynMdFwSkXM+g5cHgNN1bQX3MjQsPg2RTln5JgEaaHTGoy9Y0g6dR1oHxl\nI22RyBgyy2p5XkZURX7kaiNpbobmKhZ29hf/mBAuvQEJ7yJyL8b0NR/+WOFU2Y9X\n09nuOzstN9VyILbBL9TqGhFlUgq67AsfVuXK2OI6TSpazx/KNDkH9q9sk8BR2/c/\npD7H27OogUbdBIuYX7gj37XCDTETH50S0JRzi84hucVKawwXlwqHYJhTlKtSsi6R\nE9qlmAGciIu6UWJOHjOzsarHgpUWuOGT9UtPC/cwHUxqddNFrSXtgBo3KJvMRk8a\nASTmfHxfIHXFj/mbA/LamwK7tdfZI2PpXUkHIfrS725h5YvarnP5rotk+Mbo2A3V\nH/Iohiciyye/OvCaW7z7sq6gX3/rKMKvXi4UaUEmHQuz8NsurCn2+pp8En9KzU5G\nJG6/tzy53Tn5ZdatpFSQCniU9T4zinksu9n7Lh+iux47pVpQtNpNiKQ/3tKK5SjO\n7X/2DECNhdUuwHuiR5BAqyei1kQuRNTy31aodYVbA3AG4zl005iPqtrgNuSz3VmM\nE8Q06q83br63rZ/oeeV2\n=2EHn\n-----END PGP SIGNATURE-----\n", "payload": "tree 6d27b07353873d66c24db210826c411a095cd276\nparent 84be60cb3608009c56f54f85b777a6c26a0d3856\nauthor dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1731797624 -0800\ncommitter GitHub <noreply@github.com> 1731797624 -0800\n\nBump cryptography from 43.0.0 to 43.0.3 (#2421)\n\nBumps [cryptography](https://github.com/pyca/cryptography) from 43.0.0 to 43.0.3.\r\n- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)\r\n- [Commits](https://github.com/pyca/cryptography/compare/43.0.0...43.0.3)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: cryptography\r\n dependency-type: direct:production\r\n update-type: version-update:semver-patch\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\n\r\n[skip ci]", "verified_at": "2024-11-16T22:53:54Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/599e52de6a037855b2534e970441c74d9793980e", "html_url": "https://github.com/Tautulli/Tautulli/commit/599e52de6a037855b2534e970441c74d9793980e", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/599e52de6a037855b2534e970441c74d9793980e/comments", "author": {"login": "dependabot[bot]", "id": 49699333, "node_id": "MDM6Qm90NDk2OTkzMzM=", "avatar_url": "https://avatars.githubusercontent.com/in/29110?v=4", "gravatar_id": "", "url": "https://api.github.com/users/dependabot%5Bbot%5D", "html_url": "https://github.com/apps/dependabot", "followers_url": "https://api.github.com/users/dependabot%5Bbot%5D/followers", "following_url": "https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}", "gists_url": "https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}", "starred_url": "https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/dependabot%5Bbot%5D/subscriptions", "organizations_url": "https://api.github.com/users/dependabot%5Bbot%5D/orgs", "repos_url": "https://api.github.com/users/dependabot%5Bbot%5D/repos", "events_url": "https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}", "received_events_url": "https://api.github.com/users/dependabot%5Bbot%5D/received_events", "type": "Bot", "user_view_type": "public", "site_admin": false}, "committer": {"login": "web-flow", "id": 19864447, "node_id": "MDQ6VXNlcjE5ODY0NDQ3", "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", "gravatar_id": "", "url": "https://api.github.com/users/web-flow", "html_url": "https://github.com/web-flow", "followers_url": "https://api.github.com/users/web-flow/followers", "following_url": "https://api.github.com/users/web-flow/following{/other_user}", "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", "organizations_url": "https://api.github.com/users/web-flow/orgs", "repos_url": "https://api.github.com/users/web-flow/repos", "events_url": "https://api.github.com/users/web-flow/events{/privacy}", "received_events_url": "https://api.github.com/users/web-flow/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "84be60cb3608009c56f54f85b777a6c26a0d3856", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/84be60cb3608009c56f54f85b777a6c26a0d3856", "html_url": "https://github.com/Tautulli/Tautulli/commit/84be60cb3608009c56f54f85b777a6c26a0d3856"}]}, {"sha": "ddb4f6131b637eb309038c5452d8b288f37b4e62", "node_id": "C_kwDOAducuNoAKGRkYjRmNjEzMWI2MzdlYjMwOTAzOGM1NDUyZDhiMjg4ZjM3YjRlNjI", "commit": {"author": {"name": "dependabot[bot]", "email": "49699333+dependabot[bot]@users.noreply.github.com", "date": "2024-11-16T23:02:52Z"}, "committer": {"name": "GitHub", "email": "noreply@github.com", "date": "2024-11-16T23:02:52Z"}, "message": "Bump pyinstaller from 6.8.0 to 6.11.1 (#2431)\n\nBumps [pyinstaller](https://github.com/pyinstaller/pyinstaller) from 6.8.0 to 6.11.1.\r\n- [Release notes](https://github.com/pyinstaller/pyinstaller/releases)\r\n- [Changelog](https://github.com/pyinstaller/pyinstaller/blob/develop/doc/CHANGES.rst)\r\n- [Commits](https://github.com/pyinstaller/pyinstaller/compare/v6.8.0...v6.11.1)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: pyinstaller\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>", "tree": {"sha": "4fc3ee37c882842028e25b46571b5822048d91c3", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/4fc3ee37c882842028e25b46571b5822048d91c3"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/ddb4f6131b637eb309038c5452d8b288f37b4e62", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJnOSScCRC1aQ7uu5UhlAAAv78QAHh6gNXFoH/Df/gVKqzql9J3\ntpfgL6+2prPLmWtmfOsTHDXQ7nS28Vw2hkMll9sHO4gq4ZIVLR2KI2QosLbwJsW3\n1gYRewEZBgjTyYK/FumtaWCawtGf5dYz/yWehOJj5KZs+cm1ifwF15eAJV4C7Bu4\nD/x7PBgNA+XEvZOjLW5E5rmhQ/HG/FaqHKzZD8IlptjHc3hdszRvUBVP2T71HSUF\nSC/ap2eWQLwGsiTGzf/a2REVsxl4kOLeqc+i/O9CO5EpK3HBXgGwE6enWuiX6DwG\nqhsH7BPx/99IFKjA6gvMngTDg6pOZK2R0PV8yiC3RY58+31h2P9tapk1+xxwAb43\nJeh1Tg180bJE6ENCfvcsyKTbTlJVszctNYKn+aft/pwqlfN365Y3ZDqEowML+nkn\nBxEFMfn3xhdN40+3S899nll0/dNOaVaZUVlPA07d4KhKfB2O6B6pJbOaJ25cvrKd\nPGpRGktjvYAF9jqu/KucV87gXtAz7sRNzLt/oyc3uvPtjALG+LYTGpWHEF6EpnIq\nk4hQR2hSuYUBOBWmQypPDeS3gNbTPaguRHIot7o88Q27Z7BVHnh7KAu5hfP3RKvt\ndUfuHeurwxk88sBUtp34BHnaMtiszBrphj20AZAPiqZNQobgP1Biwq91LAflysIN\njQJ85bdRb9Mf1hVrjWQC\n=6VCV\n-----END PGP SIGNATURE-----\n", "payload": "tree 4fc3ee37c882842028e25b46571b5822048d91c3\nparent 599e52de6a037855b2534e970441c74d9793980e\nauthor dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1731798172 -0800\ncommitter GitHub <noreply@github.com> 1731798172 -0800\n\nBump pyinstaller from 6.8.0 to 6.11.1 (#2431)\n\nBumps [pyinstaller](https://github.com/pyinstaller/pyinstaller) from 6.8.0 to 6.11.1.\r\n- [Release notes](https://github.com/pyinstaller/pyinstaller/releases)\r\n- [Changelog](https://github.com/pyinstaller/pyinstaller/blob/develop/doc/CHANGES.rst)\r\n- [Commits](https://github.com/pyinstaller/pyinstaller/compare/v6.8.0...v6.11.1)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: pyinstaller\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>", "verified_at": "2024-11-16T23:02:59Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/ddb4f6131b637eb309038c5452d8b288f37b4e62", "html_url": "https://github.com/Tautulli/Tautulli/commit/ddb4f6131b637eb309038c5452d8b288f37b4e62", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/ddb4f6131b637eb309038c5452d8b288f37b4e62/comments", "author": {"login": "dependabot[bot]", "id": 49699333, "node_id": "MDM6Qm90NDk2OTkzMzM=", "avatar_url": "https://avatars.githubusercontent.com/in/29110?v=4", "gravatar_id": "", "url": "https://api.github.com/users/dependabot%5Bbot%5D", "html_url": "https://github.com/apps/dependabot", "followers_url": "https://api.github.com/users/dependabot%5Bbot%5D/followers", "following_url": "https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}", "gists_url": "https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}", "starred_url": "https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/dependabot%5Bbot%5D/subscriptions", "organizations_url": "https://api.github.com/users/dependabot%5Bbot%5D/orgs", "repos_url": "https://api.github.com/users/dependabot%5Bbot%5D/repos", "events_url": "https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}", "received_events_url": "https://api.github.com/users/dependabot%5Bbot%5D/received_events", "type": "Bot", "user_view_type": "public", "site_admin": false}, "committer": {"login": "web-flow", "id": 19864447, "node_id": "MDQ6VXNlcjE5ODY0NDQ3", "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", "gravatar_id": "", "url": "https://api.github.com/users/web-flow", "html_url": "https://github.com/web-flow", "followers_url": "https://api.github.com/users/web-flow/followers", "following_url": "https://api.github.com/users/web-flow/following{/other_user}", "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", "organizations_url": "https://api.github.com/users/web-flow/orgs", "repos_url": "https://api.github.com/users/web-flow/repos", "events_url": "https://api.github.com/users/web-flow/events{/privacy}", "received_events_url": "https://api.github.com/users/web-flow/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "599e52de6a037855b2534e970441c74d9793980e", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/599e52de6a037855b2534e970441c74d9793980e", "html_url": "https://github.com/Tautulli/Tautulli/commit/599e52de6a037855b2534e970441c74d9793980e"}]}, {"sha": "86d775a5860d968d99ee3dfa515c1277aa44745d", "node_id": "C_kwDOAducuNoAKDg2ZDc3NWE1ODYwZDk2OGQ5OWVlM2RmYTUxNWMxMjc3YWE0NDc0NWQ", "commit": {"author": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-11-14T20:05:28Z"}, "committer": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-11-16T23:19:56Z"}, "message": "Update OneSignal API calls", "tree": {"sha": "83d6a260daad1a5532a606967e177d6f61a011a9", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/83d6a260daad1a5532a606967e177d6f61a011a9"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/86d775a5860d968d99ee3dfa515c1277aa44745d", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\niQIzBAABCAAdFiEEFPKZYCM7sDX3KZzmsfH5gHGEaXoFAmc5KJwACgkQsfH5gHGE\naXrGfBAAnAFMweke5yH68AxJmA0Q3uuwklb3AJK9jn6Ob+ceufZoenOyUVGgOxbj\n5eVo9Q4TuVLIrVTUMk3LPPD4hvV2zDPo6Y5nB6lTUgVhG5gCrmbdrNKDaLEVHlPo\nYntXQaEK2ALgDe64b6kAeRi1MUurUGeeBBOvGbHcZvRhCRMpCU2ki9xlSMs8K82p\nGrWw4k5AJKhmATrLZ+PR5Gt4sjeupcGqx9MbqMeYF+5mAw2RlEMGU68nBtiihsIW\nOj4ovj/k69YizHtoD1V2u7IWf75QCh54ikwma9Nai8z28Tkg+VA3tbOlunu0KGS6\nD6KrMs9VVVjNon57RjJoTQNhbYerRtyDdH+0/xhtqNEiNCtgCUl4l78ov7cEQ+bJ\ngAjyRtxMwGe/+h7Vq5eRDWrs4WmlpOZ5/ODe4ERKo7OAUWokHohEcLFqc96veYyW\nd/cexre5aatOuL5XGtGzWHR2cE4K8OK/ovFgHCFTkHUve4eikSYt93HYT2AguHa8\nUlh5arZ96WDhoVs6dDMWL/yKh1HfuhJV4C1FH8d2gT4uhRcVRkK+WwuYfqLzlPO3\nmw5xdDHXgi5JXq2D+2hDpbWdYWVC4jSGa/wkFJVGKMShlvFJ+YzV+A3GY0F48Ak9\nAZqWLGzlfK4tVYIDIhG4Rj6css0quYfWLhXS7nJHKOPoxKatmMg=\n=ZtYY\n-----END PGP SIGNATURE-----", "payload": "tree 83d6a260daad1a5532a606967e177d6f61a011a9\nparent ddb4f6131b637eb309038c5452d8b288f37b4e62\nauthor JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1731614728 -0800\ncommitter JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1731799196 -0800\n\nUpdate OneSignal API calls\n", "verified_at": "2024-11-16T23:38:30Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/86d775a5860d968d99ee3dfa515c1277aa44745d", "html_url": "https://github.com/Tautulli/Tautulli/commit/86d775a5860d968d99ee3dfa515c1277aa44745d", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/86d775a5860d968d99ee3dfa515c1277aa44745d/comments", "author": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "committer": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "ddb4f6131b637eb309038c5452d8b288f37b4e62", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/ddb4f6131b637eb309038c5452d8b288f37b4e62", "html_url": "https://github.com/Tautulli/Tautulli/commit/ddb4f6131b637eb309038c5452d8b288f37b4e62"}]}, {"sha": "d9f38f9390f41045a812b5fa58c95a15dff7221c", "node_id": "C_kwDOAducuNoAKGQ5ZjM4ZjkzOTBmNDEwNDVhODEyYjVmYTU4Yzk1YTE1ZGZmNzIyMWM", "commit": {"author": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-11-14T20:22:52Z"}, "committer": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-11-16T23:19:57Z"}, "message": "Fix artist title for fixing metadata match\n\nFixes #2429", "tree": {"sha": "bc5eb9975fb629227249586dea873e20b8c90c6b", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/bc5eb9975fb629227249586dea873e20b8c90c6b"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/d9f38f9390f41045a812b5fa58c95a15dff7221c", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\niQIzBAABCAAdFiEEFPKZYCM7sDX3KZzmsfH5gHGEaXoFAmc5KJ0ACgkQsfH5gHGE\naXq5eA//UrNpKj29LEkI9XXolkLDZDSNl7EiqfYmu3tzd9jWAxO2P9uUaCDNOFi3\nTq+bsXmtu1V5k0xy8htDXvLtcPf20RkpqYm6CFvyA930P8/xwtDnzLuhTorOUF4v\nDWvX0rBIOr6EKSMHcdrEGyny9vvRI4RlxidbpcqEVNhCA06/1U0TS9lZLUZpeYb8\niJtMrTYScze+aMqwzlEQP4P7m7Thwh67f0hqqqk+iJonkMQiTRXrTNZHbtbZV8ej\nthFFehMTwvRYM+mBSHNyjY8MmIElgRtSqfj/LVfCbHCJatdK0ZbvzNMSV5d4OhGQ\n8Fapcml7Urvfzrx1rmefdl0hjBNpL3I37Llxs+NVLuykZ1wUzM7DoBw4dwveO5zC\n1LvO1uyKRDRHgZqVuRu51Qw2Hp/AmUEnmmKNjsZl06uQPLYUQ4Wqr6AY66FlhY5Z\nUqSCun1B88Gk/s+OkeeTvhjrnaXEu+BVvj98EIrm5SZUxm3p9cYJy1TwXZzK/1c5\n48+io24OmIGY/ZfuISe7ii1P1q3oCo0Fk+bg6A70kjreqHC76sA2m7oxTSG3m80C\nwPLkK9HjW4gIJaaH+qN9niRtBOoFjJDOXSRzmvNJDmj63EP/RYUxpDWHs1DfLmxT\npYHnOWgZUhVfcRfV8/3HZtJFh6szvgpaqbyPLfP/8BrdpL7oehU=\n=chCJ\n-----END PGP SIGNATURE-----", "payload": "tree bc5eb9975fb629227249586dea873e20b8c90c6b\nparent 86d775a5860d968d99ee3dfa515c1277aa44745d\nauthor JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1731615772 -0800\ncommitter JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1731799197 -0800\n\nFix artist title for fixing metadata match\n\nFixes #2429\n", "verified_at": "2024-11-16T23:38:30Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/d9f38f9390f41045a812b5fa58c95a15dff7221c", "html_url": "https://github.com/Tautulli/Tautulli/commit/d9f38f9390f41045a812b5fa58c95a15dff7221c", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/d9f38f9390f41045a812b5fa58c95a15dff7221c/comments", "author": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "committer": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "86d775a5860d968d99ee3dfa515c1277aa44745d", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/86d775a5860d968d99ee3dfa515c1277aa44745d", "html_url": "https://github.com/Tautulli/Tautulli/commit/86d775a5860d968d99ee3dfa515c1277aa44745d"}]}, {"sha": "5b47cebdc7011c8718c2dc4312cbe2726ef0c30e", "node_id": "C_kwDOAducuNoAKDViNDdjZWJkYzcwMTFjODcxOGMyZGM0MzEyY2JlMjcyNmVmMGMzMGU", "commit": {"author": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-11-16T23:21:41Z"}, "committer": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-11-16T23:21:41Z"}, "message": "Bump minimum Python version to 3.9", "tree": {"sha": "b3b93e8942bdc94d8bf19165a15b198b359e0a23", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/b3b93e8942bdc94d8bf19165a15b198b359e0a23"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/5b47cebdc7011c8718c2dc4312cbe2726ef0c30e", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\niQIzBAABCAAdFiEEFPKZYCM7sDX3KZzmsfH5gHGEaXoFAmc5KQYACgkQsfH5gHGE\naXq0FRAAl+PBskQ7WHBbbVZMp8pqZIRompdxkf8AaB7gvoSxVf2fdn8iaN75TS1U\n7Y983JNCUYNoFDTqQwcis4YqSeaWDuvTynfdob0+uTjPKbHL/odG9DNK9YykRXSa\n4RZfPVtl8MHf6lwjHOeFiycrpv+eu7BHEg6I0jaNsEGvnL2S+ScvvWUKQXHFXqFy\nmVstTEW3Mv3YI3VxPdcQ/dfS4M40H6zlr3aCtb2hVxkvgANyFAiS9R2DEz+4YXAI\nHSf9DSB+t2CLHtPsDCn/MU03ybdagLS746mRCaRididUfwnEMKJ1vRyCnuwyVRpH\nqAHMtighQWI6zF3Lc4rLY8uQIQ3TxIsmjWliOwTU54VsEZyMLEkf3D7lQ2UWwgA6\nHA8X9G8C6XjTCrrmuVypmaqV2pD8rGG3qX7/wa24E9pnSoCeYlD0DsZSAYqS3kA2\nnRPeYRlGKOn7vymA5o8ahqS8yNVXiO00dasms3utrz4OVDxuLrZgiO7qKLI7JEHc\nOT/l7L/8HjiFoqQv9/SbOms4ZGrKJ9uUdZ3koP/9aSjeuH1pP01GJySNp0X9zR4W\nd9OcctMcMjvrfmmcvi4W5xgMQzPMzwncOrP3czTdOu7XKga5nx+nv+cKlxWK94yW\nVELulrXSIDg8Xp5zthHwsksT4B/2LAHKAgX6wgAdozWxpKhkfDU=\n=PgZc\n-----END PGP SIGNATURE-----", "payload": "tree b3b93e8942bdc94d8bf19165a15b198b359e0a23\nparent d9f38f9390f41045a812b5fa58c95a15dff7221c\nauthor JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1731799301 -0800\ncommitter JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1731799301 -0800\n\nBump minimum Python version to 3.9\n", "verified_at": "2024-11-16T23:38:30Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/5b47cebdc7011c8718c2dc4312cbe2726ef0c30e", "html_url": "https://github.com/Tautulli/Tautulli/commit/5b47cebdc7011c8718c2dc4312cbe2726ef0c30e", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/5b47cebdc7011c8718c2dc4312cbe2726ef0c30e/comments", "author": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "committer": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "d9f38f9390f41045a812b5fa58c95a15dff7221c", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/d9f38f9390f41045a812b5fa58c95a15dff7221c", "html_url": "https://github.com/Tautulli/Tautulli/commit/d9f38f9390f41045a812b5fa58c95a15dff7221c"}]}, {"sha": "afc29604cc8c63b6087bdf45b830c1892dd088e4", "node_id": "C_kwDOAducuNoAKGFmYzI5NjA0Y2M4YzYzYjYwODdiZGY0NWI4MzBjMTg5MmRkMDg4ZTQ", "commit": {"author": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-11-19T17:34:12Z"}, "committer": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-11-19T17:34:48Z"}, "message": "Bump zipp==3.21.0\n\nCloses #2433", "tree": {"sha": "87aa68c00edac02c4aed2e4a75da67b4d4906e02", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/87aa68c00edac02c4aed2e4a75da67b4d4906e02"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/afc29604cc8c63b6087bdf45b830c1892dd088e4", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\niQIzBAABCAAdFiEEFPKZYCM7sDX3KZzmsfH5gHGEaXoFAmc8zDkACgkQsfH5gHGE\naXqC7Q/9Fwxh5AJlZhP/U409RojXWMAL5OphiTh9n1ddVaKwnEw2MOC1s3fawwWL\n9etiEHeQV92JpWB63SICXUhIaN7khMd5QMav+lc/balWdXTMlrsKHoZHavf2IFTn\ndxGWeMAf/jiHPvm14CAbjErNBO5UcWXurS0Y9XQ0vnp3GjhD/06zGiMISisd2+vE\ngiKmyI64alUax17v6aaSDYwWq0Lot2y+4y9+IFzQnL/e3vQ0fO9EJwDCcOJnZ1gn\nmr5wMg4drIYrnXppVHeQTPjRgEUMNm2LfC3XAUvaJulcHzNf4cpT8svIvWcX0GEw\nGdEtg+aGDx2kLFYP/f3DDw6dk7zeHqFBMgiY0OrvdwQSf+0sQa0+hDG8Svt5Xzro\noMMy3xYz/81IDIkR3xNGsJ7/YhsuvAUNUIEBvOLuB8935BLnlXi9SsXKmwAXbmtx\n4a6vvi4Zg8rGdDKJBn9cjvI2gMLPhvkCOoG1YWlhUilPc+qU2TZpy9nMMzUxhbr/\nzY8Xq8rPpWxuUDqIjeNN3pPBS4yZTWPZR41x8cVLg/5L108CeYtrwHFWpIJ5MBsn\neRT2YWwSnILc93ZhUWLEjlsf0twI0PlmVy+3bnS2MstGDohIVEjfrVlFVVkmecJn\n+k+pNXR5pnNXy5Hlp/hZNWXdQ9+3PL7gEgj9jJjrhDFpBdc4xgI=\n=7qkv\n-----END PGP SIGNATURE-----", "payload": "tree 87aa68c00edac02c4aed2e4a75da67b4d4906e02\nparent 5b47cebdc7011c8718c2dc4312cbe2726ef0c30e\nauthor JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1732037652 -0800\ncommitter JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1732037688 -0800\n\nBump zipp==3.21.0\n\nCloses #2433", "verified_at": "2024-11-19T17:35:45Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/afc29604cc8c63b6087bdf45b830c1892dd088e4", "html_url": "https://github.com/Tautulli/Tautulli/commit/afc29604cc8c63b6087bdf45b830c1892dd088e4", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/afc29604cc8c63b6087bdf45b830c1892dd088e4/comments", "author": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "committer": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "5b47cebdc7011c8718c2dc4312cbe2726ef0c30e", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/5b47cebdc7011c8718c2dc4312cbe2726ef0c30e", "html_url": "https://github.com/Tautulli/Tautulli/commit/5b47cebdc7011c8718c2dc4312cbe2726ef0c30e"}]}, {"sha": "85b63fb61a79215923428d47e620e92aa3cf9516", "node_id": "C_kwDOAducuNoAKDg1YjYzZmI2MWE3OTIxNTkyMzQyOGQ0N2U2MjBlOTJhYTNjZjk1MTY", "commit": {"author": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-11-19T17:34:34Z"}, "committer": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-11-19T17:34:49Z"}, "message": "Remove backports.zoneinfo", "tree": {"sha": "4afc037b61bd6647a7e60230dad714dc5cd140e7", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/4afc037b61bd6647a7e60230dad714dc5cd140e7"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/85b63fb61a79215923428d47e620e92aa3cf9516", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\niQIzBAABCAAdFiEEFPKZYCM7sDX3KZzmsfH5gHGEaXoFAmc8zDoACgkQsfH5gHGE\naXqM4Q/8C90t5hkT6XCIO61Fi2xkLE3Xu0OHyDvJ36jH+2ugylSPiU1EurPKDtX8\nsN/egpmcroPiLQZTdfKD7rXHXfpA1o94A8wkydtlKTen0jGu9fxSluSH1Oj7c5WA\n2ZvRoQLFI7Yyz8ueshWEzlnqgwgk0I39/FmhysWE1KwXY/HcPD4gQM+kkzmooWnP\nr1/LnWGUj3weFQLCu360/Y9HPvo9IbkYHHXVrcazss2k1jxZexX3p/JHQmNY9zh0\nwkDDzGw4kTYwzoaflwR8sUkFvZkjcxNg88UDpUetiTUmxQW/TLVnblD7ipI3QEuj\nJ68HbR+xeh/tlCHKZB/ic3Anzw4+CvY3DFKUjw7Hi7UF97riuOpyD7IQswHZi09V\nTpOjGsV3bB+0DcApHibYj18L2ocIAxA7oDusOVntZ+m9VWsu5pPYoJaZyQNAgihA\nCWb2Vsf3K+Mfk5LRwo4QKD4kkoZwX+zvajHBO6SawTSByOBRT3/9zWlzXfcFh6NP\nxHgSkQJrm60iJJz4mn/9MZExVjOOw4e/isO9ozey6qbqKQWW9/WOkSmZ/T4t46us\nCUzx73cpOigZyZoPlv9c3iooZFA9ho0QPgJA3jvimlSGTrM9LOGhUIThq+aDr3FJ\n8ZRQ5OzOuQqxHYMqVvxvKsljLvaekc9Fy6vMiJCfl/ivJny5/dA=\n=4gQ9\n-----END PGP SIGNATURE-----", "payload": "tree 4afc037b61bd6647a7e60230dad714dc5cd140e7\nparent afc29604cc8c63b6087bdf45b830c1892dd088e4\nauthor JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1732037674 -0800\ncommitter JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1732037689 -0800\n\nRemove backports.zoneinfo\n", "verified_at": "2024-11-19T17:35:22Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/85b63fb61a79215923428d47e620e92aa3cf9516", "html_url": "https://github.com/Tautulli/Tautulli/commit/85b63fb61a79215923428d47e620e92aa3cf9516", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/85b63fb61a79215923428d47e620e92aa3cf9516/comments", "author": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "committer": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "afc29604cc8c63b6087bdf45b830c1892dd088e4", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/afc29604cc8c63b6087bdf45b830c1892dd088e4", "html_url": "https://github.com/Tautulli/Tautulli/commit/afc29604cc8c63b6087bdf45b830c1892dd088e4"}]}, {"sha": "baf926e5db82eafa5fbf4e80ab325528cffa8491", "node_id": "C_kwDOAducuNoAKGJhZjkyNmU1ZGI4MmVhZmE1ZmJmNGU4MGFiMzI1NTI4Y2ZmYTg0OTE", "commit": {"author": {"name": "dependabot[bot]", "email": "49699333+dependabot[bot]@users.noreply.github.com", "date": "2024-11-19T17:59:40Z"}, "committer": {"name": "GitHub", "email": "noreply@github.com", "date": "2024-11-19T17:59:40Z"}, "message": "Bump markupsafe from 2.1.5 to 3.0.2 (#2435)\n\nBumps [markupsafe](https://github.com/pallets/markupsafe) from 2.1.5 to 3.0.2.\r\n- [Release notes](https://github.com/pallets/markupsafe/releases)\r\n- [Changelog](https://github.com/pallets/markupsafe/blob/main/CHANGES.rst)\r\n- [Commits](https://github.com/pallets/markupsafe/compare/2.1.5...3.0.2)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: markupsafe\r\n dependency-type: direct:production\r\n update-type: version-update:semver-major\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\n\r\n[skip ci]", "tree": {"sha": "bbbf420eb2594c0b706207b523c01732689f0171", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/bbbf420eb2594c0b706207b523c01732689f0171"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/baf926e5db82eafa5fbf4e80ab325528cffa8491", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJnPNIMCRC1aQ7uu5UhlAAA9ukQADzUbKp813l8uaKQY+4BtmfS\nEoV8I2bH+ThNemnl9ZCodjVTiUhPA/Cpv1t3cC7yhpU0+E32JmIOJT9MPF7GaAT6\nV4SAnzRw4deOJXxp1z5zSowrV/mMMh4EdroFFJaiEMOYu1HY1XFSXgRTIvrcAmo5\n7Pm7o7O3IGHKkD17B5j20TbUx032DQjn9Ksw79GgZ6ZhbdOw4cFUEy3yizubE56C\nOk3qZe11ja3Y9aAKSHvwTSQhgfNsX4aMS3UPYDEuo/rQwvVwWNHaH6/uUH53Hrk/\nz/lEcISbAkNj0+2oEwI1pBObJ5LgzjRAt21+jJv6aGf+tprQudcXnchE/WLtd8t7\nREASUmtwlgEkw1U5zJj4pZkrq+JZeWtl5jVnKSApTQdhEIXDOZTsagQLyLwlDMxz\nqrrkIl07YIDnENtlsiKtePpL6mvJ9v5Hhg8tcXThAmlo+HPNx1B9qlYSnJqRvBTB\nlmbNsYe7GD0PNdEziQE/o4vZnFqCeK5K0b+hzXoxUCyVkZCFGO0TyHCKBZFLZUSF\n7WX+rGziDMSwjJiWFNQmFFSs8+YmLG6nQRwv+OiCDBoUM3RDKwU9SRrAoivoCqtM\n5NCNTkWZy6gIcJn14XL4KXWIV4D/DV7NDQB3F+ykOFsEAfOt4meYNMKE+iaOiyXg\nOXHsIsSB2wAuvpq43m6n\n=zvcl\n-----END PGP SIGNATURE-----\n", "payload": "tree bbbf420eb2594c0b706207b523c01732689f0171\nparent 85b63fb61a79215923428d47e620e92aa3cf9516\nauthor dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1732039180 -0800\ncommitter GitHub <noreply@github.com> 1732039180 -0800\n\nBump markupsafe from 2.1.5 to 3.0.2 (#2435)\n\nBumps [markupsafe](https://github.com/pallets/markupsafe) from 2.1.5 to 3.0.2.\r\n- [Release notes](https://github.com/pallets/markupsafe/releases)\r\n- [Changelog](https://github.com/pallets/markupsafe/blob/main/CHANGES.rst)\r\n- [Commits](https://github.com/pallets/markupsafe/compare/2.1.5...3.0.2)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: markupsafe\r\n dependency-type: direct:production\r\n update-type: version-update:semver-major\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\n\r\n[skip ci]", "verified_at": "2024-11-19T18:00:17Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/baf926e5db82eafa5fbf4e80ab325528cffa8491", "html_url": "https://github.com/Tautulli/Tautulli/commit/baf926e5db82eafa5fbf4e80ab325528cffa8491", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/baf926e5db82eafa5fbf4e80ab325528cffa8491/comments", "author": {"login": "dependabot[bot]", "id": 49699333, "node_id": "MDM6Qm90NDk2OTkzMzM=", "avatar_url": "https://avatars.githubusercontent.com/in/29110?v=4", "gravatar_id": "", "url": "https://api.github.com/users/dependabot%5Bbot%5D", "html_url": "https://github.com/apps/dependabot", "followers_url": "https://api.github.com/users/dependabot%5Bbot%5D/followers", "following_url": "https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}", "gists_url": "https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}", "starred_url": "https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/dependabot%5Bbot%5D/subscriptions", "organizations_url": "https://api.github.com/users/dependabot%5Bbot%5D/orgs", "repos_url": "https://api.github.com/users/dependabot%5Bbot%5D/repos", "events_url": "https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}", "received_events_url": "https://api.github.com/users/dependabot%5Bbot%5D/received_events", "type": "Bot", "user_view_type": "public", "site_admin": false}, "committer": {"login": "web-flow", "id": 19864447, "node_id": "MDQ6VXNlcjE5ODY0NDQ3", "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", "gravatar_id": "", "url": "https://api.github.com/users/web-flow", "html_url": "https://github.com/web-flow", "followers_url": "https://api.github.com/users/web-flow/followers", "following_url": "https://api.github.com/users/web-flow/following{/other_user}", "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", "organizations_url": "https://api.github.com/users/web-flow/orgs", "repos_url": "https://api.github.com/users/web-flow/repos", "events_url": "https://api.github.com/users/web-flow/events{/privacy}", "received_events_url": "https://api.github.com/users/web-flow/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "85b63fb61a79215923428d47e620e92aa3cf9516", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/85b63fb61a79215923428d47e620e92aa3cf9516", "html_url": "https://github.com/Tautulli/Tautulli/commit/85b63fb61a79215923428d47e620e92aa3cf9516"}]}, {"sha": "2fe3f039cc8f39cd7365be9c2e037345ac8db55b", "node_id": "C_kwDOAducuNoAKDJmZTNmMDM5Y2M4ZjM5Y2Q3MzY1YmU5YzJlMDM3MzQ1YWM4ZGI1NWI", "commit": {"author": {"name": "dependabot[bot]", "email": "49699333+dependabot[bot]@users.noreply.github.com", "date": "2024-11-19T17:59:58Z"}, "committer": {"name": "GitHub", "email": "noreply@github.com", "date": "2024-11-19T17:59:58Z"}, "message": "Bump tokenize-rt from 6.0.0 to 6.1.0 (#2436)\n\n* Bump tokenize-rt from 6.0.0 to 6.1.0\r\n\r\nBumps [tokenize-rt](https://github.com/asottile/tokenize-rt) from 6.0.0 to 6.1.0.\r\n- [Commits](https://github.com/asottile/tokenize-rt/compare/v6.0.0...v6.1.0)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: tokenize-rt\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update tokenize-rt==6.1.0\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "tree": {"sha": "de9a87f10dd5eb607aca529a3e8ce56fea353a7d", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/de9a87f10dd5eb607aca529a3e8ce56fea353a7d"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/2fe3f039cc8f39cd7365be9c2e037345ac8db55b", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJnPNIeCRC1aQ7uu5UhlAAAzYYQAHllYkpy5nkPH09C1XyLjiP8\nJbX1h3Thh1mGkHKaJ7ZNw9mRXF5kb7bJNUfIuG5ha/Zckn2+93EMEsuFll0zM0za\npmcuRsu90Qf6QcwBp5/SCqRMelIYux9kxwFJdIGWKd9llCWr76o7x7tbZByp+fYx\nh0tiO8L2Fzyuus81T+kpviV1RMKncLEOO6b/PIHMea+Bf2aYYJwL00kpG36OtVSa\nJal5jeUuLhFE+xyom6J9CtWaDmd7D2gPmFx9+CJ6+5NPaxfzCsltCVZbBxVWsHu1\neTyJC6TCKwyOLe2Etn/vbBAOdgqQPu2e1fihz7OO5w0b4AQW3kRf7ELF80BdG6Av\nP4UBeUPiYTxbrc3bs8ifJmMa+3HDn4jKQ284qhKGXPpyYCP84kpH1CC3f0ZUcn+o\ncvS5q3dI+OwYCtaoTLuoQzgOL+5SDyXfsLQNal9bbKgvam3NnvQqfhcHHAOuZdwV\nlBI3gh1DjCciAli2JDio+TgTXDn0Ung4XDNTgdWOxmWZl0dZXKi3VEiPvA4/3bpF\njpd2OTeZtuvQvAnOZI+mw170BokZ4+dlbr9T+Lf/oPGS/XZ86rEQIR8UXb3cpQ90\nmy3higRdWu3o+Hmig7m355pQvvBj1ADYEdkCMrHsB2ytIo/6us9bSchtjwelyDbx\nb+rIxG8qYsn3YWm2/m3/\n=KdNi\n-----END PGP SIGNATURE-----\n", "payload": "tree de9a87f10dd5eb607aca529a3e8ce56fea353a7d\nparent baf926e5db82eafa5fbf4e80ab325528cffa8491\nauthor dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1732039198 -0800\ncommitter GitHub <noreply@github.com> 1732039198 -0800\n\nBump tokenize-rt from 6.0.0 to 6.1.0 (#2436)\n\n* Bump tokenize-rt from 6.0.0 to 6.1.0\r\n\r\nBumps [tokenize-rt](https://github.com/asottile/tokenize-rt) from 6.0.0 to 6.1.0.\r\n- [Commits](https://github.com/asottile/tokenize-rt/compare/v6.0.0...v6.1.0)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: tokenize-rt\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update tokenize-rt==6.1.0\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "verified_at": "2024-11-19T18:00:17Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/2fe3f039cc8f39cd7365be9c2e037345ac8db55b", "html_url": "https://github.com/Tautulli/Tautulli/commit/2fe3f039cc8f39cd7365be9c2e037345ac8db55b", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/2fe3f039cc8f39cd7365be9c2e037345ac8db55b/comments", "author": {"login": "dependabot[bot]", "id": 49699333, "node_id": "MDM6Qm90NDk2OTkzMzM=", "avatar_url": "https://avatars.githubusercontent.com/in/29110?v=4", "gravatar_id": "", "url": "https://api.github.com/users/dependabot%5Bbot%5D", "html_url": "https://github.com/apps/dependabot", "followers_url": "https://api.github.com/users/dependabot%5Bbot%5D/followers", "following_url": "https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}", "gists_url": "https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}", "starred_url": "https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/dependabot%5Bbot%5D/subscriptions", "organizations_url": "https://api.github.com/users/dependabot%5Bbot%5D/orgs", "repos_url": "https://api.github.com/users/dependabot%5Bbot%5D/repos", "events_url": "https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}", "received_events_url": "https://api.github.com/users/dependabot%5Bbot%5D/received_events", "type": "Bot", "user_view_type": "public", "site_admin": false}, "committer": {"login": "web-flow", "id": 19864447, "node_id": "MDQ6VXNlcjE5ODY0NDQ3", "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", "gravatar_id": "", "url": "https://api.github.com/users/web-flow", "html_url": "https://github.com/web-flow", "followers_url": "https://api.github.com/users/web-flow/followers", "following_url": "https://api.github.com/users/web-flow/following{/other_user}", "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", "organizations_url": "https://api.github.com/users/web-flow/orgs", "repos_url": "https://api.github.com/users/web-flow/repos", "events_url": "https://api.github.com/users/web-flow/events{/privacy}", "received_events_url": "https://api.github.com/users/web-flow/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "baf926e5db82eafa5fbf4e80ab325528cffa8491", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/baf926e5db82eafa5fbf4e80ab325528cffa8491", "html_url": "https://github.com/Tautulli/Tautulli/commit/baf926e5db82eafa5fbf4e80ab325528cffa8491"}]}, {"sha": "be2e63e7e0e2cae014801f63fb9766f01dd09645", "node_id": "C_kwDOAducuNoAKGJlMmU2M2U3ZTBlMmNhZTAxNDgwMWY2M2ZiOTc2NmYwMWRkMDk2NDU", "commit": {"author": {"name": "dependabot[bot]", "email": "49699333+dependabot[bot]@users.noreply.github.com", "date": "2024-11-19T18:00:11Z"}, "committer": {"name": "GitHub", "email": "noreply@github.com", "date": "2024-11-19T18:00:11Z"}, "message": "Bump pyparsing from 3.1.4 to 3.2.0 (#2437)\n\n* Bump pyparsing from 3.1.4 to 3.2.0\r\n\r\nBumps [pyparsing](https://github.com/pyparsing/pyparsing) from 3.1.4 to 3.2.0.\r\n- [Release notes](https://github.com/pyparsing/pyparsing/releases)\r\n- [Changelog](https://github.com/pyparsing/pyparsing/blob/master/CHANGES)\r\n- [Commits](https://github.com/pyparsing/pyparsing/compare/3.1.4...3.2.0)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: pyparsing\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update pyparsing==3.2.0\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "tree": {"sha": "d379bf613a8f263f163ec59a1efea5539f60cb02", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/d379bf613a8f263f163ec59a1efea5539f60cb02"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/be2e63e7e0e2cae014801f63fb9766f01dd09645", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJnPNIrCRC1aQ7uu5UhlAAAQwwQALA8bYV345qO1YHV2uvybFaC\nVUv9/TC33ZVqa2Tq/vZcFMJHouJTFX1l2auKAIDK+HRxisE/AG3Ud2bkZOPaVifw\nF539oOGbg7PBOdQ7Cq6Jk8fEuqtsPVX14InIi1LPz7GEyA8gH+XxQXx5mk3YWClu\nLjGcgNugwGRRP6S0X8eGHWqBVAgkLrQci8trbimqv8CFkl6FmG3L11D7WvjvC99n\nB1nRxOpyCJA9BI3mVksoJPbjlRWqJ4u8mQajZx6li3yXZgAwJEVkhNTQhbEZYpyp\nNvBjIw3hBUWo606gSETRbwq6C2OYWkK87WozJdQn995Nc0xwH6NPVdOg1GIiL8ad\nlbkfxRruV8eMxu8EtW+7yQv7t7jjBg+vjHRl7p7MQZgvDi2UgqpEjXT0xXmBReGI\nQVXvsKrJB3u7zkRPv7ARi1z0H+TxLc581QaCqk+HoHaKKv+oHS7H0EEZ7pq1SORc\nGonYx+E/fHPdu9ARbm0+wzMg2vOywJwkeLoBJlzUMikOm/MivISFKDu2kyZocpGq\npgil+z+GAswv30WBDAaSgU7LquGynwWN1ni9QOSfkYYKiLXrqTdAUDU9Qp96Zwom\nQmUEAtn6SzAx54nkvGGagCTKtzByumLzNIMmVQOobUAIuMIiCD7Nh68QDzuFew0S\nFwJ2JH6Ctg1dy/rd70Il\n=msuc\n-----END PGP SIGNATURE-----\n", "payload": "tree d379bf613a8f263f163ec59a1efea5539f60cb02\nparent 2fe3f039cc8f39cd7365be9c2e037345ac8db55b\nauthor dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1732039211 -0800\ncommitter GitHub <noreply@github.com> 1732039211 -0800\n\nBump pyparsing from 3.1.4 to 3.2.0 (#2437)\n\n* Bump pyparsing from 3.1.4 to 3.2.0\r\n\r\nBumps [pyparsing](https://github.com/pyparsing/pyparsing) from 3.1.4 to 3.2.0.\r\n- [Release notes](https://github.com/pyparsing/pyparsing/releases)\r\n- [Changelog](https://github.com/pyparsing/pyparsing/blob/master/CHANGES)\r\n- [Commits](https://github.com/pyparsing/pyparsing/compare/3.1.4...3.2.0)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: pyparsing\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update pyparsing==3.2.0\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "verified_at": "2024-11-19T18:00:16Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/be2e63e7e0e2cae014801f63fb9766f01dd09645", "html_url": "https://github.com/Tautulli/Tautulli/commit/be2e63e7e0e2cae014801f63fb9766f01dd09645", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/be2e63e7e0e2cae014801f63fb9766f01dd09645/comments", "author": {"login": "dependabot[bot]", "id": 49699333, "node_id": "MDM6Qm90NDk2OTkzMzM=", "avatar_url": "https://avatars.githubusercontent.com/in/29110?v=4", "gravatar_id": "", "url": "https://api.github.com/users/dependabot%5Bbot%5D", "html_url": "https://github.com/apps/dependabot", "followers_url": "https://api.github.com/users/dependabot%5Bbot%5D/followers", "following_url": "https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}", "gists_url": "https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}", "starred_url": "https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/dependabot%5Bbot%5D/subscriptions", "organizations_url": "https://api.github.com/users/dependabot%5Bbot%5D/orgs", "repos_url": "https://api.github.com/users/dependabot%5Bbot%5D/repos", "events_url": "https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}", "received_events_url": "https://api.github.com/users/dependabot%5Bbot%5D/received_events", "type": "Bot", "user_view_type": "public", "site_admin": false}, "committer": {"login": "web-flow", "id": 19864447, "node_id": "MDQ6VXNlcjE5ODY0NDQ3", "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", "gravatar_id": "", "url": "https://api.github.com/users/web-flow", "html_url": "https://github.com/web-flow", "followers_url": "https://api.github.com/users/web-flow/followers", "following_url": "https://api.github.com/users/web-flow/following{/other_user}", "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", "organizations_url": "https://api.github.com/users/web-flow/orgs", "repos_url": "https://api.github.com/users/web-flow/repos", "events_url": "https://api.github.com/users/web-flow/events{/privacy}", "received_events_url": "https://api.github.com/users/web-flow/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "2fe3f039cc8f39cd7365be9c2e037345ac8db55b", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/2fe3f039cc8f39cd7365be9c2e037345ac8db55b", "html_url": "https://github.com/Tautulli/Tautulli/commit/2fe3f039cc8f39cd7365be9c2e037345ac8db55b"}]}, {"sha": "eb2c372d8240983d2bfcc98b62ce05dff4f4de51", "node_id": "C_kwDOAducuNoAKGViMmMzNzJkODI0MDk4M2QyYmZjYzk4YjYyY2UwNWRmZjRmNGRlNTE", "commit": {"author": {"name": "dependabot[bot]", "email": "49699333+dependabot[bot]@users.noreply.github.com", "date": "2024-11-19T18:00:24Z"}, "committer": {"name": "GitHub", "email": "noreply@github.com", "date": "2024-11-19T18:00:24Z"}, "message": "Bump bleach from 6.1.0 to 6.2.0 (#2438)\n\n* Bump bleach from 6.1.0 to 6.2.0\r\n\r\nBumps [bleach](https://github.com/mozilla/bleach) from 6.1.0 to 6.2.0.\r\n- [Changelog](https://github.com/mozilla/bleach/blob/main/CHANGES)\r\n- [Commits](https://github.com/mozilla/bleach/compare/v6.1.0...v6.2.0)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: bleach\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update bleach==6.2.0\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "tree": {"sha": "1cd4567b72bf639b2c07f7dc4cca15f9a03d6855", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/1cd4567b72bf639b2c07f7dc4cca15f9a03d6855"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/eb2c372d8240983d2bfcc98b62ce05dff4f4de51", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJnPNI4CRC1aQ7uu5UhlAAAkP4QAFe/x3KhHIH7GC2Vm1IttNF1\n7+dCy+2nOGofclD3me8VFKOqR7atxtnhYBTS0h3DdqObQ3zthkAbRu3vepUWiVsT\nLMVxwjjt7aKcSaPqOy+xrE8AQltkGF2e6BYLywilfL8xaMX6yP3R1CZnQrltPAxa\nFEeYS43g12gyFJGwLER7QvBbmXABMWEThX9WTyqzaB/T9tPO8qx5s2j6dFKU7sLH\nz9LbuUAlekGoHtM/hA0S2PPwvAzS5dvvDqZz46pkWKSyMD5uoLcWDDe76VubfzZP\nUWOehDcUWR8Yc/6skcczQ2tJXMMBmyjzHqDO5zqxyS22wrzLkmiIR0EfebtDYlHF\nFxSrHlzXzKTaaYD6j0XvJgzBuRuyN+TR8Xgudad1j+eKzEiS9ARMrxn07X0wcFre\nuimrXiFc7iXU5jhKRnqkZHz2f+TOsg52ycv0L2FnuUZFlamt0bOJdyoWBZgKyttG\nQXW3nEI7QGNHanh5eddPsThNA/lJCtZPMWargbl2mw98G+maBJTp72rTzOYCn017\nsKkV5nimdP3g9r8lf7q4DijycImwJz9tWH0aDdOKfnwXGJqrwMlWML/CiFUju8Za\nEiVFz7MRyRJUX0gJ2Gw/mvtAhXuCkzCT61en8PW9czOPL9tdNRXycLZfWLdKM7HE\nZ/0QRy89zzW1QbuDrwBV\n=uSKk\n-----END PGP SIGNATURE-----\n", "payload": "tree 1cd4567b72bf639b2c07f7dc4cca15f9a03d6855\nparent be2e63e7e0e2cae014801f63fb9766f01dd09645\nauthor dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1732039224 -0800\ncommitter GitHub <noreply@github.com> 1732039224 -0800\n\nBump bleach from 6.1.0 to 6.2.0 (#2438)\n\n* Bump bleach from 6.1.0 to 6.2.0\r\n\r\nBumps [bleach](https://github.com/mozilla/bleach) from 6.1.0 to 6.2.0.\r\n- [Changelog](https://github.com/mozilla/bleach/blob/main/CHANGES)\r\n- [Commits](https://github.com/mozilla/bleach/compare/v6.1.0...v6.2.0)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: bleach\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update bleach==6.2.0\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "verified_at": "2024-11-19T18:00:25Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/eb2c372d8240983d2bfcc98b62ce05dff4f4de51", "html_url": "https://github.com/Tautulli/Tautulli/commit/eb2c372d8240983d2bfcc98b62ce05dff4f4de51", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/eb2c372d8240983d2bfcc98b62ce05dff4f4de51/comments", "author": {"login": "dependabot[bot]", "id": 49699333, "node_id": "MDM6Qm90NDk2OTkzMzM=", "avatar_url": "https://avatars.githubusercontent.com/in/29110?v=4", "gravatar_id": "", "url": "https://api.github.com/users/dependabot%5Bbot%5D", "html_url": "https://github.com/apps/dependabot", "followers_url": "https://api.github.com/users/dependabot%5Bbot%5D/followers", "following_url": "https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}", "gists_url": "https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}", "starred_url": "https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/dependabot%5Bbot%5D/subscriptions", "organizations_url": "https://api.github.com/users/dependabot%5Bbot%5D/orgs", "repos_url": "https://api.github.com/users/dependabot%5Bbot%5D/repos", "events_url": "https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}", "received_events_url": "https://api.github.com/users/dependabot%5Bbot%5D/received_events", "type": "Bot", "user_view_type": "public", "site_admin": false}, "committer": {"login": "web-flow", "id": 19864447, "node_id": "MDQ6VXNlcjE5ODY0NDQ3", "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", "gravatar_id": "", "url": "https://api.github.com/users/web-flow", "html_url": "https://github.com/web-flow", "followers_url": "https://api.github.com/users/web-flow/followers", "following_url": "https://api.github.com/users/web-flow/following{/other_user}", "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", "organizations_url": "https://api.github.com/users/web-flow/orgs", "repos_url": "https://api.github.com/users/web-flow/repos", "events_url": "https://api.github.com/users/web-flow/events{/privacy}", "received_events_url": "https://api.github.com/users/web-flow/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "be2e63e7e0e2cae014801f63fb9766f01dd09645", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/be2e63e7e0e2cae014801f63fb9766f01dd09645", "html_url": "https://github.com/Tautulli/Tautulli/commit/be2e63e7e0e2cae014801f63fb9766f01dd09645"}]}, {"sha": "0836fb902c9d5e1c63c7a2225a5b055884038b15", "node_id": "C_kwDOAducuNoAKDA4MzZmYjkwMmM5ZDVlMWM2M2M3YTIyMjVhNWIwNTU4ODQwMzhiMTU", "commit": {"author": {"name": "dependabot[bot]", "email": "49699333+dependabot[bot]@users.noreply.github.com", "date": "2024-11-19T18:00:37Z"}, "committer": {"name": "GitHub", "email": "noreply@github.com", "date": "2024-11-19T18:00:37Z"}, "message": "Bump plexapi from 4.15.16 to 4.16.0 (#2439)\n\n* Bump plexapi from 4.15.16 to 4.16.0\r\n\r\nBumps [plexapi](https://github.com/pkkid/python-plexapi) from 4.15.16 to 4.16.0.\r\n- [Release notes](https://github.com/pkkid/python-plexapi/releases)\r\n- [Commits](https://github.com/pkkid/python-plexapi/compare/4.15.16...4.16.0)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: plexapi\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update plexapi==4.16.0\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "tree": {"sha": "3f9a8214f5b108349040ab8d4d684d84e9d522c5", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/3f9a8214f5b108349040ab8d4d684d84e9d522c5"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/0836fb902c9d5e1c63c7a2225a5b055884038b15", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJnPNJGCRC1aQ7uu5UhlAAACCsQAHtw52+sTVRQet8d2xX1BNjt\nZpYC3X339EFDde1hBCZ5uQSeshkdX5K9k7DjuT49+NzU85V2LH0xSobd3LjQv8vA\nTHLIFYtpoqTemAUCfcZEVypKoSeJmjRve4yH7gJ5E+NMlfjYsvXMcHBh1aJI9hZ0\nFmKalotIlVP68MuyomkNgS+24w8DXlrpdkTHquKJarV40JsJhMVv5jYRv/cQEyEf\njh5synt5F/rb7EVdSKz/lqC+kH16EEqv+jPJqnZ8DZdo42N7yBZs9XoHsLNVIj3r\npCqEkxXL+pOzt+diW7C9Uih8jZ0arND7S0vUyhXeLCJuSVpr224dI4/pgihYRdDL\nWUHy/fw/RB3cVDfxVHO4aAt4JbqXxf7HFg0aRywMwIjTAnR/MKE2L6Sh1Nam/fR2\neXC/1U20Fmn3QDL5w+BhzBlakEAiIwnGvJFH49/ikknLMZvLwBQlcmtukarjqNqd\nA8cATaPZa0zhswqpsg0CokPB2/7sOJbtKZ0b95WC87f+YNz14qb9XMnf7pGunwkn\n6PH3B9lsdh30EO50qzUvQFbtIeOavPya5OQ/qTNa9pHsh61roV4y3qX90Icy2UxK\n5iF+UIxg/WPAewBzMMKGllA6sLjvb8sQdTIsvPZ/4VW5o7wy/3OYKbt+s2qj+Vuv\nv5eVl6ef8Ei05O5yx3+y\n=jlOS\n-----END PGP SIGNATURE-----\n", "payload": "tree 3f9a8214f5b108349040ab8d4d684d84e9d522c5\nparent eb2c372d8240983d2bfcc98b62ce05dff4f4de51\nauthor dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1732039237 -0800\ncommitter GitHub <noreply@github.com> 1732039237 -0800\n\nBump plexapi from 4.15.16 to 4.16.0 (#2439)\n\n* Bump plexapi from 4.15.16 to 4.16.0\r\n\r\nBumps [plexapi](https://github.com/pkkid/python-plexapi) from 4.15.16 to 4.16.0.\r\n- [Release notes](https://github.com/pkkid/python-plexapi/releases)\r\n- [Commits](https://github.com/pkkid/python-plexapi/compare/4.15.16...4.16.0)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: plexapi\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update plexapi==4.16.0\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "verified_at": "2024-11-19T18:00:42Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/0836fb902c9d5e1c63c7a2225a5b055884038b15", "html_url": "https://github.com/Tautulli/Tautulli/commit/0836fb902c9d5e1c63c7a2225a5b055884038b15", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/0836fb902c9d5e1c63c7a2225a5b055884038b15/comments", "author": {"login": "dependabot[bot]", "id": 49699333, "node_id": "MDM6Qm90NDk2OTkzMzM=", "avatar_url": "https://avatars.githubusercontent.com/in/29110?v=4", "gravatar_id": "", "url": "https://api.github.com/users/dependabot%5Bbot%5D", "html_url": "https://github.com/apps/dependabot", "followers_url": "https://api.github.com/users/dependabot%5Bbot%5D/followers", "following_url": "https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}", "gists_url": "https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}", "starred_url": "https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/dependabot%5Bbot%5D/subscriptions", "organizations_url": "https://api.github.com/users/dependabot%5Bbot%5D/orgs", "repos_url": "https://api.github.com/users/dependabot%5Bbot%5D/repos", "events_url": "https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}", "received_events_url": "https://api.github.com/users/dependabot%5Bbot%5D/received_events", "type": "Bot", "user_view_type": "public", "site_admin": false}, "committer": {"login": "web-flow", "id": 19864447, "node_id": "MDQ6VXNlcjE5ODY0NDQ3", "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", "gravatar_id": "", "url": "https://api.github.com/users/web-flow", "html_url": "https://github.com/web-flow", "followers_url": "https://api.github.com/users/web-flow/followers", "following_url": "https://api.github.com/users/web-flow/following{/other_user}", "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", "organizations_url": "https://api.github.com/users/web-flow/orgs", "repos_url": "https://api.github.com/users/web-flow/repos", "events_url": "https://api.github.com/users/web-flow/events{/privacy}", "received_events_url": "https://api.github.com/users/web-flow/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "eb2c372d8240983d2bfcc98b62ce05dff4f4de51", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/eb2c372d8240983d2bfcc98b62ce05dff4f4de51", "html_url": "https://github.com/Tautulli/Tautulli/commit/eb2c372d8240983d2bfcc98b62ce05dff4f4de51"}]}, {"sha": "feca713b76af848359b477daa73929304b35fd8c", "node_id": "C_kwDOAducuNoAKGZlY2E3MTNiNzZhZjg0ODM1OWI0NzdkYWE3MzkyOTMwNGIzNWZkOGM", "commit": {"author": {"name": "dependabot[bot]", "email": "49699333+dependabot[bot]@users.noreply.github.com", "date": "2024-11-19T18:00:50Z"}, "committer": {"name": "GitHub", "email": "noreply@github.com", "date": "2024-11-19T18:00:50Z"}, "message": "Bump dnspython from 2.6.1 to 2.7.0 (#2440)\n\n* Bump dnspython from 2.6.1 to 2.7.0\r\n\r\nBumps [dnspython](https://github.com/rthalley/dnspython) from 2.6.1 to 2.7.0.\r\n- [Release notes](https://github.com/rthalley/dnspython/releases)\r\n- [Changelog](https://github.com/rthalley/dnspython/blob/main/doc/whatsnew.rst)\r\n- [Commits](https://github.com/rthalley/dnspython/compare/v2.6.1...v2.7.0)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: dnspython\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update dnspython==2.7.0\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "tree": {"sha": "2d52bd89415206f2d59f0b1944dc1df05f186738", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/2d52bd89415206f2d59f0b1944dc1df05f186738"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/feca713b76af848359b477daa73929304b35fd8c", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJnPNJSCRC1aQ7uu5UhlAAAaPoQAGoOgLeFZ3HxNyhGR5cmK0qJ\nz+ZLp9NfPahnfwoiSfsji2IGr5DTUk3858ywxw7oo6+PU2GwlxRCt8n0nimtOlsV\nbwR5XWivD89cDLXRCwNyOnZlSEgkoF5M+7gUUiOdn1TXdY5r84Nc6bcVtKD5YVq3\nXv20qETcipZU1K1l1LPAC8Enw//cSUgTOH8J7bvepaTTXS1PR2Brt2dqs5hMHuKj\nEWe7Po/8jwQKuVLALUTvAalo3S/rjQcPKqgEbiE1DLhYUh0VWFpafOQLxbJyyWXg\nAh/FbmjDuhxMXlR6joCsbUo4ue4LnbAAQIV6Eg4rejoqUBSNey/kmAh45VNaxyd4\ng5XQda07MXQSZMcGV6xMJNjpao5s5KYBo2yaDT5Jp1Xc7qUorbadtCE83vlHTT5z\nNmttlGzqYP8KZymw6Nc0PBv8bwlEvu7kQieBd79FKx4Oeh98zu6pHUReA/Cp4eNx\nxqmOrJqCVTYaXXf1umhfVELPmXZ6dZH5I1vAKegVlYBsW0P2rhPL9JJ85w7OA+fH\nsix7cvWpAZ9isCgImbZUiJlKN0BHbHvefC0uTW+2v5nr+0tkdBr7Ulzo4jqoX1wl\n7jXe/sKCMakj06CHh2wrfv9dVzbbjqm8yRwLZiX2BoUs27lJ9Ddm93VRSaxb4//9\nGGGcOMh0FhTSyRAPONk9\n=6PGK\n-----END PGP SIGNATURE-----\n", "payload": "tree 2d52bd89415206f2d59f0b1944dc1df05f186738\nparent 0836fb902c9d5e1c63c7a2225a5b055884038b15\nauthor dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1732039250 -0800\ncommitter GitHub <noreply@github.com> 1732039250 -0800\n\nBump dnspython from 2.6.1 to 2.7.0 (#2440)\n\n* Bump dnspython from 2.6.1 to 2.7.0\r\n\r\nBumps [dnspython](https://github.com/rthalley/dnspython) from 2.6.1 to 2.7.0.\r\n- [Release notes](https://github.com/rthalley/dnspython/releases)\r\n- [Changelog](https://github.com/rthalley/dnspython/blob/main/doc/whatsnew.rst)\r\n- [Commits](https://github.com/rthalley/dnspython/compare/v2.6.1...v2.7.0)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: dnspython\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update dnspython==2.7.0\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "verified_at": "2024-11-19T18:00:58Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/feca713b76af848359b477daa73929304b35fd8c", "html_url": "https://github.com/Tautulli/Tautulli/commit/feca713b76af848359b477daa73929304b35fd8c", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/feca713b76af848359b477daa73929304b35fd8c/comments", "author": {"login": "dependabot[bot]", "id": 49699333, "node_id": "MDM6Qm90NDk2OTkzMzM=", "avatar_url": "https://avatars.githubusercontent.com/in/29110?v=4", "gravatar_id": "", "url": "https://api.github.com/users/dependabot%5Bbot%5D", "html_url": "https://github.com/apps/dependabot", "followers_url": "https://api.github.com/users/dependabot%5Bbot%5D/followers", "following_url": "https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}", "gists_url": "https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}", "starred_url": "https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/dependabot%5Bbot%5D/subscriptions", "organizations_url": "https://api.github.com/users/dependabot%5Bbot%5D/orgs", "repos_url": "https://api.github.com/users/dependabot%5Bbot%5D/repos", "events_url": "https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}", "received_events_url": "https://api.github.com/users/dependabot%5Bbot%5D/received_events", "type": "Bot", "user_view_type": "public", "site_admin": false}, "committer": {"login": "web-flow", "id": 19864447, "node_id": "MDQ6VXNlcjE5ODY0NDQ3", "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", "gravatar_id": "", "url": "https://api.github.com/users/web-flow", "html_url": "https://github.com/web-flow", "followers_url": "https://api.github.com/users/web-flow/followers", "following_url": "https://api.github.com/users/web-flow/following{/other_user}", "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", "organizations_url": "https://api.github.com/users/web-flow/orgs", "repos_url": "https://api.github.com/users/web-flow/repos", "events_url": "https://api.github.com/users/web-flow/events{/privacy}", "received_events_url": "https://api.github.com/users/web-flow/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "0836fb902c9d5e1c63c7a2225a5b055884038b15", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/0836fb902c9d5e1c63c7a2225a5b055884038b15", "html_url": "https://github.com/Tautulli/Tautulli/commit/0836fb902c9d5e1c63c7a2225a5b055884038b15"}]}, {"sha": "dd9a35df51234dbb19ff7f55d6ff788f4242171a", "node_id": "C_kwDOAducuNoAKGRkOWEzNWRmNTEyMzRkYmIxOWZmN2Y1NWQ2ZmY3ODhmNDI0MjE3MWE", "commit": {"author": {"name": "dependabot[bot]", "email": "49699333+dependabot[bot]@users.noreply.github.com", "date": "2024-11-19T18:01:27Z"}, "committer": {"name": "GitHub", "email": "noreply@github.com", "date": "2024-11-19T18:01:27Z"}, "message": "Bump pyjwt from 2.9.0 to 2.10.0 (#2441)\n\n* Bump pyjwt from 2.9.0 to 2.10.0\r\n\r\nBumps [pyjwt](https://github.com/jpadilla/pyjwt) from 2.9.0 to 2.10.0.\r\n- [Release notes](https://github.com/jpadilla/pyjwt/releases)\r\n- [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst)\r\n- [Commits](https://github.com/jpadilla/pyjwt/compare/2.9.0...2.10.0)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: pyjwt\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update pyjwt==2.10.0\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "tree": {"sha": "82f26a406478251991809b30f4cc0ff3b49d5910", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/82f26a406478251991809b30f4cc0ff3b49d5910"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/dd9a35df51234dbb19ff7f55d6ff788f4242171a", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJnPNJ3CRC1aQ7uu5UhlAAAuLMQAAKBVv2FydK7HMZo/rAjnPgk\nl+W549zJWQP0YI4sOp7wq5dR0buPvWcRI8nEyMkeSHQKUmj9wThT0Dah4vTsko5Y\nJ6DZFGMbmafezkMbL6T70E3ll91ti51AROUG4MSjsORjd9b6mVYl5xl6LtqupQiO\nhuQmamx4+KkmhE0cHe3O4qoLoWOY8Auw/OZ8mpGPgg3ZRJVeGqLOHM42pk8jK6oc\nr/0bX7y9zEvVpFV9TXHMOCKsk2ISc54cMTsM0CHNiDR6c/6KgupONIfXjtDBp6Nx\nZHO6wjkVgyxpEuu6ntwIQwBUglfML2nleXoecpvkxFiJ/5tAsxY1PAQW7aBj0lw3\nnOQMAgWbAcGdiEFyE0WUZbg34Dz9NU+5U2X57FdqmO+Xo98oEZuTlsEzO7jsxXWQ\nNTA5+05cwqMwJfIDcRnsR8ELM52rJVAjZnjQSeEF1MHIJy13IDKypPTAOKI1na3Y\nbgXJPN2VlnqZQm7Y59fBG8qRWO2Bx1/RglQM25XdkckYBDTTt6efBGfSL0ukqrU7\nToJLcYZSfbCsZLWFfXKtPhHRc6v3ee07pog+GuASsEbLLpc6Hc4/C5aVOk4c1qaL\nGGRRerS74IQqEWi6yhYl5eyziQ4vGER6MaykVq09fLG7tpvgjHWo/+/p3xJp6ySN\nljKb9co7EcAj9PSk/A6M\n=4IG1\n-----END PGP SIGNATURE-----\n", "payload": "tree 82f26a406478251991809b30f4cc0ff3b49d5910\nparent feca713b76af848359b477daa73929304b35fd8c\nauthor dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1732039287 -0800\ncommitter GitHub <noreply@github.com> 1732039287 -0800\n\nBump pyjwt from 2.9.0 to 2.10.0 (#2441)\n\n* Bump pyjwt from 2.9.0 to 2.10.0\r\n\r\nBumps [pyjwt](https://github.com/jpadilla/pyjwt) from 2.9.0 to 2.10.0.\r\n- [Release notes](https://github.com/jpadilla/pyjwt/releases)\r\n- [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst)\r\n- [Commits](https://github.com/jpadilla/pyjwt/compare/2.9.0...2.10.0)\r\n\r\n---\r\nupdated-dependencies:\r\n- dependency-name: pyjwt\r\n dependency-type: direct:production\r\n update-type: version-update:semver-minor\r\n...\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\n\r\n* Update pyjwt==2.10.0\r\n\r\n---------\r\n\r\nSigned-off-by: dependabot[bot] <support@github.com>\r\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>\r\n\r\n[skip ci]", "verified_at": "2024-11-19T18:01:28Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/dd9a35df51234dbb19ff7f55d6ff788f4242171a", "html_url": "https://github.com/Tautulli/Tautulli/commit/dd9a35df51234dbb19ff7f55d6ff788f4242171a", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/dd9a35df51234dbb19ff7f55d6ff788f4242171a/comments", "author": {"login": "dependabot[bot]", "id": 49699333, "node_id": "MDM6Qm90NDk2OTkzMzM=", "avatar_url": "https://avatars.githubusercontent.com/in/29110?v=4", "gravatar_id": "", "url": "https://api.github.com/users/dependabot%5Bbot%5D", "html_url": "https://github.com/apps/dependabot", "followers_url": "https://api.github.com/users/dependabot%5Bbot%5D/followers", "following_url": "https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}", "gists_url": "https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}", "starred_url": "https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/dependabot%5Bbot%5D/subscriptions", "organizations_url": "https://api.github.com/users/dependabot%5Bbot%5D/orgs", "repos_url": "https://api.github.com/users/dependabot%5Bbot%5D/repos", "events_url": "https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}", "received_events_url": "https://api.github.com/users/dependabot%5Bbot%5D/received_events", "type": "Bot", "user_view_type": "public", "site_admin": false}, "committer": {"login": "web-flow", "id": 19864447, "node_id": "MDQ6VXNlcjE5ODY0NDQ3", "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", "gravatar_id": "", "url": "https://api.github.com/users/web-flow", "html_url": "https://github.com/web-flow", "followers_url": "https://api.github.com/users/web-flow/followers", "following_url": "https://api.github.com/users/web-flow/following{/other_user}", "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", "organizations_url": "https://api.github.com/users/web-flow/orgs", "repos_url": "https://api.github.com/users/web-flow/repos", "events_url": "https://api.github.com/users/web-flow/events{/privacy}", "received_events_url": "https://api.github.com/users/web-flow/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "feca713b76af848359b477daa73929304b35fd8c", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/feca713b76af848359b477daa73929304b35fd8c", "html_url": "https://github.com/Tautulli/Tautulli/commit/feca713b76af848359b477daa73929304b35fd8c"}]}, {"sha": "ea66f6713b8a38dcb75f708b4d6726bb6ffd05e6", "node_id": "C_kwDOAducuNoAKGVhNjZmNjcxM2I4YTM4ZGNiNzVmNzA4YjRkNjcyNmJiNmZmZDA1ZTY", "commit": {"author": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-11-19T18:08:37Z"}, "committer": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-11-19T18:09:02Z"}, "message": "Add hasVoiceActivity to exporter fields", "tree": {"sha": "53b36096c772804d4c85210f9e0d7b26f5ce9082", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/53b36096c772804d4c85210f9e0d7b26f5ce9082"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/ea66f6713b8a38dcb75f708b4d6726bb6ffd05e6", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\niQIzBAABCAAdFiEEFPKZYCM7sDX3KZzmsfH5gHGEaXoFAmc81D4ACgkQsfH5gHGE\naXqrQA//b5vn0hC8ymqCTNBKJYqTHu4OPx2jm8UAa4sbFC0ofzpFuLeJgtcY3+zw\ni03QAgJxCdXbADHAd8JysnWjsFSXvtq2lK8aN491FpleNzDIhzTP+NyqzQkkIXlq\nDMSv54ZBzxlhIpFzSU3C/+mR81v/4W4mZpwUc2UXks6rPJu6XbeHy4lxiC/FI0ys\nWm5jhryau6RpDGlhVeDZt3Jbo9YV5e358SThBgjQBScvDU/jeCLFAseMasrbZn4e\nwWQTpmm0Rwka0STDoXuu0hApZqEqxHZFozCApRlsk3h5KOBoyX1dQf9A0y8Ds+ab\n5ZOG/XIupEMjKJ2EqglyJbymhvvgkFEZErlyrmdt4VdXGx4TN2VNEjAtsrFHT+W+\nq0XNAiSshjxkshGSeSm/c22CXFuRTxrc4Opez/+CyMjEkgYHVv29c0onVjXCjI5j\nR2HKjk2BzFfXvtBwcnBAbU0xVUHONLWTSqQzhH/8ZZI/cWKox/ULTk5vIlnzbZqv\nwmy/tgwTTCs8vdQbH4iqPltbaBBNVu8KFkOmG4+awFB5w/KaOvmSi6hauKDTvnG5\nW0jUmKqhwcZ/0RyxNt4hUJBDps8IMKmzfaMR5PUarf+oIPUcBhAnAOunLwBW87lT\nz0TujFOs6qohX6rYY8rypOPIL7XJqv/BbVE8/Uy/8dF86m8Gyjw=\n=yflU\n-----END PGP SIGNATURE-----", "payload": "tree 53b36096c772804d4c85210f9e0d7b26f5ce9082\nparent dd9a35df51234dbb19ff7f55d6ff788f4242171a\nauthor JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1732039717 -0800\ncommitter JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1732039742 -0800\n\nAdd hasVoiceActivity to exporter fields\n", "verified_at": "2024-11-19T18:09:41Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/ea66f6713b8a38dcb75f708b4d6726bb6ffd05e6", "html_url": "https://github.com/Tautulli/Tautulli/commit/ea66f6713b8a38dcb75f708b4d6726bb6ffd05e6", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/ea66f6713b8a38dcb75f708b4d6726bb6ffd05e6/comments", "author": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "committer": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "dd9a35df51234dbb19ff7f55d6ff788f4242171a", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/dd9a35df51234dbb19ff7f55d6ff788f4242171a", "html_url": "https://github.com/Tautulli/Tautulli/commit/dd9a35df51234dbb19ff7f55d6ff788f4242171a"}]}, {"sha": "5c38de0dfb0a7bd7978d87b7910209f711bd2b03", "node_id": "C_kwDOAducuNoAKDVjMzhkZTBkZmIwYTdiZDc5NzhkODdiNzkxMDIwOWY3MTFiZDJiMDM", "commit": {"author": {"name": "Castle", "email": "7586648+MythodeaLoL@users.noreply.github.com", "date": "2024-11-19T18:12:00Z"}, "committer": {"name": "GitHub", "email": "noreply@github.com", "date": "2024-11-19T18:12:00Z"}, "message": "Allow Telegram blockquote expandable (#2427)\n\n* Allow Telegram blockquote expandable\r\n\r\nBlockquote is not yet supported, this feature adds support along with expandable functionality.\r\n\r\n* Add support for tg-emoji in Telegram HTML\r\n\r\n---------\r\n\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>", "tree": {"sha": "9ff0a081e8ca9e86687ef710c72a1127388cc4ab", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/9ff0a081e8ca9e86687ef710c72a1127388cc4ab"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/5c38de0dfb0a7bd7978d87b7910209f711bd2b03", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJnPNTwCRC1aQ7uu5UhlAAACeEQABidKfvR1QY4DuibAikJz8nc\noLNglAl9ziUtaZ0VLdgbPAI46XCfZCvD3YPdbhAXcOlrlTeS8j6sqEWPJ9AbxJ/v\nzfARcuGlayKxvwadjRp9uIwxpqDQ5fYjPQLqMUZPDqIpeK4iwMxPAzTNdhBIDfQ+\nQXQNddAyC+xA97AK+T12tkgBr/s/qMfUamvr1pAoxrsktdrfkqCpELozcVkAGvV6\nRKE8yc/oJ/3lntBG90H/bMd1h7/9QzkVJL22WVq0ah2LTWxYx3XQc2tX0PnqDIoC\ngZCKzK1Q2W9JvfZVxzAjmJ4UidGVJfaARab3pxOqApg8MA9Lb+kpuQT+y7WSaa4q\nkfoMAOruSQRqLP8oV7WUFHitBfCYn9iD0BC5QfyiDHBFaw0iJrXAWXjn3pBH1MJ2\nAJrWuHmF4Ssw8HmqTih0MbdxEN7yDjOuwR7o4faJ45WyPVRWpafF7zZMB0feo7KS\nhoNYRBalnal6UGuvbh2R4x/DfMC87u9HtBsocgT9RZllHg8iWNvkHPCp5l9tTUhn\n83c0pZjBRfUoyDxWXzIuqVzuSksHdpxbJsU0qs6VavOcLZazt665YNwnhH6iCk+j\na7UQ4oZWO8VABVAF+CxqGXtXbBNtDCDB4dmGM8GP9On/iJWYVcy7JUut8RbQou79\niqMq8wBk9ASEc+6vLobJ\n=nH6B\n-----END PGP SIGNATURE-----\n", "payload": "tree 9ff0a081e8ca9e86687ef710c72a1127388cc4ab\nparent ea66f6713b8a38dcb75f708b4d6726bb6ffd05e6\nauthor Castle <7586648+MythodeaLoL@users.noreply.github.com> 1732039920 -0300\ncommitter GitHub <noreply@github.com> 1732039920 -0800\n\nAllow Telegram blockquote expandable (#2427)\n\n* Allow Telegram blockquote expandable\r\n\r\nBlockquote is not yet supported, this feature adds support along with expandable functionality.\r\n\r\n* Add support for tg-emoji in Telegram HTML\r\n\r\n---------\r\n\r\nCo-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>", "verified_at": "2024-11-19T18:15:21Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/5c38de0dfb0a7bd7978d87b7910209f711bd2b03", "html_url": "https://github.com/Tautulli/Tautulli/commit/5c38de0dfb0a7bd7978d87b7910209f711bd2b03", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/5c38de0dfb0a7bd7978d87b7910209f711bd2b03/comments", "author": {"login": "MythodeaLoL", "id": 7586648, "node_id": "MDQ6VXNlcjc1ODY2NDg=", "avatar_url": "https://avatars.githubusercontent.com/u/7586648?v=4", "gravatar_id": "", "url": "https://api.github.com/users/MythodeaLoL", "html_url": "https://github.com/MythodeaLoL", "followers_url": "https://api.github.com/users/MythodeaLoL/followers", "following_url": "https://api.github.com/users/MythodeaLoL/following{/other_user}", "gists_url": "https://api.github.com/users/MythodeaLoL/gists{/gist_id}", "starred_url": "https://api.github.com/users/MythodeaLoL/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/MythodeaLoL/subscriptions", "organizations_url": "https://api.github.com/users/MythodeaLoL/orgs", "repos_url": "https://api.github.com/users/MythodeaLoL/repos", "events_url": "https://api.github.com/users/MythodeaLoL/events{/privacy}", "received_events_url": "https://api.github.com/users/MythodeaLoL/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "committer": {"login": "web-flow", "id": 19864447, "node_id": "MDQ6VXNlcjE5ODY0NDQ3", "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", "gravatar_id": "", "url": "https://api.github.com/users/web-flow", "html_url": "https://github.com/web-flow", "followers_url": "https://api.github.com/users/web-flow/followers", "following_url": "https://api.github.com/users/web-flow/following{/other_user}", "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", "organizations_url": "https://api.github.com/users/web-flow/orgs", "repos_url": "https://api.github.com/users/web-flow/repos", "events_url": "https://api.github.com/users/web-flow/events{/privacy}", "received_events_url": "https://api.github.com/users/web-flow/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "ea66f6713b8a38dcb75f708b4d6726bb6ffd05e6", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/ea66f6713b8a38dcb75f708b4d6726bb6ffd05e6", "html_url": "https://github.com/Tautulli/Tautulli/commit/ea66f6713b8a38dcb75f708b4d6726bb6ffd05e6"}]}, {"sha": "9c473c6528d43bcf14eded46597f8fe86496d0a7", "node_id": "C_kwDOAducuNoAKDljNDczYzY1MjhkNDNiY2YxNGVkZWQ0NjU5N2Y4ZmU4NjQ5NmQwYTc", "commit": {"author": {"name": "peagravel", "email": "peagravel@proton.me", "date": "2024-11-19T18:14:45Z"}, "committer": {"name": "GitHub", "email": "noreply@github.com", "date": "2024-11-19T18:14:45Z"}, "message": "Add friendly name to the top bar of config modals (#2432)", "tree": {"sha": "996536d6c40328f5ae4b0f518988d675f71a548d", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/996536d6c40328f5ae4b0f518988d675f71a548d"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/9c473c6528d43bcf14eded46597f8fe86496d0a7", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\nwsFcBAABCAAQBQJnPNWVCRC1aQ7uu5UhlAAAHaQQAKuEdWB6LZMXMZKlyKPehiF7\nxhZy1TcO/yngtN/EVuy5LpkVnbTVS8yGFcxrrJa1Czj/NeCTfmPF4xs+19VQ7+38\n6SuKRKvSDdzgi7ogbX1T1tniI5cMgwJssvVNmM4m5iFI/taL5UoNooCJ7ZCZ1MQ/\nCcXp1y2UmgQRMqKxufIOcmekOAXaRG0m/biikZMmfE2kVreWwM6UcrQ+wnOpJuZ5\nZ02qCfs4FEer9QIeWZyDKK93vlfiObQKA9gsPb5kfgXdHgjeR+C0BsXrwNNTZFZ+\nXomIs+zGGBZJDM0N/Vvs9WVrVYbplkk/k/WNg9Wopenx/643Uk7HmQtFSVFSecP+\neZZWZxMyJ+RfBEZbiGAOGCM/8/w6rgi20Z+fE2eSUZjrEyKCYTE5sabHt4yv8P30\nKEGHv3OLg03htmD+5swc813AJRVJIMssIwwDD+J1eHH0T1fD2/lwlh3lMoNxUhVY\nQHaN9/12Jum2s29eLmYqfouxXMM4Xw6th62/IbcuWt4ijyZ65l35UosAts60z71Y\nDU4NtzBh5E2/RHjTbLI5BuQ+9VXp0IbpUmjGjEs49fThoE6Jk79YGwnJY3qowMlK\nDJXf2o07d0vDRaKis97jHYziLCGtg2mEraZ2n8hmr3Ae2TPj4CnBqfkmHR6wQyVH\nl92Caw1QjfDHgmSUpOcq\n=y/Qe\n-----END PGP SIGNATURE-----\n", "payload": "tree 996536d6c40328f5ae4b0f518988d675f71a548d\nparent 5c38de0dfb0a7bd7978d87b7910209f711bd2b03\nauthor peagravel <peagravel@proton.me> 1732040085 -0300\ncommitter GitHub <noreply@github.com> 1732040085 -0800\n\nAdd friendly name to the top bar of config modals (#2432)\n\n", "verified_at": "2024-11-19T18:15:20Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/9c473c6528d43bcf14eded46597f8fe86496d0a7", "html_url": "https://github.com/Tautulli/Tautulli/commit/9c473c6528d43bcf14eded46597f8fe86496d0a7", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/9c473c6528d43bcf14eded46597f8fe86496d0a7/comments", "author": {"login": "peagravel", "id": 186631124, "node_id": "U_kgDOCx_D1A", "avatar_url": "https://avatars.githubusercontent.com/u/186631124?v=4", "gravatar_id": "", "url": "https://api.github.com/users/peagravel", "html_url": "https://github.com/peagravel", "followers_url": "https://api.github.com/users/peagravel/followers", "following_url": "https://api.github.com/users/peagravel/following{/other_user}", "gists_url": "https://api.github.com/users/peagravel/gists{/gist_id}", "starred_url": "https://api.github.com/users/peagravel/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/peagravel/subscriptions", "organizations_url": "https://api.github.com/users/peagravel/orgs", "repos_url": "https://api.github.com/users/peagravel/repos", "events_url": "https://api.github.com/users/peagravel/events{/privacy}", "received_events_url": "https://api.github.com/users/peagravel/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "committer": {"login": "web-flow", "id": 19864447, "node_id": "MDQ6VXNlcjE5ODY0NDQ3", "avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4", "gravatar_id": "", "url": "https://api.github.com/users/web-flow", "html_url": "https://github.com/web-flow", "followers_url": "https://api.github.com/users/web-flow/followers", "following_url": "https://api.github.com/users/web-flow/following{/other_user}", "gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}", "starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/web-flow/subscriptions", "organizations_url": "https://api.github.com/users/web-flow/orgs", "repos_url": "https://api.github.com/users/web-flow/repos", "events_url": "https://api.github.com/users/web-flow/events{/privacy}", "received_events_url": "https://api.github.com/users/web-flow/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "5c38de0dfb0a7bd7978d87b7910209f711bd2b03", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/5c38de0dfb0a7bd7978d87b7910209f711bd2b03", "html_url": "https://github.com/Tautulli/Tautulli/commit/5c38de0dfb0a7bd7978d87b7910209f711bd2b03"}]}, {"sha": "6e6fe1fb65f9111d54d79f50613e6d7e877d7084", "node_id": "C_kwDOAducuNoAKDZlNmZlMWZiNjVmOTExMWQ1NGQ3OWY1MDYxM2U2ZDdlODc3ZDcwODQ", "commit": {"author": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-11-19T18:36:34Z"}, "committer": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-11-19T18:36:34Z"}, "message": "Add slugs to metadata details", "tree": {"sha": "63531135d9df65f270d9aefb8e50887410a5a91e", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/63531135d9df65f270d9aefb8e50887410a5a91e"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/6e6fe1fb65f9111d54d79f50613e6d7e877d7084", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\niQIzBAABCAAdFiEEFPKZYCM7sDX3KZzmsfH5gHGEaXoFAmc82rIACgkQsfH5gHGE\naXpmAQ/6AuGRm6wau+SCA0v6ApIAljrq2nSrLSeCG6QbDZtndhmQxyV1T6Ch1Cww\nP3LRarPY4GD/JdPeJmuyRxYBvPTmYe0nNTfZXkMOxeicgReNwwJEEvGXIr3fyesZ\nIalJ3H7hpMNJLwqkO2UfGYy6BekWX3vIA/v8Pj3jyEzZo0u3xxv1B3MdCRJjUwDZ\nvV6wJ63Z2M7l2ElZ7cq/mGPjP6mT3ranGqsA3xREoAkhgC3BQercIV9vynxYjhKX\nYAlSmTMuPO7xTGHMwxXWPz0PzxqKqiiKRmjL3sTAh7uogD1EY7EZeO3zzkymBoGu\nzs6r1Jb7+VEM5D8kpEOEuNImR8Rw9EtDyZDPN9PKmDuDpk5zzq7PQXIVpA2tEClA\nNdMfkDRKhSaID0n1uB2+VdeSjSOekB9EPhjvLWosn460Zgcqti/44FP0oY0M9MLp\ntpKwz5c4/nDYmEki/dCp26PQQleSlPWJSLYeR86Y6vrQv5pZKESti/T9IZWTETzi\nZFaIvZAPINWkqpstdhu3NQlwKFG8aG2B0wh5+gL+CiulVtDLqbYpzPOmT8v/ydH8\nGOmeCA9+gFeP714y7ZkM0v1zjeJY8RJ0ugxDiwbduMq3ear/hRzJkctYZOPOtxdF\nA9vwzRRUJ00pePTM7kDEjRXFXwwRCJeNE4satmuS342S9nuGAHg=\n=ckHa\n-----END PGP SIGNATURE-----", "payload": "tree 63531135d9df65f270d9aefb8e50887410a5a91e\nparent 9c473c6528d43bcf14eded46597f8fe86496d0a7\nauthor JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1732041394 -0800\ncommitter JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1732041394 -0800\n\nAdd slugs to metadata details\n", "verified_at": "2024-11-19T19:00:58Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/6e6fe1fb65f9111d54d79f50613e6d7e877d7084", "html_url": "https://github.com/Tautulli/Tautulli/commit/6e6fe1fb65f9111d54d79f50613e6d7e877d7084", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/6e6fe1fb65f9111d54d79f50613e6d7e877d7084/comments", "author": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "committer": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "9c473c6528d43bcf14eded46597f8fe86496d0a7", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/9c473c6528d43bcf14eded46597f8fe86496d0a7", "html_url": "https://github.com/Tautulli/Tautulli/commit/9c473c6528d43bcf14eded46597f8fe86496d0a7"}]}, {"sha": "b9cb7102c44badcbbb0812126e3849b86e715a03", "node_id": "C_kwDOAducuNoAKGI5Y2I3MTAyYzQ0YmFkY2JiYjA4MTIxMjZlMzg0OWI4NmU3MTVhMDM", "commit": {"author": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-11-19T18:58:55Z"}, "committer": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-11-19T18:58:55Z"}, "message": "Add plex_slug and plex_watch_url to nofication parameters", "tree": {"sha": "e68a40574477d6533aabb223ef9b57500b2d9344", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/e68a40574477d6533aabb223ef9b57500b2d9344"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/b9cb7102c44badcbbb0812126e3849b86e715a03", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\niQIzBAABCAAdFiEEFPKZYCM7sDX3KZzmsfH5gHGEaXoFAmc83+8ACgkQsfH5gHGE\naXqvBQ//f6TATzkUBMReo6bExbY3m9jj8LocvtXgu3ZCHVMS6HkCeDgzj+PYuaAS\n3i9WWwUIbDwjKoD3yuImnyIaPgSBVb6KDcHsLKiwvH8VNWN7BX39A3f3P90JJPE9\ndBtVsNyQT9TqNPCxbjdSCNtWKChEIKfSReplPnLsUWJeEBmbBH98X2YC8qWdim9b\ngG4XC4WxQkyyj6DMuDFwKlxbrij2TiukU/WXbJhMMEF85hBWZf2rFket4+IFLmS7\nrP2/rwgoSUPlnz2tuqf6/2hrqMvjHJyZjJFfYeKIkf7Nh27ID8mfhBUXpNePTpsF\n/NSFyhE0/lgiGd72bL7qj0r6zLZizQkXsRbPm3T8g3dG/1VOzhLY6CFk6OD58x+K\neUmOMbrDFPckLGlZn9UlezChVStVK1SHSrobejuyidMJNqSSK42XoU0TJBOtafeP\nDimdonzLXHX3Bnt+kdZY0bpoTNel7Kk7/1NPs6Bdnhg2kzFO7fWkByYBQw6paWWq\nA5BUBQv3jkpve8K49pe+YHx35TCRRfJiQnMdWfNmj6jycmrGVbIPPAYLVz1vlPc2\nZ2T36TgodDhTOsvsfKQDyB4QbDx6RAjD80z9KqZ0Pbdts+4AWeS9QMmGtGOcpYVp\nJ8okBIFO4EyKqe2hdrOqvqDiu//Mju1HSPOhNs0HkyUf0wh8z/I=\n=tx+H\n-----END PGP SIGNATURE-----", "payload": "tree e68a40574477d6533aabb223ef9b57500b2d9344\nparent 6e6fe1fb65f9111d54d79f50613e6d7e877d7084\nauthor JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1732042735 -0800\ncommitter JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1732042735 -0800\n\nAdd plex_slug and plex_watch_url to nofication parameters\n", "verified_at": "2024-11-19T19:00:58Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/b9cb7102c44badcbbb0812126e3849b86e715a03", "html_url": "https://github.com/Tautulli/Tautulli/commit/b9cb7102c44badcbbb0812126e3849b86e715a03", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/b9cb7102c44badcbbb0812126e3849b86e715a03/comments", "author": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "committer": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "6e6fe1fb65f9111d54d79f50613e6d7e877d7084", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/6e6fe1fb65f9111d54d79f50613e6d7e877d7084", "html_url": "https://github.com/Tautulli/Tautulli/commit/6e6fe1fb65f9111d54d79f50613e6d7e877d7084"}]}, {"sha": "ca0e1c321d264e8d86a6594ea7fc4ca3a426dc9d", "node_id": "C_kwDOAducuNoAKGNhMGUxYzMyMWQyNjRlOGQ4NmE2NTk0ZWE3ZmM0Y2EzYTQyNmRjOWQ", "commit": {"author": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-11-24T22:17:35Z"}, "committer": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-11-24T22:17:35Z"}, "message": "Convert CustomArrow to string in newsletter raw json", "tree": {"sha": "432adedd2f9de69b1ab2118e9f66d11a37b18355", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/432adedd2f9de69b1ab2118e9f66d11a37b18355"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/ca0e1c321d264e8d86a6594ea7fc4ca3a426dc9d", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\niQIzBAABCAAdFiEEFPKZYCM7sDX3KZzmsfH5gHGEaXoFAmdDpf8ACgkQsfH5gHGE\naXo+KhAAgvRKD/kdW71/AbOGMcVMAzzxN/jtmvF4Pg7qzCzNcFQy/8nBo+XCVNzG\n/sg6yH5ve4ehrehULlL0O0lx+sZymg4dkwNBIDdtWH4XLpzVxRbOE8FX8ojy5Bku\nVLGRGWC+hKTPBy6WSRE8I0bxfAjvam8TXAAcwC5ZwkO15FuObsxUgz4LuOBXjslu\nl8nkvg9tGmCajPM+lHEYeHoP194q28Pb4roB0usVTwMqKqmGzWVvHWYl02b3aegz\n+bsTaarAJGcfUUD8/wYOdmNo/aMGQaTISYFy9sJKciZsU9MefVvqRpd1yzWTR9Qx\nIrcLeON23qteqsCFyx86dZBrLlqMVSh4vBodsWPeux2v9dwyZ8Ssm+GrYZlg62SZ\nn01jWLtK6s0Glss0RMhqm9cAhy6udMSuGdwJ1TZJ0ujb5JeBxmJOrWGUKWGrN+rc\nQb8ElR9ZSeeWp+EggudTgvaDJy3bHDj0pl0aDRJ6kyORYloDHGXTBUqg95h4Trhs\ntPe5WPdusWrdBOCTPX+WpN17k9aa04ZjoWlputs6Fn/FU+pJp8dajlqrwGFr59pb\n2C9jvisGX7QBa6pcUlVqA42OvRTVKZMaAr9g7CTE7YFi0fijmfmWtnd0ALNhTEoY\nsA/uwroDot75IsTUFDC77zx9qAuArF9cBzDbC6l9a+MuRrixg1k=\n=F1Zn\n-----END PGP SIGNATURE-----", "payload": "tree 432adedd2f9de69b1ab2118e9f66d11a37b18355\nparent b9cb7102c44badcbbb0812126e3849b86e715a03\nauthor JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1732486655 -0800\ncommitter JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1732486655 -0800\n\nConvert CustomArrow to string in newsletter raw json\n", "verified_at": "2024-11-24T22:59:02Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/ca0e1c321d264e8d86a6594ea7fc4ca3a426dc9d", "html_url": "https://github.com/Tautulli/Tautulli/commit/ca0e1c321d264e8d86a6594ea7fc4ca3a426dc9d", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/ca0e1c321d264e8d86a6594ea7fc4ca3a426dc9d/comments", "author": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "committer": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "b9cb7102c44badcbbb0812126e3849b86e715a03", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/b9cb7102c44badcbbb0812126e3849b86e715a03", "html_url": "https://github.com/Tautulli/Tautulli/commit/b9cb7102c44badcbbb0812126e3849b86e715a03"}]}, {"sha": "62a05712f8b272734c6d722f15dd14cf6c450720", "node_id": "C_kwDOAducuNoAKDYyYTA1NzEyZjhiMjcyNzM0YzZkNzIyZjE1ZGQxNGNmNmM0NTA3MjA", "commit": {"author": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-11-24T22:55:51Z"}, "committer": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-11-24T22:55:51Z"}, "message": "Add logos to exporter", "tree": {"sha": "28b56d8d5401b5b4c93be8f3385f915e24ab0934", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/28b56d8d5401b5b4c93be8f3385f915e24ab0934"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/62a05712f8b272734c6d722f15dd14cf6c450720", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\niQIzBAABCAAdFiEEFPKZYCM7sDX3KZzmsfH5gHGEaXoFAmdDrvgACgkQsfH5gHGE\naXplPA/+JIFxfOvBXvD93CeePUHNf12915/C/PQESIwCcmLCQqiA8WhSvz0FByYL\nmKPUB05WBklQG5+TzeWMVSuL73G6JfGqASKy0QPU6Jc7yMsZjGF3J+vi4nU/4KPl\nBtcW1ewHJwMc4wnfL0X/P4bQWgcj3UNWnOKSUkDjbxMBS2q0uu8AVFLh8pIxW3sq\n2HCcZvZutZUd5EEdVu5AYv6UXl21Q0bBHLyd/ykJ3D2LFe5tnxJh2xx7WpMDXATV\ngAfPaeiuXxeBeR/+RE+hKGE283haBwqLDHffXTxw3cAXEu8EFc4fE4oxGIft0sQx\nyN8Xa6L7wnk8hz24L+kUs/K8XebRkTsicoeNIiQz1pgN9ipxZHBpYUY6LovR654b\nbXKj4L76gbgFWg4eopbOH5sSfIXVj+d5ftge9BXNEe+6wWy3Y/Ht3Q0PVWfw02hm\n9YyfcAglQvRcMq/HBbZa5cRGXSnxvI5e+m6e1mlD78tmP2n5ndu1WvLFblN2QuOB\nz7DoC2YyYkRmRRUPyqTe+l+43FGB3gCNHxA/zW7yteSrfsO5meprZefWYn7+pQ/H\nTqGP+0IY6kAmhEGuHF6WKbRgxorE7E0khcgiCsgJxPwKuZjdR2IW3Pke8XcMFkXu\nK9QlLkFnV6B5FAQ2cE/sUxiQWpDKmxYMOZ/BDcykzud3fE20Pa0=\n=qnmi\n-----END PGP SIGNATURE-----", "payload": "tree 28b56d8d5401b5b4c93be8f3385f915e24ab0934\nparent ca0e1c321d264e8d86a6594ea7fc4ca3a426dc9d\nauthor JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1732488951 -0800\ncommitter JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1732488951 -0800\n\nAdd logos to exporter\n", "verified_at": "2024-11-24T22:59:02Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/62a05712f8b272734c6d722f15dd14cf6c450720", "html_url": "https://github.com/Tautulli/Tautulli/commit/62a05712f8b272734c6d722f15dd14cf6c450720", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/62a05712f8b272734c6d722f15dd14cf6c450720/comments", "author": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "committer": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "ca0e1c321d264e8d86a6594ea7fc4ca3a426dc9d", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/ca0e1c321d264e8d86a6594ea7fc4ca3a426dc9d", "html_url": "https://github.com/Tautulli/Tautulli/commit/ca0e1c321d264e8d86a6594ea7fc4ca3a426dc9d"}]}, {"sha": "78864d7a97ab94d433f5eca205897f2b10be455b", "node_id": "C_kwDOAducuNoAKDc4ODY0ZDdhOTdhYjk0ZDQzM2Y1ZWNhMjA1ODk3ZjJiMTBiZTQ1NWI", "commit": {"author": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-11-24T22:57:29Z"}, "committer": {"name": "JonnyWong16", "email": "9099342+JonnyWong16@users.noreply.github.com", "date": "2024-11-24T22:57:29Z"}, "message": "v2.15.0", "tree": {"sha": "c4c09e79e80b93e2550cbca8bd0cf0d1c1dbd2fc", "url": "https://api.github.com/repos/Tautulli/Tautulli/git/trees/c4c09e79e80b93e2550cbca8bd0cf0d1c1dbd2fc"}, "url": "https://api.github.com/repos/Tautulli/Tautulli/git/commits/78864d7a97ab94d433f5eca205897f2b10be455b", "comment_count": 0, "verification": {"verified": true, "reason": "valid", "signature": "-----BEGIN PGP SIGNATURE-----\n\niQIzBAABCAAdFiEEFPKZYCM7sDX3KZzmsfH5gHGEaXoFAmdDr1kACgkQsfH5gHGE\naXowexAAg3PxvB+i7cpBtD8jHkEeh7ZBAwk8hQS4hKrbxBMeEe9rl48w0/IM+MxV\n0yh0Z0zxkZqEMJ5afvtG4dQAH0kO81wAh8mIvr86TEPXoUNW/MuZy/9cfv+cCMlZ\nN+06RYKaLzXhk6vuFEYo0QPjHMZXgFPl07n3h5I/qVpVsoJxa3SIoz+S30Q9FPOi\nOxSrBnNYo9nvZzj/9tdOV5w/RtGXDJdFSCm1viQRnTgen72JeAnI7v58nK4sIL0W\nbirUJhg6fvWOfUKunQdOKDBQnLFwDcx73DLC8/OFM+8P1xYPwr1CqT4wkmTqonW/\nQNhrNACJLwKGzVVNnAKRSn0tb9wFB67iJVt00i0O7BBuALiV31gNQfHSYV2yQ2uQ\no6nNk1ZeuRX4hzXvDmIUcPUF0Nq0OUPEtqsn6hDCxoNJwpKYeHbAO6OlAxPYMmT6\nXWz2vaYa27NGKuNtl2txoDlUIKYUnOFFI0aDlEHclDnrdGeV+tw0SndCrmlLLIx3\nEDp+9/G+lXF3aeDLHsfqOOf+G/uuUE0BrT7hQ2ZuAihcL1F8uWI6Qg1XVolgZ1Fm\n2JbrSa+RQ00nHLmndBjlKUBpiXyfO+GnScO7zC3o8ZeBCOHzD1KiUse7B0vazkO3\nhHWfTdI0NSRHD1E5wUIpqUNdI0JvXa/H52z62G8AK68VGXmkrpk=\n=MB6X\n-----END PGP SIGNATURE-----", "payload": "tree c4c09e79e80b93e2550cbca8bd0cf0d1c1dbd2fc\nparent 62a05712f8b272734c6d722f15dd14cf6c450720\nauthor JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1732489049 -0800\ncommitter JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> 1732489049 -0800\n\nv2.15.0\n", "verified_at": "2024-11-24T22:59:01Z"}}, "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/78864d7a97ab94d433f5eca205897f2b10be455b", "html_url": "https://github.com/Tautulli/Tautulli/commit/78864d7a97ab94d433f5eca205897f2b10be455b", "comments_url": "https://api.github.com/repos/Tautulli/Tautulli/commits/78864d7a97ab94d433f5eca205897f2b10be455b/comments", "author": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "committer": {"login": "JonnyWong16", "id": 9099342, "node_id": "MDQ6VXNlcjkwOTkzNDI=", "avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4", "gravatar_id": "", "url": "https://api.github.com/users/JonnyWong16", "html_url": "https://github.com/JonnyWong16", "followers_url": "https://api.github.com/users/JonnyWong16/followers", "following_url": "https://api.github.com/users/JonnyWong16/following{/other_user}", "gists_url": "https://api.github.com/users/JonnyWong16/gists{/gist_id}", "starred_url": "https://api.github.com/users/JonnyWong16/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/JonnyWong16/subscriptions", "organizations_url": "https://api.github.com/users/JonnyWong16/orgs", "repos_url": "https://api.github.com/users/JonnyWong16/repos", "events_url": "https://api.github.com/users/JonnyWong16/events{/privacy}", "received_events_url": "https://api.github.com/users/JonnyWong16/received_events", "type": "User", "user_view_type": "public", "site_admin": false}, "parents": [{"sha": "62a05712f8b272734c6d722f15dd14cf6c450720", "url": "https://api.github.com/repos/Tautulli/Tautulli/commits/62a05712f8b272734c6d722f15dd14cf6c450720", "html_url": "https://github.com/Tautulli/Tautulli/commit/62a05712f8b272734c6d722f15dd14cf6c450720"}]}], "files": [{"sha": "abd3f8d4b89c6194a4d6b8e999ab5e88b69f33e0", "filename": "CHANGELOG.md", "status": "modified", "additions": 24, "deletions": 0, "changes": 24, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/CHANGELOG.md", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/CHANGELOG.md", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/CHANGELOG.md?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,5 +1,29 @@\n # Changelog\n \n+## v2.15.0 (2024-11-24)\n+\n+* Notes:\n+ * Support for Python 3.8 has been dropped. The minimum Python version is now 3.9.\n+* Notifications:\n+ * New: Allow Telegram blockquote and tg-emoji HTML tags. (Thanks @MythodeaLoL) (#2427)\n+ * New: Added Plex slug and Plex Watch URL notification parameters. (#2420)\n+ * Change: Update OneSignal API calls to use the new API endpoint for Tautulli Remote App notifications.\n+* Newsletters:\n+ * Fix: Dumping custom dates in raw newsletter json.\n+* History:\n+ * Fix: Unable to fix match for artists. (#2429)\n+* Exporter:\n+ * New: Added movie and episode hasVoiceActivity attribute to exporter fields.\n+ * New: Added subtitle canAutoSync attribute to exporter fields.\n+ * New: Added logos to the exporter fields.\n+* UI:\n+ * New: Add friendly name to the top bar of config modals. (Thanks @peagravel) (#2432)\n+* API:\n+ * New: Added plex slugs to metadata in the get_metadata API command.\n+* Other:\n+ * Fix: Tautulli failing to start with Python 3.13. (#2426)\n+\n+\n ## v2.14.6 (2024-10-12)\n \n * Newsletters:"}, {"sha": "1f24741e7dd34b149e3353b50a049afacef0fd56", "filename": "README.md", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/README.md", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/README.md", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/README.md?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -36,7 +36,7 @@ and [PlexWatchWeb](https://github.com/ecleese/plexWatchWeb).\n [![Docker Stars][badge-docker-stars]][DockerHub]\n [![Downloads][badge-downloads]][Releases Latest]\n \n-[badge-python]: https://img.shields.io/badge/python->=3.8-blue?style=flat-square\n+[badge-python]: https://img.shields.io/badge/python->=3.9-blue?style=flat-square\n [badge-docker-pulls]: https://img.shields.io/docker/pulls/tautulli/tautulli?style=flat-square\n [badge-docker-stars]: https://img.shields.io/docker/stars/tautulli/tautulli?style=flat-square\n [badge-downloads]: https://img.shields.io/github/downloads/Tautulli/Tautulli/total?style=flat-square"}, {"sha": "390ee26878a5767feab4bf3b79244513b521aafb", "filename": "data/interfaces/default/export_modal.html", "status": "modified", "additions": 21, "deletions": 0, "changes": 21, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/data%2Finterfaces%2Fdefault%2Fexport_modal.html", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/data%2Finterfaces%2Fdefault%2Fexport_modal.html", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/data%2Finterfaces%2Fdefault%2Fexport_modal.html?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -20,6 +20,7 @@\n export = exporter.Export()\n thumb_media_types = ', '.join([export.PLURAL_MEDIA_TYPES[k] for k, v in export.MEDIA_TYPES.items() if v[0]])\n art_media_types = ', '.join([export.PLURAL_MEDIA_TYPES[k] for k, v in export.MEDIA_TYPES.items() if v[1]])\n+ logo_media_types = ', '.join([export.PLURAL_MEDIA_TYPES[k] for k, v in export.MEDIA_TYPES.items() if v[2]])\n %>\n <div class=\"modal-dialog\" role=\"document\">\n <div class=\"modal-content\">\n@@ -144,6 +145,22 @@ <h4 class=\"modal-title\" id=\"info-modal-title\">\n Select the level to export background artwork image files.<br>Note: Only applies to ${art_media_types}.\n </p>\n </div>\n+ <div class=\"form-group\">\n+ <label for=\"export_logo_level\">Logo Image Export Level</label>\n+ <div class=\"row\">\n+ <div class=\"col-md-12\">\n+ <select class=\"form-control\" id=\"export_logo_level\" name=\"export_logo_level\">\n+ <option value=\"0\" selected>Level 0 - None / Custom</option>\n+ <option value=\"1\">Level 1 - Uploaded and Selected Logos Only</option>\n+ <option value=\"2\">Level 2 - Selected and Locked Logos Only</option>\n+ <option value=\"9\">Level 9 - All Selected Logos</option>\n+ </select>\n+ </div>\n+ </div>\n+ <p class=\"help-block\">\n+ Select the level to export logo image files.<br>Note: Only applies to ${logo_media_types}.\n+ </p>\n+ </div>\n <p class=\"help-block\">\n Warning: Exporting images may take a long time! Images will be saved to a folder alongside the data file.\n </p>\n@@ -231,13 +248,15 @@ <h4 class=\"modal-title\" id=\"info-modal-title\">\n $('#export_media_info_level').prop('disabled', true);\n $(\"#export_thumb_level\").prop('disabled', true);\n $(\"#export_art_level\").prop('disabled', true);\n+ $(\"#export_logo_level\").prop('disabled', true);\n export_custom_metadata_fields.disable();\n export_custom_media_info_fields.disable();\n } else {\n $('#export_metadata_level').prop('disabled', false);\n $('#export_media_info_level').prop('disabled', false);\n $(\"#export_thumb_level\").prop('disabled', false);\n $(\"#export_art_level\").prop('disabled', false);\n+ $(\"#export_logo_level\").prop('disabled', false);\n export_custom_metadata_fields.enable();\n export_custom_media_info_fields.enable();\n }\n@@ -252,6 +271,7 @@ <h4 class=\"modal-title\" id=\"info-modal-title\">\n var file_format = $('#export_file_format option:selected').val();\n var thumb_level = $(\"#export_thumb_level option:selected\").val();\n var art_level = $(\"#export_art_level option:selected\").val();\n+ var logo_level = $(\"#export_logo_level option:selected\").val();\n var custom_fields = [\n $('#export_custom_metadata_fields').val(),\n $('#export_custom_media_info_fields').val()\n@@ -270,6 +290,7 @@ <h4 class=\"modal-title\" id=\"info-modal-title\">\n file_format: file_format,\n thumb_level: thumb_level,\n art_level: art_level,\n+ logo_level: logo_level,\n custom_fields: custom_fields,\n export_type: export_type,\n individual_files: individual_files"}, {"sha": "1d8a7b43cd61ad4e3820083f231df4448112b74c", "filename": "data/interfaces/default/js/tables/export_table.js", "status": "modified", "additions": 3, "deletions": 3, "changes": 6, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/data%2Finterfaces%2Fdefault%2Fjs%2Ftables%2Fexport_table.js", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/data%2Finterfaces%2Fdefault%2Fjs%2Ftables%2Fexport_table.js", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/data%2Finterfaces%2Fdefault%2Fjs%2Ftables%2Fexport_table.js?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -100,7 +100,7 @@ export_table_options = {\n \"createdCell\": function (td, cellData, rowData, row, col) {\n if (cellData !== '') {\n var images = '';\n- if (rowData['thumb_level'] || rowData['art_level']) {\n+ if (rowData['thumb_level'] || rowData['art_level'] || rowData['logo_level']) {\n images = ' + images';\n }\n $(td).html(cellData + images);\n@@ -161,14 +161,14 @@ export_table_options = {\n if (cellData === 1 && rowData['exists']) {\n var tooltip_title = '';\n var icon = '';\n- if (rowData['thumb_level'] || rowData['art_level'] || rowData['individual_files']) {\n+ if (rowData['thumb_level'] || rowData['art_level'] || rowData['logo_level'] || rowData['individual_files']) {\n tooltip_title = 'Zip Archive';\n icon = 'fa-file-archive';\n } else {\n tooltip_title = rowData['file_format'].toUpperCase() + ' File';\n icon = 'fa-file-download';\n }\n- var icon = (rowData['thumb_level'] || rowData['art_level'] || rowData['individual_files']) ? 'fa-file-archive' : 'fa-file-download';\n+ var icon = (rowData['thumb_level'] || rowData['art_level'] || rowData['logo_level'] || rowData['individual_files']) ? 'fa-file-archive' : 'fa-file-download';\n $(td).html('<button class=\"btn btn-xs btn-success pull-left\" data-id=\"' + rowData['export_id'] + '\"><span data-toggle=\"tooltip\" data-placement=\"left\" title=\"' + tooltip_title + '\"><i class=\"fa ' + icon + ' fa-fw\"></i> Download</span></button>');\n } else if (cellData === 0) {\n var percent = Math.min(getPercent(rowData['exported_items'], rowData['total_items']), 99)"}, {"sha": "7808060349fb70d555dd77ed452182dafc351409", "filename": "data/interfaces/default/mobile_device_config.html", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/data%2Finterfaces%2Fdefault%2Fmobile_device_config.html", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/data%2Finterfaces%2Fdefault%2Fmobile_device_config.html", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/data%2Finterfaces%2Fdefault%2Fmobile_device_config.html?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -3,7 +3,7 @@\n <div class=\"modal-content\">\n <div class=\"modal-header\">\n <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\"><i class=\"fa fa-remove\"></i></button>\n- <h4 class=\"modal-title\" id=\"mobile-device-config-modal-header\">${device['device_name']} Settings <small><span class=\"device_id\">(Device ID: ${device['id']})</span></small></h4>\n+ <h4 class=\"modal-title\" id=\"mobile-device-config-modal-header\">${device['device_name']} Settings <small><span class=\"device_id\">(Device ID: ${device['id']}${' - ' + device['friendly_name'] if device['friendly_name'] else ''})</span></small></h4>\n </div>\n <div class=\"modal-body\">\n <div class=\"container-fluid\">"}, {"sha": "2e46c998a76b62ba6f195bf997ffb4efc818b8a5", "filename": "data/interfaces/default/newsletter_config.html", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/data%2Finterfaces%2Fdefault%2Fnewsletter_config.html", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/data%2Finterfaces%2Fdefault%2Fnewsletter_config.html", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/data%2Finterfaces%2Fdefault%2Fnewsletter_config.html?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -13,7 +13,7 @@\n <div class=\"modal-content\">\n <div class=\"modal-header\">\n <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\"><i class=\"fa fa-remove\"></i></button>\n- <h4 class=\"modal-title\" id=\"newsletter-config-modal-header\">${newsletter['agent_label']} Newsletter Settings <small><span class=\"newsletter_id\">(Newsletter ID: ${newsletter['id']})</span></small></h4>\n+ <h4 class=\"modal-title\" id=\"newsletter-config-modal-header\">${newsletter['agent_label']} Newsletter Settings <small><span class=\"newsletter_id\">(Newsletter ID: ${newsletter['id']}${' - ' + newsletter['friendly_name'] if newsletter['friendly_name'] else ''})</span></small></h4>\n </div>\n <div class=\"modal-body\">\n <div class=\"container-fluid\">"}, {"sha": "ff7e1d03e6bb3c7151a44c18a520c15f6553546c", "filename": "data/interfaces/default/notifier_config.html", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/data%2Finterfaces%2Fdefault%2Fnotifier_config.html", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/data%2Finterfaces%2Fdefault%2Fnotifier_config.html", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/data%2Finterfaces%2Fdefault%2Fnotifier_config.html?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -12,7 +12,7 @@\n <div class=\"modal-content\">\n <div class=\"modal-header\">\n <button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\"><i class=\"fa fa-remove\"></i></button>\n- <h4 class=\"modal-title\" id=\"notifier-config-modal-header\">${notifier['agent_label']} Settings <small><span class=\"notifier_id\">(Notifier ID: ${notifier['id']})</span></small></h4>\n+ <h4 class=\"modal-title\" id=\"notifier-config-modal-header\">${notifier['agent_label']} Settings <small><span class=\"notifier_id\">(Notifier ID: ${notifier['id']}${' - ' + notifier['friendly_name'] if notifier['friendly_name'] else ''})</span></small></h4>\n </div>\n <div class=\"modal-body\">\n <div class=\"container-fluid\">"}, {"sha": "861fc48e5078e83e1c08ddebae39589103c5ac0b", "filename": "lib/backports/zoneinfo/__init__.py", "status": "removed", "additions": 0, "deletions": 49, "changes": 49, "blob_url": "https://github.com/Tautulli/Tautulli/blob/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fbackports%2Fzoneinfo%2F__init__.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fbackports%2Fzoneinfo%2F__init__.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fbackports%2Fzoneinfo%2F__init__.py?ref=940c2ae6cd064817512aa7646386a31fa9f465fa", "patch": "@@ -1,49 +0,0 @@\n-__all__ = [\n- \"ZoneInfo\",\n- \"reset_tzpath\",\n- \"available_timezones\",\n- \"TZPATH\",\n- \"ZoneInfoNotFoundError\",\n- \"InvalidTZPathWarning\",\n-]\n-import sys\n-\n-from . import _tzpath\n-from ._common import ZoneInfoNotFoundError\n-from ._version import __version__\n-\n-try:\n- from ._czoneinfo import ZoneInfo\n-except ImportError: # pragma: nocover\n- from ._zoneinfo import ZoneInfo\n-\n-reset_tzpath = _tzpath.reset_tzpath\n-available_timezones = _tzpath.available_timezones\n-InvalidTZPathWarning = _tzpath.InvalidTZPathWarning\n-\n-if sys.version_info < (3, 7):\n- # Module-level __getattr__ was added in Python 3.7, so instead of lazily\n- # populating TZPATH on every access, we will register a callback with\n- # reset_tzpath to update the top-level tuple.\n- TZPATH = _tzpath.TZPATH\n-\n- def _tzpath_callback(new_tzpath):\n- global TZPATH\n- TZPATH = new_tzpath\n-\n- _tzpath.TZPATH_CALLBACKS.append(_tzpath_callback)\n- del _tzpath_callback\n-\n-else:\n-\n- def __getattr__(name):\n- if name == \"TZPATH\":\n- return _tzpath.TZPATH\n- else:\n- raise AttributeError(\n- f\"module {__name__!r} has no attribute {name!r}\"\n- )\n-\n-\n-def __dir__():\n- return sorted(list(globals()) + [\"TZPATH\"])"}, {"sha": "6e56abf2cdcf03046b10c9943e52c265829dc67c", "filename": "lib/backports/zoneinfo/__init__.pyi", "status": "removed", "additions": 0, "deletions": 45, "changes": 45, "blob_url": "https://github.com/Tautulli/Tautulli/blob/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fbackports%2Fzoneinfo%2F__init__.pyi", "raw_url": "https://github.com/Tautulli/Tautulli/raw/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fbackports%2Fzoneinfo%2F__init__.pyi", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fbackports%2Fzoneinfo%2F__init__.pyi?ref=940c2ae6cd064817512aa7646386a31fa9f465fa", "patch": "@@ -1,45 +0,0 @@\n-import os\n-import typing\n-from datetime import datetime, tzinfo\n-from typing import (\n- Any,\n- Iterable,\n- Optional,\n- Protocol,\n- Sequence,\n- Set,\n- Type,\n- Union,\n-)\n-\n-_T = typing.TypeVar(\"_T\", bound=\"ZoneInfo\")\n-\n-class _IOBytes(Protocol):\n- def read(self, __size: int) -> bytes: ...\n- def seek(self, __size: int, __whence: int = ...) -> Any: ...\n-\n-class ZoneInfo(tzinfo):\n- @property\n- def key(self) -> str: ...\n- def __init__(self, key: str) -> None: ...\n- @classmethod\n- def no_cache(cls: Type[_T], key: str) -> _T: ...\n- @classmethod\n- def from_file(\n- cls: Type[_T], __fobj: _IOBytes, key: Optional[str] = ...\n- ) -> _T: ...\n- @classmethod\n- def clear_cache(cls, *, only_keys: Iterable[str] = ...) -> None: ...\n-\n-# Note: Both here and in clear_cache, the types allow the use of `str` where\n-# a sequence of strings is required. This should be remedied if a solution\n-# to this typing bug is found: https://github.com/python/typing/issues/256\n-def reset_tzpath(\n- to: Optional[Sequence[Union[os.PathLike, str]]] = ...\n-) -> None: ...\n-def available_timezones() -> Set[str]: ...\n-\n-TZPATH: Sequence[str]\n-\n-class ZoneInfoNotFoundError(KeyError): ...\n-class InvalidTZPathWarning(RuntimeWarning): ..."}, {"sha": "27a6ab026eac2586e6449c25d82f4c8bad2d9219", "filename": "lib/backports/zoneinfo/_common.py", "status": "removed", "additions": 0, "deletions": 171, "changes": 171, "blob_url": "https://github.com/Tautulli/Tautulli/blob/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fbackports%2Fzoneinfo%2F_common.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fbackports%2Fzoneinfo%2F_common.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fbackports%2Fzoneinfo%2F_common.py?ref=940c2ae6cd064817512aa7646386a31fa9f465fa", "patch": "@@ -1,171 +0,0 @@\n-import struct\n-\n-\n-def load_tzdata(key):\n- try:\n- import importlib.resources as importlib_resources\n- except ImportError:\n- import importlib_resources\n-\n- components = key.split(\"/\")\n- package_name = \".\".join([\"tzdata.zoneinfo\"] + components[:-1])\n- resource_name = components[-1]\n-\n- try:\n- return importlib_resources.open_binary(package_name, resource_name)\n- except (ImportError, FileNotFoundError, UnicodeEncodeError):\n- # There are three types of exception that can be raised that all amount\n- # to \"we cannot find this key\":\n- #\n- # ImportError: If package_name doesn't exist (e.g. if tzdata is not\n- # installed, or if there's an error in the folder name like\n- # Amrica/New_York)\n- # FileNotFoundError: If resource_name doesn't exist in the package\n- # (e.g. Europe/Krasnoy)\n- # UnicodeEncodeError: If package_name or resource_name are not UTF-8,\n- # such as keys containing a surrogate character.\n- raise ZoneInfoNotFoundError(f\"No time zone found with key {key}\")\n-\n-\n-def load_data(fobj):\n- header = _TZifHeader.from_file(fobj)\n-\n- if header.version == 1:\n- time_size = 4\n- time_type = \"l\"\n- else:\n- # Version 2+ has 64-bit integer transition times\n- time_size = 8\n- time_type = \"q\"\n-\n- # Version 2+ also starts with a Version 1 header and data, which\n- # we need to skip now\n- skip_bytes = (\n- header.timecnt * 5 # Transition times and types\n- + header.typecnt * 6 # Local time type records\n- + header.charcnt # Time zone designations\n- + header.leapcnt * 8 # Leap second records\n- + header.isstdcnt # Standard/wall indicators\n- + header.isutcnt # UT/local indicators\n- )\n-\n- fobj.seek(skip_bytes, 1)\n-\n- # Now we need to read the second header, which is not the same\n- # as the first\n- header = _TZifHeader.from_file(fobj)\n-\n- typecnt = header.typecnt\n- timecnt = header.timecnt\n- charcnt = header.charcnt\n-\n- # The data portion starts with timecnt transitions and indices\n- if timecnt:\n- trans_list_utc = struct.unpack(\n- f\">{timecnt}{time_type}\", fobj.read(timecnt * time_size)\n- )\n- trans_idx = struct.unpack(f\">{timecnt}B\", fobj.read(timecnt))\n- else:\n- trans_list_utc = ()\n- trans_idx = ()\n-\n- # Read the ttinfo struct, (utoff, isdst, abbrind)\n- if typecnt:\n- utcoff, isdst, abbrind = zip(\n- *(struct.unpack(\">lbb\", fobj.read(6)) for i in range(typecnt))\n- )\n- else:\n- utcoff = ()\n- isdst = ()\n- abbrind = ()\n-\n- # Now read the abbreviations. They are null-terminated strings, indexed\n- # not by position in the array but by position in the unsplit\n- # abbreviation string. I suppose this makes more sense in C, which uses\n- # null to terminate the strings, but it's inconvenient here...\n- abbr_vals = {}\n- abbr_chars = fobj.read(charcnt)\n-\n- def get_abbr(idx):\n- # Gets a string starting at idx and running until the next \\x00\n- #\n- # We cannot pre-populate abbr_vals by splitting on \\x00 because there\n- # are some zones that use subsets of longer abbreviations, like so:\n- #\n- # LMT\\x00AHST\\x00HDT\\x00\n- #\n- # Where the idx to abbr mapping should be:\n- #\n- # {0: \"LMT\", 4: \"AHST\", 5: \"HST\", 9: \"HDT\"}\n- if idx not in abbr_vals:\n- span_end = abbr_chars.find(b\"\\x00\", idx)\n- abbr_vals[idx] = abbr_chars[idx:span_end].decode()\n-\n- return abbr_vals[idx]\n-\n- abbr = tuple(get_abbr(idx) for idx in abbrind)\n-\n- # The remainder of the file consists of leap seconds (currently unused) and\n- # the standard/wall and ut/local indicators, which are metadata we don't need.\n- # In version 2 files, we need to skip the unnecessary data to get at the TZ string:\n- if header.version >= 2:\n- # Each leap second record has size (time_size + 4)\n- skip_bytes = header.isutcnt + header.isstdcnt + header.leapcnt * 12\n- fobj.seek(skip_bytes, 1)\n-\n- c = fobj.read(1) # Should be \\n\n- assert c == b\"\\n\", c\n-\n- tz_bytes = b\"\"\n- while True:\n- c = fobj.read(1)\n- if c == b\"\\n\":\n- break\n- tz_bytes += c\n-\n- tz_str = tz_bytes\n- else:\n- tz_str = None\n-\n- return trans_idx, trans_list_utc, utcoff, isdst, abbr, tz_str\n-\n-\n-class _TZifHeader:\n- __slots__ = [\n- \"version\",\n- \"isutcnt\",\n- \"isstdcnt\",\n- \"leapcnt\",\n- \"timecnt\",\n- \"typecnt\",\n- \"charcnt\",\n- ]\n-\n- def __init__(self, *args):\n- assert len(self.__slots__) == len(args)\n- for attr, val in zip(self.__slots__, args):\n- setattr(self, attr, val)\n-\n- @classmethod\n- def from_file(cls, stream):\n- # The header starts with a 4-byte \"magic\" value\n- if stream.read(4) != b\"TZif\":\n- raise ValueError(\"Invalid TZif file: magic not found\")\n-\n- _version = stream.read(1)\n- if _version == b\"\\x00\":\n- version = 1\n- else:\n- version = int(_version)\n- stream.read(15)\n-\n- args = (version,)\n-\n- # Slots are defined in the order that the bytes are arranged\n- args = args + struct.unpack(\">6l\", stream.read(24))\n-\n- return cls(*args)\n-\n-\n-class ZoneInfoNotFoundError(KeyError):\n- \"\"\"Exception raised when a ZoneInfo key is not found.\"\"\""}, {"sha": "9baaf6bce573e28a2bf0f17e2b248a92aa66a39d", "filename": "lib/backports/zoneinfo/_tzpath.py", "status": "removed", "additions": 0, "deletions": 207, "changes": 207, "blob_url": "https://github.com/Tautulli/Tautulli/blob/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fbackports%2Fzoneinfo%2F_tzpath.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fbackports%2Fzoneinfo%2F_tzpath.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fbackports%2Fzoneinfo%2F_tzpath.py?ref=940c2ae6cd064817512aa7646386a31fa9f465fa", "patch": "@@ -1,207 +0,0 @@\n-import os\n-import sys\n-\n-PY36 = sys.version_info < (3, 7)\n-\n-\n-def reset_tzpath(to=None):\n- global TZPATH\n-\n- tzpaths = to\n- if tzpaths is not None:\n- if isinstance(tzpaths, (str, bytes)):\n- raise TypeError(\n- f\"tzpaths must be a list or tuple, \"\n- + f\"not {type(tzpaths)}: {tzpaths!r}\"\n- )\n-\n- if not all(map(os.path.isabs, tzpaths)):\n- raise ValueError(_get_invalid_paths_message(tzpaths))\n- base_tzpath = tzpaths\n- else:\n- env_var = os.environ.get(\"PYTHONTZPATH\", None)\n- if env_var is not None:\n- base_tzpath = _parse_python_tzpath(env_var)\n- elif sys.platform != \"win32\":\n- base_tzpath = [\n- \"/usr/share/zoneinfo\",\n- \"/usr/lib/zoneinfo\",\n- \"/usr/share/lib/zoneinfo\",\n- \"/etc/zoneinfo\",\n- ]\n-\n- base_tzpath.sort(key=lambda x: not os.path.exists(x))\n- else:\n- base_tzpath = ()\n-\n- TZPATH = tuple(base_tzpath)\n-\n- if TZPATH_CALLBACKS:\n- for callback in TZPATH_CALLBACKS:\n- callback(TZPATH)\n-\n-\n-def _parse_python_tzpath(env_var):\n- if not env_var:\n- return ()\n-\n- raw_tzpath = env_var.split(os.pathsep)\n- new_tzpath = tuple(filter(os.path.isabs, raw_tzpath))\n-\n- # If anything has been filtered out, we will warn about it\n- if len(new_tzpath) != len(raw_tzpath):\n- import warnings\n-\n- msg = _get_invalid_paths_message(raw_tzpath)\n-\n- warnings.warn(\n- \"Invalid paths specified in PYTHONTZPATH environment variable.\"\n- + msg,\n- InvalidTZPathWarning,\n- )\n-\n- return new_tzpath\n-\n-\n-def _get_invalid_paths_message(tzpaths):\n- invalid_paths = (path for path in tzpaths if not os.path.isabs(path))\n-\n- prefix = \"\\n \"\n- indented_str = prefix + prefix.join(invalid_paths)\n-\n- return (\n- \"Paths should be absolute but found the following relative paths:\"\n- + indented_str\n- )\n-\n-\n-if sys.version_info < (3, 8):\n-\n- def _isfile(path):\n- # bpo-33721: In Python 3.8 non-UTF8 paths return False rather than\n- # raising an error. See https://bugs.python.org/issue33721\n- try:\n- return os.path.isfile(path)\n- except ValueError:\n- return False\n-\n-\n-else:\n- _isfile = os.path.isfile\n-\n-\n-def find_tzfile(key):\n- \"\"\"Retrieve the path to a TZif file from a key.\"\"\"\n- _validate_tzfile_path(key)\n- for search_path in TZPATH:\n- filepath = os.path.join(search_path, key)\n- if _isfile(filepath):\n- return filepath\n-\n- return None\n-\n-\n-_TEST_PATH = os.path.normpath(os.path.join(\"_\", \"_\"))[:-1]\n-\n-\n-def _validate_tzfile_path(path, _base=_TEST_PATH):\n- if os.path.isabs(path):\n- raise ValueError(\n- f\"ZoneInfo keys may not be absolute paths, got: {path}\"\n- )\n-\n- # We only care about the kinds of path normalizations that would change the\n- # length of the key - e.g. a/../b -> a/b, or a/b/ -> a/b. On Windows,\n- # normpath will also change from a/b to a\\b, but that would still preserve\n- # the length.\n- new_path = os.path.normpath(path)\n- if len(new_path) != len(path):\n- raise ValueError(\n- f\"ZoneInfo keys must be normalized relative paths, got: {path}\"\n- )\n-\n- resolved = os.path.normpath(os.path.join(_base, new_path))\n- if not resolved.startswith(_base):\n- raise ValueError(\n- f\"ZoneInfo keys must refer to subdirectories of TZPATH, got: {path}\"\n- )\n-\n-\n-del _TEST_PATH\n-\n-\n-def available_timezones():\n- \"\"\"Returns a set containing all available time zones.\n-\n- .. caution::\n-\n- This may attempt to open a large number of files, since the best way to\n- determine if a given file on the time zone search path is to open it\n- and check for the \"magic string\" at the beginning.\n- \"\"\"\n- try:\n- from importlib import resources\n- except ImportError:\n- import importlib_resources as resources\n-\n- valid_zones = set()\n-\n- # Start with loading from the tzdata package if it exists: this has a\n- # pre-assembled list of zones that only requires opening one file.\n- try:\n- with resources.open_text(\"tzdata\", \"zones\") as f:\n- for zone in f:\n- zone = zone.strip()\n- if zone:\n- valid_zones.add(zone)\n- except (ImportError, FileNotFoundError):\n- pass\n-\n- def valid_key(fpath):\n- try:\n- with open(fpath, \"rb\") as f:\n- return f.read(4) == b\"TZif\"\n- except Exception: # pragma: nocover\n- return False\n-\n- for tz_root in TZPATH:\n- if not os.path.exists(tz_root):\n- continue\n-\n- for root, dirnames, files in os.walk(tz_root):\n- if root == tz_root:\n- # right/ and posix/ are special directories and shouldn't be\n- # included in the output of available zones\n- if \"right\" in dirnames:\n- dirnames.remove(\"right\")\n- if \"posix\" in dirnames:\n- dirnames.remove(\"posix\")\n-\n- for file in files:\n- fpath = os.path.join(root, file)\n-\n- key = os.path.relpath(fpath, start=tz_root)\n- if os.sep != \"/\": # pragma: nocover\n- key = key.replace(os.sep, \"/\")\n-\n- if not key or key in valid_zones:\n- continue\n-\n- if valid_key(fpath):\n- valid_zones.add(key)\n-\n- if \"posixrules\" in valid_zones:\n- # posixrules is a special symlink-only time zone where it exists, it\n- # should not be included in the output\n- valid_zones.remove(\"posixrules\")\n-\n- return valid_zones\n-\n-\n-class InvalidTZPathWarning(RuntimeWarning):\n- \"\"\"Warning raised if an invalid path is specified in PYTHONTZPATH.\"\"\"\n-\n-\n-TZPATH = ()\n-TZPATH_CALLBACKS = []\n-reset_tzpath()"}, {"sha": "3ced3581bb601ae91b1e1da4b8f4f520855a065e", "filename": "lib/backports/zoneinfo/_version.py", "status": "removed", "additions": 0, "deletions": 1, "changes": 1, "blob_url": "https://github.com/Tautulli/Tautulli/blob/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fbackports%2Fzoneinfo%2F_version.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fbackports%2Fzoneinfo%2F_version.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fbackports%2Fzoneinfo%2F_version.py?ref=940c2ae6cd064817512aa7646386a31fa9f465fa", "patch": "@@ -1 +0,0 @@\n-__version__ = \"0.2.1\""}, {"sha": "c15a553491cd9a55079d5e127d1c754178ba9724", "filename": "lib/backports/zoneinfo/_zoneinfo.py", "status": "removed", "additions": 0, "deletions": 754, "changes": 754, "blob_url": "https://github.com/Tautulli/Tautulli/blob/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fbackports%2Fzoneinfo%2F_zoneinfo.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fbackports%2Fzoneinfo%2F_zoneinfo.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fbackports%2Fzoneinfo%2F_zoneinfo.py?ref=940c2ae6cd064817512aa7646386a31fa9f465fa", "patch": "@@ -1,754 +0,0 @@\n-import bisect\n-import calendar\n-import collections\n-import functools\n-import re\n-import weakref\n-from datetime import datetime, timedelta, tzinfo\n-\n-from . import _common, _tzpath\n-\n-EPOCH = datetime(1970, 1, 1)\n-EPOCHORDINAL = datetime(1970, 1, 1).toordinal()\n-\n-# It is relatively expensive to construct new timedelta objects, and in most\n-# cases we're looking at the same deltas, like integer numbers of hours, etc.\n-# To improve speed and memory use, we'll keep a dictionary with references\n-# to the ones we've already used so far.\n-#\n-# Loading every time zone in the 2020a version of the time zone database\n-# requires 447 timedeltas, which requires approximately the amount of space\n-# that ZoneInfo(\"America/New_York\") with 236 transitions takes up, so we will\n-# set the cache size to 512 so that in the common case we always get cache\n-# hits, but specifically crafted ZoneInfo objects don't leak arbitrary amounts\n-# of memory.\n-@functools.lru_cache(maxsize=512)\n-def _load_timedelta(seconds):\n- return timedelta(seconds=seconds)\n-\n-\n-class ZoneInfo(tzinfo):\n- _strong_cache_size = 8\n- _strong_cache = collections.OrderedDict()\n- _weak_cache = weakref.WeakValueDictionary()\n- __module__ = \"backports.zoneinfo\"\n-\n- def __init_subclass__(cls):\n- cls._strong_cache = collections.OrderedDict()\n- cls._weak_cache = weakref.WeakValueDictionary()\n-\n- def __new__(cls, key):\n- instance = cls._weak_cache.get(key, None)\n- if instance is None:\n- instance = cls._weak_cache.setdefault(key, cls._new_instance(key))\n- instance._from_cache = True\n-\n- # Update the \"strong\" cache\n- cls._strong_cache[key] = cls._strong_cache.pop(key, instance)\n-\n- if len(cls._strong_cache) > cls._strong_cache_size:\n- cls._strong_cache.popitem(last=False)\n-\n- return instance\n-\n- @classmethod\n- def no_cache(cls, key):\n- obj = cls._new_instance(key)\n- obj._from_cache = False\n-\n- return obj\n-\n- @classmethod\n- def _new_instance(cls, key):\n- obj = super().__new__(cls)\n- obj._key = key\n- obj._file_path = obj._find_tzfile(key)\n-\n- if obj._file_path is not None:\n- file_obj = open(obj._file_path, \"rb\")\n- else:\n- file_obj = _common.load_tzdata(key)\n-\n- with file_obj as f:\n- obj._load_file(f)\n-\n- return obj\n-\n- @classmethod\n- def from_file(cls, fobj, key=None):\n- obj = super().__new__(cls)\n- obj._key = key\n- obj._file_path = None\n- obj._load_file(fobj)\n- obj._file_repr = repr(fobj)\n-\n- # Disable pickling for objects created from files\n- obj.__reduce__ = obj._file_reduce\n-\n- return obj\n-\n- @classmethod\n- def clear_cache(cls, *, only_keys=None):\n- if only_keys is not None:\n- for key in only_keys:\n- cls._weak_cache.pop(key, None)\n- cls._strong_cache.pop(key, None)\n-\n- else:\n- cls._weak_cache.clear()\n- cls._strong_cache.clear()\n-\n- @property\n- def key(self):\n- return self._key\n-\n- def utcoffset(self, dt):\n- return self._find_trans(dt).utcoff\n-\n- def dst(self, dt):\n- return self._find_trans(dt).dstoff\n-\n- def tzname(self, dt):\n- return self._find_trans(dt).tzname\n-\n- def fromutc(self, dt):\n- \"\"\"Convert from datetime in UTC to datetime in local time\"\"\"\n-\n- if not isinstance(dt, datetime):\n- raise TypeError(\"fromutc() requires a datetime argument\")\n- if dt.tzinfo is not self:\n- raise ValueError(\"dt.tzinfo is not self\")\n-\n- timestamp = self._get_local_timestamp(dt)\n- num_trans = len(self._trans_utc)\n-\n- if num_trans >= 1 and timestamp < self._trans_utc[0]:\n- tti = self._tti_before\n- fold = 0\n- elif (\n- num_trans == 0 or timestamp > self._trans_utc[-1]\n- ) and not isinstance(self._tz_after, _ttinfo):\n- tti, fold = self._tz_after.get_trans_info_fromutc(\n- timestamp, dt.year\n- )\n- elif num_trans == 0:\n- tti = self._tz_after\n- fold = 0\n- else:\n- idx = bisect.bisect_right(self._trans_utc, timestamp)\n-\n- if num_trans > 1 and timestamp >= self._trans_utc[1]:\n- tti_prev, tti = self._ttinfos[idx - 2 : idx]\n- elif timestamp > self._trans_utc[-1]:\n- tti_prev = self._ttinfos[-1]\n- tti = self._tz_after\n- else:\n- tti_prev = self._tti_before\n- tti = self._ttinfos[0]\n-\n- # Detect fold\n- shift = tti_prev.utcoff - tti.utcoff\n- fold = shift.total_seconds() > timestamp - self._trans_utc[idx - 1]\n- dt += tti.utcoff\n- if fold:\n- return dt.replace(fold=1)\n- else:\n- return dt\n-\n- def _find_trans(self, dt):\n- if dt is None:\n- if self._fixed_offset:\n- return self._tz_after\n- else:\n- return _NO_TTINFO\n-\n- ts = self._get_local_timestamp(dt)\n-\n- lt = self._trans_local[dt.fold]\n-\n- num_trans = len(lt)\n-\n- if num_trans and ts < lt[0]:\n- return self._tti_before\n- elif not num_trans or ts > lt[-1]:\n- if isinstance(self._tz_after, _TZStr):\n- return self._tz_after.get_trans_info(ts, dt.year, dt.fold)\n- else:\n- return self._tz_after\n- else:\n- # idx is the transition that occurs after this timestamp, so we\n- # subtract off 1 to get the current ttinfo\n- idx = bisect.bisect_right(lt, ts) - 1\n- assert idx >= 0\n- return self._ttinfos[idx]\n-\n- def _get_local_timestamp(self, dt):\n- return (\n- (dt.toordinal() - EPOCHORDINAL) * 86400\n- + dt.hour * 3600\n- + dt.minute * 60\n- + dt.second\n- )\n-\n- def __str__(self):\n- if self._key is not None:\n- return f\"{self._key}\"\n- else:\n- return repr(self)\n-\n- def __repr__(self):\n- if self._key is not None:\n- return f\"{self.__class__.__name__}(key={self._key!r})\"\n- else:\n- return f\"{self.__class__.__name__}.from_file({self._file_repr})\"\n-\n- def __reduce__(self):\n- return (self.__class__._unpickle, (self._key, self._from_cache))\n-\n- def _file_reduce(self):\n- import pickle\n-\n- raise pickle.PicklingError(\n- \"Cannot pickle a ZoneInfo file created from a file stream.\"\n- )\n-\n- @classmethod\n- def _unpickle(cls, key, from_cache):\n- if from_cache:\n- return cls(key)\n- else:\n- return cls.no_cache(key)\n-\n- def _find_tzfile(self, key):\n- return _tzpath.find_tzfile(key)\n-\n- def _load_file(self, fobj):\n- # Retrieve all the data as it exists in the zoneinfo file\n- trans_idx, trans_utc, utcoff, isdst, abbr, tz_str = _common.load_data(\n- fobj\n- )\n-\n- # Infer the DST offsets (needed for .dst()) from the data\n- dstoff = self._utcoff_to_dstoff(trans_idx, utcoff, isdst)\n-\n- # Convert all the transition times (UTC) into \"seconds since 1970-01-01 local time\"\n- trans_local = self._ts_to_local(trans_idx, trans_utc, utcoff)\n-\n- # Construct `_ttinfo` objects for each transition in the file\n- _ttinfo_list = [\n- _ttinfo(\n- _load_timedelta(utcoffset), _load_timedelta(dstoffset), tzname\n- )\n- for utcoffset, dstoffset, tzname in zip(utcoff, dstoff, abbr)\n- ]\n-\n- self._trans_utc = trans_utc\n- self._trans_local = trans_local\n- self._ttinfos = [_ttinfo_list[idx] for idx in trans_idx]\n-\n- # Find the first non-DST transition\n- for i in range(len(isdst)):\n- if not isdst[i]:\n- self._tti_before = _ttinfo_list[i]\n- break\n- else:\n- if self._ttinfos:\n- self._tti_before = self._ttinfos[0]\n- else:\n- self._tti_before = None\n-\n- # Set the \"fallback\" time zone\n- if tz_str is not None and tz_str != b\"\":\n- self._tz_after = _parse_tz_str(tz_str.decode())\n- else:\n- if not self._ttinfos and not _ttinfo_list:\n- raise ValueError(\"No time zone information found.\")\n-\n- if self._ttinfos:\n- self._tz_after = self._ttinfos[-1]\n- else:\n- self._tz_after = _ttinfo_list[-1]\n-\n- # Determine if this is a \"fixed offset\" zone, meaning that the output\n- # of the utcoffset, dst and tzname functions does not depend on the\n- # specific datetime passed.\n- #\n- # We make three simplifying assumptions here:\n- #\n- # 1. If _tz_after is not a _ttinfo, it has transitions that might\n- # actually occur (it is possible to construct TZ strings that\n- # specify STD and DST but no transitions ever occur, such as\n- # AAA0BBB,0/0,J365/25).\n- # 2. If _ttinfo_list contains more than one _ttinfo object, the objects\n- # represent different offsets.\n- # 3. _ttinfo_list contains no unused _ttinfos (in which case an\n- # otherwise fixed-offset zone with extra _ttinfos defined may\n- # appear to *not* be a fixed offset zone).\n- #\n- # Violations to these assumptions would be fairly exotic, and exotic\n- # zones should almost certainly not be used with datetime.time (the\n- # only thing that would be affected by this).\n- if len(_ttinfo_list) > 1 or not isinstance(self._tz_after, _ttinfo):\n- self._fixed_offset = False\n- elif not _ttinfo_list:\n- self._fixed_offset = True\n- else:\n- self._fixed_offset = _ttinfo_list[0] == self._tz_after\n-\n- @staticmethod\n- def _utcoff_to_dstoff(trans_idx, utcoffsets, isdsts):\n- # Now we must transform our ttis and abbrs into `_ttinfo` objects,\n- # but there is an issue: .dst() must return a timedelta with the\n- # difference between utcoffset() and the \"standard\" offset, but\n- # the \"base offset\" and \"DST offset\" are not encoded in the file;\n- # we can infer what they are from the isdst flag, but it is not\n- # sufficient to to just look at the last standard offset, because\n- # occasionally countries will shift both DST offset and base offset.\n-\n- typecnt = len(isdsts)\n- dstoffs = [0] * typecnt # Provisionally assign all to 0.\n- dst_cnt = sum(isdsts)\n- dst_found = 0\n-\n- for i in range(1, len(trans_idx)):\n- if dst_cnt == dst_found:\n- break\n-\n- idx = trans_idx[i]\n-\n- dst = isdsts[idx]\n-\n- # We're only going to look at daylight saving time\n- if not dst:\n- continue\n-\n- # Skip any offsets that have already been assigned\n- if dstoffs[idx] != 0:\n- continue\n-\n- dstoff = 0\n- utcoff = utcoffsets[idx]\n-\n- comp_idx = trans_idx[i - 1]\n-\n- if not isdsts[comp_idx]:\n- dstoff = utcoff - utcoffsets[comp_idx]\n-\n- if not dstoff and idx < (typecnt - 1):\n- comp_idx = trans_idx[i + 1]\n-\n- # If the following transition is also DST and we couldn't\n- # find the DST offset by this point, we're going ot have to\n- # skip it and hope this transition gets assigned later\n- if isdsts[comp_idx]:\n- continue\n-\n- dstoff = utcoff - utcoffsets[comp_idx]\n-\n- if dstoff:\n- dst_found += 1\n- dstoffs[idx] = dstoff\n- else:\n- # If we didn't find a valid value for a given index, we'll end up\n- # with dstoff = 0 for something where `isdst=1`. This is obviously\n- # wrong - one hour will be a much better guess than 0\n- for idx in range(typecnt):\n- if not dstoffs[idx] and isdsts[idx]:\n- dstoffs[idx] = 3600\n-\n- return dstoffs\n-\n- @staticmethod\n- def _ts_to_local(trans_idx, trans_list_utc, utcoffsets):\n- \"\"\"Generate number of seconds since 1970 *in the local time*.\n-\n- This is necessary to easily find the transition times in local time\"\"\"\n- if not trans_list_utc:\n- return [[], []]\n-\n- # Start with the timestamps and modify in-place\n- trans_list_wall = [list(trans_list_utc), list(trans_list_utc)]\n-\n- if len(utcoffsets) > 1:\n- offset_0 = utcoffsets[0]\n- offset_1 = utcoffsets[trans_idx[0]]\n- if offset_1 > offset_0:\n- offset_1, offset_0 = offset_0, offset_1\n- else:\n- offset_0 = offset_1 = utcoffsets[0]\n-\n- trans_list_wall[0][0] += offset_0\n- trans_list_wall[1][0] += offset_1\n-\n- for i in range(1, len(trans_idx)):\n- offset_0 = utcoffsets[trans_idx[i - 1]]\n- offset_1 = utcoffsets[trans_idx[i]]\n-\n- if offset_1 > offset_0:\n- offset_1, offset_0 = offset_0, offset_1\n-\n- trans_list_wall[0][i] += offset_0\n- trans_list_wall[1][i] += offset_1\n-\n- return trans_list_wall\n-\n-\n-class _ttinfo:\n- __slots__ = [\"utcoff\", \"dstoff\", \"tzname\"]\n-\n- def __init__(self, utcoff, dstoff, tzname):\n- self.utcoff = utcoff\n- self.dstoff = dstoff\n- self.tzname = tzname\n-\n- def __eq__(self, other):\n- return (\n- self.utcoff == other.utcoff\n- and self.dstoff == other.dstoff\n- and self.tzname == other.tzname\n- )\n-\n- def __repr__(self): # pragma: nocover\n- return (\n- f\"{self.__class__.__name__}\"\n- + f\"({self.utcoff}, {self.dstoff}, {self.tzname})\"\n- )\n-\n-\n-_NO_TTINFO = _ttinfo(None, None, None)\n-\n-\n-class _TZStr:\n- __slots__ = (\n- \"std\",\n- \"dst\",\n- \"start\",\n- \"end\",\n- \"get_trans_info\",\n- \"get_trans_info_fromutc\",\n- \"dst_diff\",\n- )\n-\n- def __init__(\n- self, std_abbr, std_offset, dst_abbr, dst_offset, start=None, end=None\n- ):\n- self.dst_diff = dst_offset - std_offset\n- std_offset = _load_timedelta(std_offset)\n- self.std = _ttinfo(\n- utcoff=std_offset, dstoff=_load_timedelta(0), tzname=std_abbr\n- )\n-\n- self.start = start\n- self.end = end\n-\n- dst_offset = _load_timedelta(dst_offset)\n- delta = _load_timedelta(self.dst_diff)\n- self.dst = _ttinfo(utcoff=dst_offset, dstoff=delta, tzname=dst_abbr)\n-\n- # These are assertions because the constructor should only be called\n- # by functions that would fail before passing start or end\n- assert start is not None, \"No transition start specified\"\n- assert end is not None, \"No transition end specified\"\n-\n- self.get_trans_info = self._get_trans_info\n- self.get_trans_info_fromutc = self._get_trans_info_fromutc\n-\n- def transitions(self, year):\n- start = self.start.year_to_epoch(year)\n- end = self.end.year_to_epoch(year)\n- return start, end\n-\n- def _get_trans_info(self, ts, year, fold):\n- \"\"\"Get the information about the current transition - tti\"\"\"\n- start, end = self.transitions(year)\n-\n- # With fold = 0, the period (denominated in local time) with the\n- # smaller offset starts at the end of the gap and ends at the end of\n- # the fold; with fold = 1, it runs from the start of the gap to the\n- # beginning of the fold.\n- #\n- # So in order to determine the DST boundaries we need to know both\n- # the fold and whether DST is positive or negative (rare), and it\n- # turns out that this boils down to fold XOR is_positive.\n- if fold == (self.dst_diff >= 0):\n- end -= self.dst_diff\n- else:\n- start += self.dst_diff\n-\n- if start < end:\n- isdst = start <= ts < end\n- else:\n- isdst = not (end <= ts < start)\n-\n- return self.dst if isdst else self.std\n-\n- def _get_trans_info_fromutc(self, ts, year):\n- start, end = self.transitions(year)\n- start -= self.std.utcoff.total_seconds()\n- end -= self.dst.utcoff.total_seconds()\n-\n- if start < end:\n- isdst = start <= ts < end\n- else:\n- isdst = not (end <= ts < start)\n-\n- # For positive DST, the ambiguous period is one dst_diff after the end\n- # of DST; for negative DST, the ambiguous period is one dst_diff before\n- # the start of DST.\n- if self.dst_diff > 0:\n- ambig_start = end\n- ambig_end = end + self.dst_diff\n- else:\n- ambig_start = start\n- ambig_end = start - self.dst_diff\n-\n- fold = ambig_start <= ts < ambig_end\n-\n- return (self.dst if isdst else self.std, fold)\n-\n-\n-def _post_epoch_days_before_year(year):\n- \"\"\"Get the number of days between 1970-01-01 and YEAR-01-01\"\"\"\n- y = year - 1\n- return y * 365 + y // 4 - y // 100 + y // 400 - EPOCHORDINAL\n-\n-\n-class _DayOffset:\n- __slots__ = [\"d\", \"julian\", \"hour\", \"minute\", \"second\"]\n-\n- def __init__(self, d, julian, hour=2, minute=0, second=0):\n- if not (0 + julian) <= d <= 365:\n- min_day = 0 + julian\n- raise ValueError(f\"d must be in [{min_day}, 365], not: {d}\")\n-\n- self.d = d\n- self.julian = julian\n- self.hour = hour\n- self.minute = minute\n- self.second = second\n-\n- def year_to_epoch(self, year):\n- days_before_year = _post_epoch_days_before_year(year)\n-\n- d = self.d\n- if self.julian and d >= 59 and calendar.isleap(year):\n- d += 1\n-\n- epoch = (days_before_year + d) * 86400\n- epoch += self.hour * 3600 + self.minute * 60 + self.second\n-\n- return epoch\n-\n-\n-class _CalendarOffset:\n- __slots__ = [\"m\", \"w\", \"d\", \"hour\", \"minute\", \"second\"]\n-\n- _DAYS_BEFORE_MONTH = (\n- -1,\n- 0,\n- 31,\n- 59,\n- 90,\n- 120,\n- 151,\n- 181,\n- 212,\n- 243,\n- 273,\n- 304,\n- 334,\n- )\n-\n- def __init__(self, m, w, d, hour=2, minute=0, second=0):\n- if not 0 < m <= 12:\n- raise ValueError(\"m must be in (0, 12]\")\n-\n- if not 0 < w <= 5:\n- raise ValueError(\"w must be in (0, 5]\")\n-\n- if not 0 <= d <= 6:\n- raise ValueError(\"d must be in [0, 6]\")\n-\n- self.m = m\n- self.w = w\n- self.d = d\n- self.hour = hour\n- self.minute = minute\n- self.second = second\n-\n- @classmethod\n- def _ymd2ord(cls, year, month, day):\n- return (\n- _post_epoch_days_before_year(year)\n- + cls._DAYS_BEFORE_MONTH[month]\n- + (month > 2 and calendar.isleap(year))\n- + day\n- )\n-\n- # TODO: These are not actually epoch dates as they are expressed in local time\n- def year_to_epoch(self, year):\n- \"\"\"Calculates the datetime of the occurrence from the year\"\"\"\n- # We know year and month, we need to convert w, d into day of month\n- #\n- # Week 1 is the first week in which day `d` (where 0 = Sunday) appears.\n- # Week 5 represents the last occurrence of day `d`, so we need to know\n- # the range of the month.\n- first_day, days_in_month = calendar.monthrange(year, self.m)\n-\n- # This equation seems magical, so I'll break it down:\n- # 1. calendar says 0 = Monday, POSIX says 0 = Sunday\n- # so we need first_day + 1 to get 1 = Monday -> 7 = Sunday,\n- # which is still equivalent because this math is mod 7\n- # 2. Get first day - desired day mod 7: -1 % 7 = 6, so we don't need\n- # to do anything to adjust negative numbers.\n- # 3. Add 1 because month days are a 1-based index.\n- month_day = (self.d - (first_day + 1)) % 7 + 1\n-\n- # Now use a 0-based index version of `w` to calculate the w-th\n- # occurrence of `d`\n- month_day += (self.w - 1) * 7\n-\n- # month_day will only be > days_in_month if w was 5, and `w` means\n- # \"last occurrence of `d`\", so now we just check if we over-shot the\n- # end of the month and if so knock off 1 week.\n- if month_day > days_in_month:\n- month_day -= 7\n-\n- ordinal = self._ymd2ord(year, self.m, month_day)\n- epoch = ordinal * 86400\n- epoch += self.hour * 3600 + self.minute * 60 + self.second\n- return epoch\n-\n-\n-def _parse_tz_str(tz_str):\n- # The tz string has the format:\n- #\n- # std[offset[dst[offset],start[/time],end[/time]]]\n- #\n- # std and dst must be 3 or more characters long and must not contain\n- # a leading colon, embedded digits, commas, nor a plus or minus signs;\n- # The spaces between \"std\" and \"offset\" are only for display and are\n- # not actually present in the string.\n- #\n- # The format of the offset is ``[+|-]hh[:mm[:ss]]``\n-\n- offset_str, *start_end_str = tz_str.split(\",\", 1)\n-\n- # fmt: off\n- parser_re = re.compile(\n- r\"(?P<std>[^<0-9:.+-]+|<[a-zA-Z0-9+\\-]+>)\" +\n- r\"((?P<stdoff>[+-]?\\d{1,2}(:\\d{2}(:\\d{2})?)?)\" +\n- r\"((?P<dst>[^0-9:.+-]+|<[a-zA-Z0-9+\\-]+>)\" +\n- r\"((?P<dstoff>[+-]?\\d{1,2}(:\\d{2}(:\\d{2})?)?))?\" +\n- r\")?\" + # dst\n- r\")?$\" # stdoff\n- )\n- # fmt: on\n-\n- m = parser_re.match(offset_str)\n-\n- if m is None:\n- raise ValueError(f\"{tz_str} is not a valid TZ string\")\n-\n- std_abbr = m.group(\"std\")\n- dst_abbr = m.group(\"dst\")\n- dst_offset = None\n-\n- std_abbr = std_abbr.strip(\"<>\")\n-\n- if dst_abbr:\n- dst_abbr = dst_abbr.strip(\"<>\")\n-\n- std_offset = m.group(\"stdoff\")\n- if std_offset:\n- try:\n- std_offset = _parse_tz_delta(std_offset)\n- except ValueError as e:\n- raise ValueError(f\"Invalid STD offset in {tz_str}\") from e\n- else:\n- std_offset = 0\n-\n- if dst_abbr is not None:\n- dst_offset = m.group(\"dstoff\")\n- if dst_offset:\n- try:\n- dst_offset = _parse_tz_delta(dst_offset)\n- except ValueError as e:\n- raise ValueError(f\"Invalid DST offset in {tz_str}\") from e\n- else:\n- dst_offset = std_offset + 3600\n-\n- if not start_end_str:\n- raise ValueError(f\"Missing transition rules: {tz_str}\")\n-\n- start_end_strs = start_end_str[0].split(\",\", 1)\n- try:\n- start, end = (_parse_dst_start_end(x) for x in start_end_strs)\n- except ValueError as e:\n- raise ValueError(f\"Invalid TZ string: {tz_str}\") from e\n-\n- return _TZStr(std_abbr, std_offset, dst_abbr, dst_offset, start, end)\n- elif start_end_str:\n- raise ValueError(f\"Transition rule present without DST: {tz_str}\")\n- else:\n- # This is a static ttinfo, don't return _TZStr\n- return _ttinfo(\n- _load_timedelta(std_offset), _load_timedelta(0), std_abbr\n- )\n-\n-\n-def _parse_dst_start_end(dststr):\n- date, *time = dststr.split(\"/\")\n- if date[0] == \"M\":\n- n_is_julian = False\n- m = re.match(r\"M(\\d{1,2})\\.(\\d).(\\d)$\", date)\n- if m is None:\n- raise ValueError(f\"Invalid dst start/end date: {dststr}\")\n- date_offset = tuple(map(int, m.groups()))\n- offset = _CalendarOffset(*date_offset)\n- else:\n- if date[0] == \"J\":\n- n_is_julian = True\n- date = date[1:]\n- else:\n- n_is_julian = False\n-\n- doy = int(date)\n- offset = _DayOffset(doy, n_is_julian)\n-\n- if time:\n- time_components = list(map(int, time[0].split(\":\")))\n- n_components = len(time_components)\n- if n_components < 3:\n- time_components.extend([0] * (3 - n_components))\n- offset.hour, offset.minute, offset.second = time_components\n-\n- return offset\n-\n-\n-def _parse_tz_delta(tz_delta):\n- match = re.match(\n- r\"(?P<sign>[+-])?(?P<h>\\d{1,2})(:(?P<m>\\d{2})(:(?P<s>\\d{2}))?)?\",\n- tz_delta,\n- )\n- # Anything passed to this function should already have hit an equivalent\n- # regular expression to find the section to parse.\n- assert match is not None, tz_delta\n-\n- h, m, s = (\n- int(v) if v is not None else 0\n- for v in map(match.group, (\"h\", \"m\", \"s\"))\n- )\n-\n- total = h * 3600 + m * 60 + s\n-\n- if not -86400 < total < 86400:\n- raise ValueError(\n- \"Offset must be strictly between -24h and +24h:\" + tz_delta\n- )\n-\n- # Yes, +5 maps to an offset of -5h\n- if match.group(\"sign\") != \"-\":\n- total *= -1\n-\n- return total"}, {"sha": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", "filename": "lib/backports/zoneinfo/py.typed", "status": "removed", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fbackports%2Fzoneinfo%2Fpy.typed", "raw_url": "https://github.com/Tautulli/Tautulli/raw/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fbackports%2Fzoneinfo%2Fpy.typed", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fbackports%2Fzoneinfo%2Fpy.typed?ref=940c2ae6cd064817512aa7646386a31fa9f465fa"}, {"sha": "942987d95e121bfdac91bd99d13ab3b9db64c988", "filename": "lib/bleach/__init__.py", "status": "modified", "additions": 2, "deletions": 2, "changes": 4, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F__init__.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F__init__.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fbleach%2F__init__.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -11,9 +11,9 @@\n \n \n # yyyymmdd\n-__releasedate__ = \"20231006\"\n+__releasedate__ = \"20241029\"\n # x.y.z or x.y.z.dev0 -- semver\n-__version__ = \"6.1.0\"\n+__version__ = \"6.2.0\"\n \n \n __all__ = [\"clean\", \"linkify\"]"}, {"sha": "097625172f51f86c7df3ccde97161eaf973e3f56", "filename": "lib/bleach/_vendor/html5lib/_inputstream.py", "status": "modified", "additions": 2, "deletions": 2, "changes": 4, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fhtml5lib%2F_inputstream.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fhtml5lib%2F_inputstream.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fbleach%2F_vendor%2Fhtml5lib%2F_inputstream.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,7 +1,7 @@\n from __future__ import absolute_import, division, unicode_literals\n \n-from six import text_type\n-from six.moves import http_client, urllib\n+from bleach.six_shim import text_type\n+from bleach.six_shim import http_client, urllib\n \n import codecs\n import re"}, {"sha": "d8848016f65b0989fac2f7ab7934c6e3093eae68", "filename": "lib/bleach/_vendor/html5lib/_tokenizer.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fhtml5lib%2F_tokenizer.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fhtml5lib%2F_tokenizer.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fbleach%2F_vendor%2Fhtml5lib%2F_tokenizer.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,6 +1,6 @@\n from __future__ import absolute_import, division, unicode_literals\n \n-from six import unichr as chr\n+from bleach.six_shim import unichr as chr\n \n from collections import deque, OrderedDict\n from sys import version_info"}, {"sha": "56f66bd5aac1fae4c3e60fd937282971c5dcadb9", "filename": "lib/bleach/_vendor/html5lib/_trie/py.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fhtml5lib%2F_trie%2Fpy.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fhtml5lib%2F_trie%2Fpy.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fbleach%2F_vendor%2Fhtml5lib%2F_trie%2Fpy.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,5 +1,5 @@\n from __future__ import absolute_import, division, unicode_literals\n-from six import text_type\n+from bleach.six_shim import text_type\n \n from bisect import bisect_left\n "}, {"sha": "635bb02419b6060c4c02a97c679a0b26ba707a33", "filename": "lib/bleach/_vendor/html5lib/_utils.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fhtml5lib%2F_utils.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fhtml5lib%2F_utils.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fbleach%2F_vendor%2Fhtml5lib%2F_utils.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -7,7 +7,7 @@\n except ImportError:\n from collections import Mapping\n \n-from six import text_type, PY3\n+from bleach.six_shim import text_type, PY3\n \n if PY3:\n import xml.etree.ElementTree as default_etree"}, {"sha": "1340d9722f531838ac855326448a629a758e3aa4", "filename": "lib/bleach/_vendor/html5lib/filters/lint.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fhtml5lib%2Ffilters%2Flint.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fhtml5lib%2Ffilters%2Flint.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fbleach%2F_vendor%2Fhtml5lib%2Ffilters%2Flint.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,6 +1,6 @@\n from __future__ import absolute_import, division, unicode_literals\n \n-from six import text_type\n+from bleach.six_shim import text_type\n \n from . import base\n from ..constants import namespaces, voidElements"}, {"sha": "5c31e974578eb2986d58d85c2813b2b5cc5b9458", "filename": "lib/bleach/_vendor/html5lib/filters/sanitizer.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fhtml5lib%2Ffilters%2Fsanitizer.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fhtml5lib%2Ffilters%2Fsanitizer.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fbleach%2F_vendor%2Fhtml5lib%2Ffilters%2Fsanitizer.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -12,7 +12,7 @@\n import warnings\n from xml.sax.saxutils import escape, unescape\n \n-from six.moves import urllib_parse as urlparse\n+from bleach.six_shim import urllib_parse as urlparse\n \n from . import base\n from ..constants import namespaces, prefixes"}, {"sha": "5427b7dd0853e0ec435bbca0c1c6ba6d6db6ad15", "filename": "lib/bleach/_vendor/html5lib/html5parser.py", "status": "modified", "additions": 2, "deletions": 2, "changes": 4, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fhtml5lib%2Fhtml5parser.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fhtml5lib%2Fhtml5parser.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fbleach%2F_vendor%2Fhtml5lib%2Fhtml5parser.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,5 +1,5 @@\n from __future__ import absolute_import, division, unicode_literals\n-from six import with_metaclass, viewkeys\n+from bleach.six_shim import viewkeys\n \n import types\n \n@@ -423,7 +423,7 @@ def getMetaclass(use_metaclass, metaclass_func):\n return type\n \n # pylint:disable=unused-argument\n- class Phase(with_metaclass(getMetaclass(debug, log))):\n+ class Phase(metaclass=getMetaclass(debug, log)):\n \"\"\"Base class for helper object that implements each phase of processing\n \"\"\"\n __slots__ = (\"parser\", \"tree\", \"__startTagCache\", \"__endTagCache\")"}, {"sha": "5666f49a6b46acd7e3f3a8eb1a15e5e6d3896895", "filename": "lib/bleach/_vendor/html5lib/serializer.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fhtml5lib%2Fserializer.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fhtml5lib%2Fserializer.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fbleach%2F_vendor%2Fhtml5lib%2Fserializer.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,5 +1,5 @@\n from __future__ import absolute_import, division, unicode_literals\n-from six import text_type\n+from bleach.six_shim import text_type\n \n import re\n "}, {"sha": "2869da007f14aca1baf05bea8b9ddee66a60f01c", "filename": "lib/bleach/_vendor/html5lib/treebuilders/base.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fhtml5lib%2Ftreebuilders%2Fbase.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fhtml5lib%2Ftreebuilders%2Fbase.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fbleach%2F_vendor%2Fhtml5lib%2Ftreebuilders%2Fbase.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,5 +1,5 @@\n from __future__ import absolute_import, division, unicode_literals\n-from six import text_type\n+from bleach.six_shim import text_type\n \n from ..constants import scopingElements, tableInsertModeElements, namespaces\n "}, {"sha": "5ccfc4d6c5f19f89ae23024781c2db83c1792d48", "filename": "lib/bleach/_vendor/html5lib/treebuilders/etree.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fhtml5lib%2Ftreebuilders%2Fetree.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fhtml5lib%2Ftreebuilders%2Fetree.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fbleach%2F_vendor%2Fhtml5lib%2Ftreebuilders%2Fetree.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,7 +1,7 @@\n from __future__ import absolute_import, division, unicode_literals\n # pylint:disable=protected-access\n \n-from six import text_type\n+from bleach.six_shim import text_type\n \n import re\n "}, {"sha": "f4622322c818048e73be254b9ec26e5b498bf306", "filename": "lib/bleach/_vendor/html5lib/treebuilders/etree_lxml.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fhtml5lib%2Ftreebuilders%2Fetree_lxml.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fhtml5lib%2Ftreebuilders%2Fetree_lxml.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fbleach%2F_vendor%2Fhtml5lib%2Ftreebuilders%2Fetree_lxml.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -28,7 +28,7 @@\n from .. import _ihatexml\n \n import lxml.etree as etree\n-from six import PY3, binary_type\n+from bleach.six_shim import PY3, binary_type\n \n \n fullTree = True"}, {"sha": "a9d9450cd5e8515662575914e93a74f91e9b3d0e", "filename": "lib/bleach/_vendor/html5lib/treewalkers/etree.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fhtml5lib%2Ftreewalkers%2Fetree.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fhtml5lib%2Ftreewalkers%2Fetree.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fbleach%2F_vendor%2Fhtml5lib%2Ftreewalkers%2Fetree.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -3,7 +3,7 @@\n from collections import OrderedDict\n import re\n \n-from six import string_types\n+from bleach.six_shim import string_types\n \n from . import base\n from .._utils import moduleFactoryFactory"}, {"sha": "ef42163be8978d673a591ded32a3680c684bba1b", "filename": "lib/bleach/_vendor/html5lib/treewalkers/etree_lxml.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fhtml5lib%2Ftreewalkers%2Fetree_lxml.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fhtml5lib%2Ftreewalkers%2Fetree_lxml.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fbleach%2F_vendor%2Fhtml5lib%2Ftreewalkers%2Fetree_lxml.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,5 +1,5 @@\n from __future__ import absolute_import, division, unicode_literals\n-from six import text_type\n+from bleach.six_shim import text_type\n \n from collections import OrderedDict\n "}, {"sha": "6c896ee452b694e18f5d7b006aaddc75bd6c941d", "filename": "lib/bleach/_vendor/vendor_install.sh", "status": "modified", "additions": 4, "deletions": 0, "changes": 4, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fvendor_install.sh", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2F_vendor%2Fvendor_install.sh", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fbleach%2F_vendor%2Fvendor_install.sh?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -7,8 +7,12 @@ set -o pipefail\n BLEACH_VENDOR_DIR=${BLEACH_VENDOR_DIR:-\".\"}\n DEST=${DEST:-\".\"}\n \n+# Install with no dependencies\n pip install --no-binary all --no-compile --no-deps -r \"${BLEACH_VENDOR_DIR}/vendor.txt\" --target \"${DEST}\"\n \n+# Apply patches\n+(cd \"${DEST}\" && patch -p2 < 01_html5lib_six.patch)\n+\n # install Python 3.6.14 urllib.urlparse for #536\n curl --proto '=https' --tlsv1.2 -o \"${DEST}/parse.py\" https://raw.githubusercontent.com/python/cpython/v3.6.14/Lib/urllib/parse.py\n (cd \"${DEST}\" && sha256sum parse.py > parse.py.SHA256SUM)"}, {"sha": "f083db75a4d0f8fc949d83eeac9efc7301741448", "filename": "lib/bleach/html5lib_shim.py", "status": "modified", "additions": 12, "deletions": 3, "changes": 15, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2Fhtml5lib_shim.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2Fhtml5lib_shim.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fbleach%2Fhtml5lib_shim.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -396,16 +396,25 @@ def __iter__(self):\n # name that abruptly ends, but we should treat that like\n # character data\n yield {\"type\": TAG_TOKEN_TYPE_CHARACTERS, \"data\": self.stream.get_tag()}\n+\n elif last_error_token[\"data\"] in (\n+ \"duplicate-attribute\",\n \"eof-in-attribute-name\",\n \"eof-in-attribute-value-no-quotes\",\n+ \"expected-end-of-tag-but-got-eof\",\n ):\n # Handle the case where the text being parsed ends with <\n- # followed by a series of characters and then space and then\n- # more characters. It's treated as a tag name followed by an\n+ # followed by characters and then space and then:\n+ #\n+ # * more characters\n+ # * more characters repeated with a space between (e.g. \"abc abc\")\n+ # * more characters and then a space and then an EOF (e.g. \"abc def \")\n+ #\n+ # These cases are treated as a tag name followed by an\n # attribute that abruptly ends, but we should treat that like\n- # character data.\n+ # character data instead.\n yield {\"type\": TAG_TOKEN_TYPE_CHARACTERS, \"data\": self.stream.get_tag()}\n+\n else:\n yield last_error_token\n "}, {"sha": "7db96011a43a0b8bd05268aeb2a935a80956304d", "filename": "lib/bleach/six_shim.py", "status": "added", "additions": 19, "deletions": 0, "changes": 19, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2Fsix_shim.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fbleach%2Fsix_shim.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fbleach%2Fsix_shim.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -0,0 +1,19 @@\n+\"\"\"\n+Replacement module for what html5lib uses six for.\n+\"\"\"\n+\n+import http.client\n+import operator\n+import urllib\n+\n+\n+PY3 = True\n+binary_type = bytes\n+string_types = (str,)\n+text_type = str\n+unichr = chr\n+viewkeys = operator.methodcaller(\"keys\")\n+\n+http_client = http.client\n+urllib = urllib\n+urllib_parse = urllib.parse"}, {"sha": "f61d77fa382e836d69b48f93752b9b9e803483e1", "filename": "lib/certifi/__init__.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fcertifi%2F__init__.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fcertifi%2F__init__.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fcertifi%2F__init__.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,4 +1,4 @@\n from .core import contents, where\n \n __all__ = [\"contents\", \"where\"]\n-__version__ = \"2024.07.04\"\n+__version__ = \"2024.08.30\""}, {"sha": "3c165a1b85e5fd8940bddbf2349c24ae2b4f314b", "filename": "lib/certifi/cacert.pem", "status": "modified", "additions": 131, "deletions": 0, "changes": 131, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fcertifi%2Fcacert.pem", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fcertifi%2Fcacert.pem", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fcertifi%2Fcacert.pem?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -4796,3 +4796,134 @@ PQQDAwNoADBlAjAdfKR7w4l1M+E7qUW/Runpod3JIha3RxEL2Jq68cgLcFBTApFw\n hVmpHqTm6iMxoAACMQD94vizrxa5HnPEluPBMBnYfubDl94cT7iJLzPrSA8Z94dG\n XSaQpYXFuXqUPoeovQA=\n -----END CERTIFICATE-----\n+\n+# Issuer: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA\n+# Subject: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA\n+# Label: \"TWCA CYBER Root CA\"\n+# Serial: 85076849864375384482682434040119489222\n+# MD5 Fingerprint: 0b:33:a0:97:52:95:d4:a9:fd:bb:db:6e:a3:55:5b:51\n+# SHA1 Fingerprint: f6:b1:1c:1a:83:38:e9:7b:db:b3:a8:c8:33:24:e0:2d:9c:7f:26:66\n+# SHA256 Fingerprint: 3f:63:bb:28:14:be:17:4e:c8:b6:43:9c:f0:8d:6d:56:f0:b7:c4:05:88:3a:56:48:a3:34:42:4d:6b:3e:c5:58\n+-----BEGIN CERTIFICATE-----\n+MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQ\n+MQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290\n+IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3QgQ0EwHhcNMjIxMTIyMDY1NDI5\n+WhcNNDcxMTIyMTU1OTU5WjBQMQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FO\n+LUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3Qg\n+Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDG+Moe2Qkgfh1sTs6P\n+40czRJzHyWmqOlt47nDSkvgEs1JSHWdyKKHfi12VCv7qze33Kc7wb3+szT3vsxxF\n+avcokPFhV8UMxKNQXd7UtcsZyoC5dc4pztKFIuwCY8xEMCDa6pFbVuYdHNWdZsc/\n+34bKS1PE2Y2yHer43CdTo0fhYcx9tbD47nORxc5zb87uEB8aBs/pJ2DFTxnk684i\n+JkXXYJndzk834H/nY62wuFm40AZoNWDTNq5xQwTxaWV4fPMf88oon1oglWa0zbfu\n+j3ikRRjpJi+NmykosaS3Om251Bw4ckVYsV7r8Cibt4LK/c/WMw+f+5eesRycnupf\n+Xtuq3VTpMCEobY5583WSjCb+3MX2w7DfRFlDo7YDKPYIMKoNM+HvnKkHIuNZW0CP\n+2oi3aQiotyMuRAlZN1vH4xfyIutuOVLF3lSnmMlLIJXcRolftBL5hSmO68gnFSDA\n+S9TMfAxsNAwmmyYxpjyn9tnQS6Jk/zuZQXLB4HCX8SS7K8R0IrGsayIyJNN4KsDA\n+oS/xUgXJP+92ZuJF2A09rZXIx4kmyA+upwMu+8Ff+iDhcK2wZSA3M2Cw1a/XDBzC\n+kHDXShi8fgGwsOsVHkQGzaRP6AzRwyAQ4VRlnrZR0Bp2a0JaWHY06rc3Ga4udfmW\n+5cFZ95RXKSWNOkyrTZpB0F8mAwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYD\n+VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSdhWEUfMFib5do5E83QOGt4A1WNzAd\n+BgNVHQ4EFgQUnYVhFHzBYm+XaORPN0DhreANVjcwDQYJKoZIhvcNAQEMBQADggIB\n+AGSPesRiDrWIzLjHhg6hShbNcAu3p4ULs3a2D6f/CIsLJc+o1IN1KriWiLb73y0t\n+tGlTITVX1olNc79pj3CjYcya2x6a4CD4bLubIp1dhDGaLIrdaqHXKGnK/nZVekZn\n+68xDiBaiA9a5F/gZbG0jAn/xX9AKKSM70aoK7akXJlQKTcKlTfjF/biBzysseKNn\n+TKkHmvPfXvt89YnNdJdhEGoHK4Fa0o635yDRIG4kqIQnoVesqlVYL9zZyvpoBJ7t\n+RCT5dEA7IzOrg1oYJkK2bVS1FmAwbLGg+LhBoF1JSdJlBTrq/p1hvIbZv97Tujqx\n+f36SNI7JAG7cmL3c7IAFrQI932XtCwP39xaEBDG6k5TY8hL4iuO/Qq+n1M0RFxbI\n+Qh0UqEL20kCGoE8jypZFVmAGzbdVAaYBlGX+bgUJurSkquLvWL69J1bY73NxW0Qz\n+8ppy6rBePm6pUlvscG21h483XjyMnM7k8M4MZ0HMzvaAq07MTFb1wWFZk7Q+ptq4\n+NxKfKjLji7gh7MMrZQzvIt6IKTtM1/r+t+FHvpw+PoP7UV31aPcuIYXcv/Fa4nzX\n+xeSDwWrruoBa3lwtcHb4yOWHh8qgnaHlIhInD0Q9HWzq1MKLL295q39QpsQZp6F6\n+t5b5wR9iWqJDB0BeJsas7a5wFsWqynKKTbDPAYsDP27X\n+-----END CERTIFICATE-----\n+\n+# Issuer: CN=SecureSign Root CA12 O=Cybertrust Japan Co., Ltd.\n+# Subject: CN=SecureSign Root CA12 O=Cybertrust Japan Co., Ltd.\n+# Label: \"SecureSign Root CA12\"\n+# Serial: 587887345431707215246142177076162061960426065942\n+# MD5 Fingerprint: c6:89:ca:64:42:9b:62:08:49:0b:1e:7f:e9:07:3d:e8\n+# SHA1 Fingerprint: 7a:22:1e:3d:de:1b:06:ac:9e:c8:47:70:16:8e:3c:e5:f7:6b:06:f4\n+# SHA256 Fingerprint: 3f:03:4b:b5:70:4d:44:b2:d0:85:45:a0:20:57:de:93:eb:f3:90:5f:ce:72:1a:cb:c7:30:c0:6d:da:ee:90:4e\n+-----BEGIN CERTIFICATE-----\n+MIIDcjCCAlqgAwIBAgIUZvnHwa/swlG07VOX5uaCwysckBYwDQYJKoZIhvcNAQEL\n+BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u\n+LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExMjAeFw0yMDA0MDgw\n+NTM2NDZaFw00MDA0MDgwNTM2NDZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD\n+eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS\n+b290IENBMTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6OcE3emhF\n+KxS06+QT61d1I02PJC0W6K6OyX2kVzsqdiUzg2zqMoqUm048luT9Ub+ZyZN+v/mt\n+p7JIKwccJ/VMvHASd6SFVLX9kHrko+RRWAPNEHl57muTH2SOa2SroxPjcf59q5zd\n+J1M3s6oYwlkm7Fsf0uZlfO+TvdhYXAvA42VvPMfKWeP+bl+sg779XSVOKik71gur\n+FzJ4pOE+lEa+Ym6b3kaosRbnhW70CEBFEaCeVESE99g2zvVQR9wsMJvuwPWW0v4J\n+hscGWa5Pro4RmHvzC1KqYiaqId+OJTN5lxZJjfU+1UefNzFJM3IFTQy2VYzxV4+K\n+h9GtxRESOaCtAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD\n+AgEGMB0GA1UdDgQWBBRXNPN0zwRL1SXm8UC2LEzZLemgrTANBgkqhkiG9w0BAQsF\n+AAOCAQEAPrvbFxbS8hQBICw4g0utvsqFepq2m2um4fylOqyttCg6r9cBg0krY6Ld\n+mmQOmFxv3Y67ilQiLUoT865AQ9tPkbeGGuwAtEGBpE/6aouIs3YIcipJQMPTw4WJ\n+mBClnW8Zt7vPemVV2zfrPIpyMpcemik+rY3moxtt9XUa5rBouVui7mlHJzWhhpmA\n+8zNL4WukJsPvdFlseqJkth5Ew1DgDzk9qTPxpfPSvWKErI4cqc1avTc7bgoitPQV\n+55FYxTpE05Uo2cBl6XLK0A+9H7MV2anjpEcJnuDLN/v9vZfVvhgaaaI5gdka9at/\n+yOPiZwud9AzqVN/Ssq+xIvEg37xEHA==\n+-----END CERTIFICATE-----\n+\n+# Issuer: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd.\n+# Subject: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd.\n+# Label: \"SecureSign Root CA14\"\n+# Serial: 575790784512929437950770173562378038616896959179\n+# MD5 Fingerprint: 71:0d:72:fa:92:19:65:5e:89:04:ac:16:33:f0:bc:d5\n+# SHA1 Fingerprint: dd:50:c0:f7:79:b3:64:2e:74:a2:b8:9d:9f:d3:40:dd:bb:f0:f2:4f\n+# SHA256 Fingerprint: 4b:00:9c:10:34:49:4f:9a:b5:6b:ba:3b:a1:d6:27:31:fc:4d:20:d8:95:5a:dc:ec:10:a9:25:60:72:61:e3:38\n+-----BEGIN CERTIFICATE-----\n+MIIFcjCCA1qgAwIBAgIUZNtaDCBO6Ncpd8hQJ6JaJ90t8sswDQYJKoZIhvcNAQEM\n+BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u\n+LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNDAeFw0yMDA0MDgw\n+NzA2MTlaFw00NTA0MDgwNzA2MTlaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD\n+eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS\n+b290IENBMTQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDF0nqh1oq/\n+FjHQmNE6lPxauG4iwWL3pwon71D2LrGeaBLwbCRjOfHw3xDG3rdSINVSW0KZnvOg\n+vlIfX8xnbacuUKLBl422+JX1sLrcneC+y9/3OPJH9aaakpUqYllQC6KxNedlsmGy\n+6pJxaeQp8E+BgQQ8sqVb1MWoWWd7VRxJq3qdwudzTe/NCcLEVxLbAQ4jeQkHO6Lo\n+/IrPj8BGJJw4J+CDnRugv3gVEOuGTgpa/d/aLIJ+7sr2KeH6caH3iGicnPCNvg9J\n+kdjqOvn90Ghx2+m1K06Ckm9mH+Dw3EzsytHqunQG+bOEkJTRX45zGRBdAuVwpcAQ\n+0BB8b8VYSbSwbprafZX1zNoCr7gsfXmPvkPx+SgojQlD+Ajda8iLLCSxjVIHvXib\n+y8posqTdDEx5YMaZ0ZPxMBoH064iwurO8YQJzOAUbn8/ftKChazcqRZOhaBgy/ac\n+18izju3Gm5h1DVXoX+WViwKkrkMpKBGk5hIwAUt1ax5mnXkvpXYvHUC0bcl9eQjs\n+0Wq2XSqypWa9a4X0dFbD9ed1Uigspf9mR6XU/v6eVL9lfgHWMI+lNpyiUBzuOIAB\n+SMbHdPTGrMNASRZhdCyvjG817XsYAFs2PJxQDcqSMxDxJklt33UkN4Ii1+iW/RVL\n+ApY+B3KVfqs9TC7XyvDf4Fg/LS8EmjijAQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD\n+AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUBpOjCl4oaTeqYR3r6/wtbyPk\n+86AwDQYJKoZIhvcNAQEMBQADggIBAJaAcgkGfpzMkwQWu6A6jZJOtxEaCnFxEM0E\n+rX+lRVAQZk5KQaID2RFPeje5S+LGjzJmdSX7684/AykmjbgWHfYfM25I5uj4V7Ib\n+ed87hwriZLoAymzvftAj63iP/2SbNDefNWWipAA9EiOWWF3KY4fGoweITedpdopT\n+zfFP7ELyk+OZpDc8h7hi2/DsHzc/N19DzFGdtfCXwreFamgLRB7lUe6TzktuhsHS\n+DCRZNhqfLJGP4xjblJUK7ZGqDpncllPjYYPGFrojutzdfhrGe0K22VoF3Jpf1d+4\n+2kd92jjbrDnVHmtsKheMYc2xbXIBw8MgAGJoFjHVdqqGuw6qnsb58Nn4DSEC5MUo\n+FlkRudlpcyqSeLiSV5sI8jrlL5WwWLdrIBRtFO8KvH7YVdiI2i/6GaX7i+B/OfVy\n+K4XELKzvGUWSTLNhB9xNH27SgRNcmvMSZ4PPmz+Ln52kuaiWA3rF7iDeM9ovnhp6\n+dB7h7sxaOgTdsxoEqBRjrLdHEoOabPXm6RUVkRqEGQ6UROcSjiVbgGcZ3GOTEAtl\n+Lor6CZpO2oYofaphNdgOpygau1LgePhsumywbrmHXumZNTfxPWQrqaA0k89jL9WB\n+365jJ6UeTo3cKXhZ+PmhIIynJkBugnLNeLLIjzwec+fBH7/PzqUqm9tEZDKgu39c\n+JRNItX+S\n+-----END CERTIFICATE-----\n+\n+# Issuer: CN=SecureSign Root CA15 O=Cybertrust Japan Co., Ltd.\n+# Subject: CN=SecureSign Root CA15 O=Cybertrust Japan Co., Ltd.\n+# Label: \"SecureSign Root CA15\"\n+# Serial: 126083514594751269499665114766174399806381178503\n+# MD5 Fingerprint: 13:30:fc:c4:62:a6:a9:de:b5:c1:68:af:b5:d2:31:47\n+# SHA1 Fingerprint: cb:ba:83:c8:c1:5a:5d:f1:f9:73:6f:ca:d7:ef:28:13:06:4a:07:7d\n+# SHA256 Fingerprint: e7:78:f0:f0:95:fe:84:37:29:cd:1a:00:82:17:9e:53:14:a9:c2:91:44:28:05:e1:fb:1d:8f:b6:b8:88:6c:3a\n+-----BEGIN CERTIFICATE-----\n+MIICIzCCAamgAwIBAgIUFhXHw9hJp75pDIqI7fBw+d23PocwCgYIKoZIzj0EAwMw\n+UTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBM\n+dGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNTAeFw0yMDA0MDgwODMy\n+NTZaFw00NTA0MDgwODMyNTZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpDeWJl\n+cnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBSb290\n+IENBMTUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQLUHSNZDKZmbPSYAi4Io5GdCx4\n+wCtELW1fHcmuS1Iggz24FG1Th2CeX2yF2wYUleDHKP+dX+Sq8bOLbe1PL0vJSpSR\n+ZHX+AezB2Ot6lHhWGENfa4HL9rzatAy2KZMIaY+jQjBAMA8GA1UdEwEB/wQFMAMB\n+Af8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTrQciu/NWeUUj1vYv0hyCTQSvT\n+9DAKBggqhkjOPQQDAwNoADBlAjEA2S6Jfl5OpBEHvVnCB96rMjhTKkZEBhd6zlHp\n+4P9mLQlO4E/0BdGF9jVg3PVys0Z9AjBEmEYagoUeYWmJSwdLZrWeqrqgHkHZAXQ6\n+bkU6iYAZezKYVWOr62Nuk22rGwlgMU4=\n+-----END CERTIFICATE-----"}, {"sha": "e3f2283b9457c74cbe3df050938b7ece2d379404", "filename": "lib/charset_normalizer/api.py", "status": "modified", "additions": 60, "deletions": 18, "changes": 78, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fcharset_normalizer%2Fapi.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fcharset_normalizer%2Fapi.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fcharset_normalizer%2Fapi.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -159,6 +159,8 @@ def from_bytes(\n \n results: CharsetMatches = CharsetMatches()\n \n+ early_stop_results: CharsetMatches = CharsetMatches()\n+\n sig_encoding, sig_payload = identify_sig_or_bom(sequences)\n \n if sig_encoding is not None:\n@@ -221,16 +223,20 @@ def from_bytes(\n try:\n if is_too_large_sequence and is_multi_byte_decoder is False:\n str(\n- sequences[: int(50e4)]\n- if strip_sig_or_bom is False\n- else sequences[len(sig_payload) : int(50e4)],\n+ (\n+ sequences[: int(50e4)]\n+ if strip_sig_or_bom is False\n+ else sequences[len(sig_payload) : int(50e4)]\n+ ),\n encoding=encoding_iana,\n )\n else:\n decoded_payload = str(\n- sequences\n- if strip_sig_or_bom is False\n- else sequences[len(sig_payload) :],\n+ (\n+ sequences\n+ if strip_sig_or_bom is False\n+ else sequences[len(sig_payload) :]\n+ ),\n encoding=encoding_iana,\n )\n except (UnicodeDecodeError, LookupError) as e:\n@@ -367,7 +373,13 @@ def from_bytes(\n and not lazy_str_hard_failure\n ):\n fallback_entry = CharsetMatch(\n- sequences, encoding_iana, threshold, False, [], decoded_payload\n+ sequences,\n+ encoding_iana,\n+ threshold,\n+ False,\n+ [],\n+ decoded_payload,\n+ preemptive_declaration=specified_encoding,\n )\n if encoding_iana == specified_encoding:\n fallback_specified = fallback_entry\n@@ -421,28 +433,58 @@ def from_bytes(\n ),\n )\n \n- results.append(\n- CharsetMatch(\n- sequences,\n- encoding_iana,\n- mean_mess_ratio,\n- bom_or_sig_available,\n- cd_ratios_merged,\n- decoded_payload,\n- )\n+ current_match = CharsetMatch(\n+ sequences,\n+ encoding_iana,\n+ mean_mess_ratio,\n+ bom_or_sig_available,\n+ cd_ratios_merged,\n+ (\n+ decoded_payload\n+ if (\n+ is_too_large_sequence is False\n+ or encoding_iana in [specified_encoding, \"ascii\", \"utf_8\"]\n+ )\n+ else None\n+ ),\n+ preemptive_declaration=specified_encoding,\n )\n \n+ results.append(current_match)\n+\n if (\n encoding_iana in [specified_encoding, \"ascii\", \"utf_8\"]\n and mean_mess_ratio < 0.1\n ):\n+ # If md says nothing to worry about, then... stop immediately!\n+ if mean_mess_ratio == 0.0:\n+ logger.debug(\n+ \"Encoding detection: %s is most likely the one.\",\n+ current_match.encoding,\n+ )\n+ if explain:\n+ logger.removeHandler(explain_handler)\n+ logger.setLevel(previous_logger_level)\n+ return CharsetMatches([current_match])\n+\n+ early_stop_results.append(current_match)\n+\n+ if (\n+ len(early_stop_results)\n+ and (specified_encoding is None or specified_encoding in tested)\n+ and \"ascii\" in tested\n+ and \"utf_8\" in tested\n+ ):\n+ probable_result: CharsetMatch = early_stop_results.best() # type: ignore[assignment]\n logger.debug(\n- \"Encoding detection: %s is most likely the one.\", encoding_iana\n+ \"Encoding detection: %s is most likely the one.\",\n+ probable_result.encoding,\n )\n if explain:\n logger.removeHandler(explain_handler)\n logger.setLevel(previous_logger_level)\n- return CharsetMatches([results[encoding_iana]])\n+\n+ return CharsetMatches([probable_result])\n \n if encoding_iana == sig_encoding:\n logger.debug("}, {"sha": "e7edd0fc84c6cb1ecbf74c96c3c84c8521427755", "filename": "lib/charset_normalizer/cli/__main__.py", "status": "modified", "additions": 30, "deletions": 6, "changes": 36, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fcharset_normalizer%2Fcli%2F__main__.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fcharset_normalizer%2Fcli%2F__main__.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fcharset_normalizer%2Fcli%2F__main__.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -109,6 +109,14 @@ def cli_detect(argv: Optional[List[str]] = None) -> int:\n dest=\"force\",\n help=\"Replace file without asking if you are sure, use this flag with caution.\",\n )\n+ parser.add_argument(\n+ \"-i\",\n+ \"--no-preemptive\",\n+ action=\"store_true\",\n+ default=False,\n+ dest=\"no_preemptive\",\n+ help=\"Disable looking at a charset declaration to hint the detector.\",\n+ )\n parser.add_argument(\n \"-t\",\n \"--threshold\",\n@@ -133,31 +141,47 @@ def cli_detect(argv: Optional[List[str]] = None) -> int:\n args = parser.parse_args(argv)\n \n if args.replace is True and args.normalize is False:\n+ if args.files:\n+ for my_file in args.files:\n+ my_file.close()\n print(\"Use --replace in addition of --normalize only.\", file=sys.stderr)\n return 1\n \n if args.force is True and args.replace is False:\n+ if args.files:\n+ for my_file in args.files:\n+ my_file.close()\n print(\"Use --force in addition of --replace only.\", file=sys.stderr)\n return 1\n \n if args.threshold < 0.0 or args.threshold > 1.0:\n+ if args.files:\n+ for my_file in args.files:\n+ my_file.close()\n print(\"--threshold VALUE should be between 0. AND 1.\", file=sys.stderr)\n return 1\n \n x_ = []\n \n for my_file in args.files:\n- matches = from_fp(my_file, threshold=args.threshold, explain=args.verbose)\n+ matches = from_fp(\n+ my_file,\n+ threshold=args.threshold,\n+ explain=args.verbose,\n+ preemptive_behaviour=args.no_preemptive is False,\n+ )\n \n best_guess = matches.best()\n \n if best_guess is None:\n print(\n 'Unable to identify originating encoding for \"{}\". {}'.format(\n my_file.name,\n- \"Maybe try increasing maximum amount of chaos.\"\n- if args.threshold < 1.0\n- else \"\",\n+ (\n+ \"Maybe try increasing maximum amount of chaos.\"\n+ if args.threshold < 1.0\n+ else \"\"\n+ ),\n ),\n file=sys.stderr,\n )\n@@ -258,8 +282,8 @@ def cli_detect(argv: Optional[List[str]] = None) -> int:\n try:\n x_[0].unicode_path = join(dir_path, \".\".join(o_))\n \n- with open(x_[0].unicode_path, \"w\", encoding=\"utf-8\") as fp:\n- fp.write(str(best_guess))\n+ with open(x_[0].unicode_path, \"wb\") as fp:\n+ fp.write(best_guess.output())\n except IOError as e:\n print(str(e), file=sys.stderr)\n if my_file.closed is False:"}, {"sha": "f8f2a811f765e8e54a9801b27ffed0b3d3df2d61", "filename": "lib/charset_normalizer/constant.py", "status": "modified", "additions": 2, "deletions": 0, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fcharset_normalizer%2Fconstant.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fcharset_normalizer%2Fconstant.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fcharset_normalizer%2Fconstant.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -544,6 +544,8 @@\n \"|\",\n '\"',\n \"-\",\n+ \"(\",\n+ \")\",\n }\n \n "}, {"sha": "3f6d49079769acfe4f52048125b437e96d6ee12e", "filename": "lib/charset_normalizer/legacy.py", "status": "modified", "additions": 13, "deletions": 2, "changes": 15, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fcharset_normalizer%2Flegacy.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fcharset_normalizer%2Flegacy.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fcharset_normalizer%2Flegacy.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,13 +1,24 @@\n-from typing import Any, Dict, Optional, Union\n+from __future__ import annotations\n+\n+from typing import TYPE_CHECKING, Any, Optional\n from warnings import warn\n \n from .api import from_bytes\n from .constant import CHARDET_CORRESPONDENCE\n \n+# TODO: remove this check when dropping Python 3.7 support\n+if TYPE_CHECKING:\n+ from typing_extensions import TypedDict\n+\n+ class ResultDict(TypedDict):\n+ encoding: Optional[str]\n+ language: str\n+ confidence: Optional[float]\n+\n \n def detect(\n byte_str: bytes, should_rename_legacy: bool = False, **kwargs: Any\n-) -> Dict[str, Optional[Union[str, float]]]:\n+) -> ResultDict:\n \"\"\"\n chardet legacy method\n Detect the encoding of the given byte string. It should be mostly backward-compatible."}, {"sha": "d834db0e4d361df1ec838a137d2c500947f08a75", "filename": "lib/charset_normalizer/md.py", "status": "modified", "additions": 16, "deletions": 3, "changes": 19, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fcharset_normalizer%2Fmd.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fcharset_normalizer%2Fmd.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fcharset_normalizer%2Fmd.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -236,7 +236,7 @@ def reset(self) -> None: # pragma: no cover\n \n @property\n def ratio(self) -> float:\n- if self._character_count <= 24:\n+ if self._character_count <= 13:\n return 0.0\n \n ratio_of_suspicious_range_usage: float = (\n@@ -260,6 +260,7 @@ def __init__(self) -> None:\n \n self._buffer: str = \"\"\n self._buffer_accent_count: int = 0\n+ self._buffer_glyph_count: int = 0\n \n def eligible(self, character: str) -> bool:\n return True\n@@ -279,6 +280,14 @@ def feed(self, character: str) -> None:\n and is_thai(character) is False\n ):\n self._foreign_long_watch = True\n+ if (\n+ is_cjk(character)\n+ or is_hangul(character)\n+ or is_katakana(character)\n+ or is_hiragana(character)\n+ or is_thai(character)\n+ ):\n+ self._buffer_glyph_count += 1\n return\n if not self._buffer:\n return\n@@ -291,17 +300,20 @@ def feed(self, character: str) -> None:\n self._character_count += buffer_length\n \n if buffer_length >= 4:\n- if self._buffer_accent_count / buffer_length > 0.34:\n+ if self._buffer_accent_count / buffer_length >= 0.5:\n self._is_current_word_bad = True\n # Word/Buffer ending with an upper case accentuated letter are so rare,\n # that we will consider them all as suspicious. Same weight as foreign_long suspicious.\n- if (\n+ elif (\n is_accentuated(self._buffer[-1])\n and self._buffer[-1].isupper()\n and all(_.isupper() for _ in self._buffer) is False\n ):\n self._foreign_long_count += 1\n self._is_current_word_bad = True\n+ elif self._buffer_glyph_count == 1:\n+ self._is_current_word_bad = True\n+ self._foreign_long_count += 1\n if buffer_length >= 24 and self._foreign_long_watch:\n camel_case_dst = [\n i\n@@ -325,6 +337,7 @@ def feed(self, character: str) -> None:\n self._foreign_long_watch = False\n self._buffer = \"\"\n self._buffer_accent_count = 0\n+ self._buffer_glyph_count = 0\n elif (\n character not in {\"<\", \">\", \"-\", \"=\", \"~\", \"|\", \"_\"}\n and character.isdigit() is False"}, {"sha": "6f6b86b382d28ef95f4db86a5ae7d77961e8c230", "filename": "lib/charset_normalizer/models.py", "status": "modified", "additions": 27, "deletions": 8, "changes": 35, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fcharset_normalizer%2Fmodels.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fcharset_normalizer%2Fmodels.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fcharset_normalizer%2Fmodels.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,9 +1,10 @@\n from encodings.aliases import aliases\n from hashlib import sha256\n from json import dumps\n+from re import sub\n from typing import Any, Dict, Iterator, List, Optional, Tuple, Union\n \n-from .constant import TOO_BIG_SEQUENCE\n+from .constant import RE_POSSIBLE_ENCODING_INDICATION, TOO_BIG_SEQUENCE\n from .utils import iana_name, is_multi_byte_encoding, unicode_range\n \n \n@@ -16,6 +17,7 @@ def __init__(\n has_sig_or_bom: bool,\n languages: \"CoherenceMatches\",\n decoded_payload: Optional[str] = None,\n+ preemptive_declaration: Optional[str] = None,\n ):\n self._payload: bytes = payload\n \n@@ -33,13 +35,13 @@ def __init__(\n \n self._string: Optional[str] = decoded_payload\n \n+ self._preemptive_declaration: Optional[str] = preemptive_declaration\n+\n def __eq__(self, other: object) -> bool:\n if not isinstance(other, CharsetMatch):\n- raise TypeError(\n- \"__eq__ cannot be invoked on {} and {}.\".format(\n- str(other.__class__), str(self.__class__)\n- )\n- )\n+ if isinstance(other, str):\n+ return iana_name(other) == self.encoding\n+ return False\n return self.encoding == other.encoding and self.fingerprint == other.fingerprint\n \n def __lt__(self, other: object) -> bool:\n@@ -210,7 +212,24 @@ def output(self, encoding: str = \"utf_8\") -> bytes:\n \"\"\"\n if self._output_encoding is None or self._output_encoding != encoding:\n self._output_encoding = encoding\n- self._output_payload = str(self).encode(encoding, \"replace\")\n+ decoded_string = str(self)\n+ if (\n+ self._preemptive_declaration is not None\n+ and self._preemptive_declaration.lower()\n+ not in [\"utf-8\", \"utf8\", \"utf_8\"]\n+ ):\n+ patched_header = sub(\n+ RE_POSSIBLE_ENCODING_INDICATION,\n+ lambda m: m.string[m.span()[0] : m.span()[1]].replace(\n+ m.groups()[0], iana_name(self._output_encoding) # type: ignore[arg-type]\n+ ),\n+ decoded_string[:8192],\n+ 1,\n+ )\n+\n+ decoded_string = patched_header + decoded_string[8192:]\n+\n+ self._output_payload = decoded_string.encode(encoding, \"replace\")\n \n return self._output_payload # type: ignore\n \n@@ -266,7 +285,7 @@ def append(self, item: CharsetMatch) -> None:\n )\n )\n # We should disable the submatch factoring when the input file is too heavy (conserve RAM usage)\n- if len(item.raw) <= TOO_BIG_SEQUENCE:\n+ if len(item.raw) < TOO_BIG_SEQUENCE:\n for match in self._results:\n if match.fingerprint == item.fingerprint and match.chaos == item.chaos:\n match.add_submatch(item)"}, {"sha": "699990ee2467c406f176c8ba686e0b4e57dd4641", "filename": "lib/charset_normalizer/version.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fcharset_normalizer%2Fversion.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fcharset_normalizer%2Fversion.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fcharset_normalizer%2Fversion.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -2,5 +2,5 @@\n Expose version\n \"\"\"\n \n-__version__ = \"3.3.2\"\n+__version__ = \"3.4.0\"\n VERSION = __version__.split(\".\")"}, {"sha": "f6760fd0da90f1f000ba45631ee5ae9ef13cf78d", "filename": "lib/dns/_asyncbackend.py", "status": "modified", "additions": 4, "deletions": 3, "changes": 7, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2F_asyncbackend.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2F_asyncbackend.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2F_asyncbackend.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -26,6 +26,10 @@ async def __aexit__(self, exc_type, exc_value, traceback):\n \n \n class Socket: # pragma: no cover\n+ def __init__(self, family: int, type: int):\n+ self.family = family\n+ self.type = type\n+\n async def close(self):\n pass\n \n@@ -46,9 +50,6 @@ async def __aexit__(self, exc_type, exc_value, traceback):\n \n \n class DatagramSocket(Socket): # pragma: no cover\n- def __init__(self, family: int):\n- self.family = family\n-\n async def sendto(self, what, destination, timeout):\n raise NotImplementedError\n "}, {"sha": "6ab168de543892770055eb8412bf3de80edb046c", "filename": "lib/dns/_asyncio_backend.py", "status": "modified", "additions": 4, "deletions": 4, "changes": 8, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2F_asyncio_backend.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2F_asyncio_backend.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2F_asyncio_backend.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -42,7 +42,7 @@ def connection_lost(self, exc):\n if exc is None:\n # EOF we triggered. Is there a better way to do this?\n try:\n- raise EOFError\n+ raise EOFError(\"EOF\")\n except EOFError as e:\n self.recvfrom.set_exception(e)\n else:\n@@ -64,7 +64,7 @@ async def _maybe_wait_for(awaitable, timeout):\n \n class DatagramSocket(dns._asyncbackend.DatagramSocket):\n def __init__(self, family, transport, protocol):\n- super().__init__(family)\n+ super().__init__(family, socket.SOCK_DGRAM)\n self.transport = transport\n self.protocol = protocol\n \n@@ -99,7 +99,7 @@ async def getpeercert(self, timeout):\n \n class StreamSocket(dns._asyncbackend.StreamSocket):\n def __init__(self, af, reader, writer):\n- self.family = af\n+ super().__init__(af, socket.SOCK_STREAM)\n self.reader = reader\n self.writer = writer\n \n@@ -197,7 +197,7 @@ def __init__(\n family=socket.AF_UNSPEC,\n **kwargs,\n ):\n- if resolver is None:\n+ if resolver is None and bootstrap_address is None:\n # pylint: disable=import-outside-toplevel,redefined-outer-name\n import dns.asyncresolver\n "}, {"sha": "fa6d49554d0ac4c70d456223fbcdc450042cbfb0", "filename": "lib/dns/_features.py", "status": "modified", "additions": 6, "deletions": 3, "changes": 9, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2F_features.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2F_features.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2F_features.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -32,6 +32,9 @@ def _version_check(\n package, minimum = requirement.split(\">=\")\n try:\n version = importlib.metadata.version(package)\n+ # This shouldn't happen, but it apparently can.\n+ if version is None:\n+ return False\n except Exception:\n return False\n t_version = _tuple_from_text(version)\n@@ -82,10 +85,10 @@ def force(feature: str, enabled: bool) -> None:\n \n _requirements: Dict[str, List[str]] = {\n ### BEGIN generated requirements\n- \"dnssec\": [\"cryptography>=41\"],\n+ \"dnssec\": [\"cryptography>=43\"],\n \"doh\": [\"httpcore>=1.0.0\", \"httpx>=0.26.0\", \"h2>=4.1.0\"],\n- \"doq\": [\"aioquic>=0.9.25\"],\n- \"idna\": [\"idna>=3.6\"],\n+ \"doq\": [\"aioquic>=1.0.0\"],\n+ \"idna\": [\"idna>=3.7\"],\n \"trio\": [\"trio>=0.23\"],\n \"wmi\": [\"wmi>=1.5.1\"],\n ### END generated requirements"}, {"sha": "0ed904ddcf13a544630c3bd1b8176d50cba32a98", "filename": "lib/dns/_trio_backend.py", "status": "modified", "additions": 10, "deletions": 7, "changes": 17, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2F_trio_backend.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2F_trio_backend.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2F_trio_backend.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -30,13 +30,16 @@ def _maybe_timeout(timeout):\n \n \n class DatagramSocket(dns._asyncbackend.DatagramSocket):\n- def __init__(self, socket):\n- super().__init__(socket.family)\n- self.socket = socket\n+ def __init__(self, sock):\n+ super().__init__(sock.family, socket.SOCK_DGRAM)\n+ self.socket = sock\n \n async def sendto(self, what, destination, timeout):\n with _maybe_timeout(timeout):\n- return await self.socket.sendto(what, destination)\n+ if destination is None:\n+ return await self.socket.send(what)\n+ else:\n+ return await self.socket.sendto(what, destination)\n raise dns.exception.Timeout(\n timeout=timeout\n ) # pragma: no cover lgtm[py/unreachable-statement]\n@@ -61,7 +64,7 @@ async def getpeercert(self, timeout):\n \n class StreamSocket(dns._asyncbackend.StreamSocket):\n def __init__(self, family, stream, tls=False):\n- self.family = family\n+ super().__init__(family, socket.SOCK_STREAM)\n self.stream = stream\n self.tls = tls\n \n@@ -171,7 +174,7 @@ def __init__(\n family=socket.AF_UNSPEC,\n **kwargs,\n ):\n- if resolver is None:\n+ if resolver is None and bootstrap_address is None:\n # pylint: disable=import-outside-toplevel,redefined-outer-name\n import dns.asyncresolver\n \n@@ -205,7 +208,7 @@ async def make_socket(\n try:\n if source:\n await s.bind(_lltuple(source, af))\n- if socktype == socket.SOCK_STREAM:\n+ if socktype == socket.SOCK_STREAM or destination is not None:\n connected = False\n with _maybe_timeout(timeout):\n await s.connect(_lltuple(destination, af))"}, {"sha": "efad0fd7594ad4bdb47b87696931db98a174d4ea", "filename": "lib/dns/asyncquery.py", "status": "modified", "additions": 256, "deletions": 123, "changes": 379, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fasyncquery.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fasyncquery.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fasyncquery.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -19,10 +19,12 @@\n \n import base64\n import contextlib\n+import random\n import socket\n import struct\n import time\n-from typing import Any, Dict, Optional, Tuple, Union\n+import urllib.parse\n+from typing import Any, Dict, Optional, Tuple, Union, cast\n \n import dns.asyncbackend\n import dns.exception\n@@ -37,9 +39,11 @@\n from dns._asyncbackend import NullContext\n from dns.query import (\n BadResponse,\n+ HTTPVersion,\n NoDOH,\n NoDOQ,\n UDPMode,\n+ _check_status,\n _compute_times,\n _make_dot_ssl_context,\n _matches_destination,\n@@ -338,7 +342,7 @@ async def _read_exactly(sock, count, expiration):\n while count > 0:\n n = await sock.recv(count, _timeout(expiration))\n if n == b\"\":\n- raise EOFError\n+ raise EOFError(\"EOF\")\n count = count - len(n)\n s = s + n\n return s\n@@ -500,6 +504,20 @@ async def tls(\n return response\n \n \n+def _maybe_get_resolver(\n+ resolver: Optional[\"dns.asyncresolver.Resolver\"],\n+) -> \"dns.asyncresolver.Resolver\":\n+ # We need a separate method for this to avoid overriding the global\n+ # variable \"dns\" with the as-yet undefined local variable \"dns\"\n+ # in https().\n+ if resolver is None:\n+ # pylint: disable=import-outside-toplevel,redefined-outer-name\n+ import dns.asyncresolver\n+\n+ resolver = dns.asyncresolver.Resolver()\n+ return resolver\n+\n+\n async def https(\n q: dns.message.Message,\n where: str,\n@@ -515,7 +533,8 @@ async def https(\n verify: Union[bool, str] = True,\n bootstrap_address: Optional[str] = None,\n resolver: Optional[\"dns.asyncresolver.Resolver\"] = None,\n- family: Optional[int] = socket.AF_UNSPEC,\n+ family: int = socket.AF_UNSPEC,\n+ http_version: HTTPVersion = HTTPVersion.DEFAULT,\n ) -> dns.message.Message:\n \"\"\"Return the response obtained after sending a query via DNS-over-HTTPS.\n \n@@ -529,26 +548,65 @@ async def https(\n parameters, exceptions, and return type of this method.\n \"\"\"\n \n- if not have_doh:\n- raise NoDOH # pragma: no cover\n- if client and not isinstance(client, httpx.AsyncClient):\n- raise ValueError(\"session parameter must be an httpx.AsyncClient\")\n-\n- wire = q.to_wire()\n try:\n af = dns.inet.af_for_address(where)\n except ValueError:\n af = None\n- transport = None\n- headers = {\"accept\": \"application/dns-message\"}\n if af is not None and dns.inet.is_address(where):\n if af == socket.AF_INET:\n- url = \"https://{}:{}{}\".format(where, port, path)\n+ url = f\"https://{where}:{port}{path}\"\n elif af == socket.AF_INET6:\n- url = \"https://[{}]:{}{}\".format(where, port, path)\n+ url = f\"https://[{where}]:{port}{path}\"\n else:\n url = where\n \n+ extensions = {}\n+ if bootstrap_address is None:\n+ # pylint: disable=possibly-used-before-assignment\n+ parsed = urllib.parse.urlparse(url)\n+ if parsed.hostname is None:\n+ raise ValueError(\"no hostname in URL\")\n+ if dns.inet.is_address(parsed.hostname):\n+ bootstrap_address = parsed.hostname\n+ extensions[\"sni_hostname\"] = parsed.hostname\n+ if parsed.port is not None:\n+ port = parsed.port\n+\n+ if http_version == HTTPVersion.H3 or (\n+ http_version == HTTPVersion.DEFAULT and not have_doh\n+ ):\n+ if bootstrap_address is None:\n+ resolver = _maybe_get_resolver(resolver)\n+ assert parsed.hostname is not None # for mypy\n+ answers = await resolver.resolve_name(parsed.hostname, family)\n+ bootstrap_address = random.choice(list(answers.addresses()))\n+ return await _http3(\n+ q,\n+ bootstrap_address,\n+ url,\n+ timeout,\n+ port,\n+ source,\n+ source_port,\n+ one_rr_per_rrset,\n+ ignore_trailing,\n+ verify=verify,\n+ post=post,\n+ )\n+\n+ if not have_doh:\n+ raise NoDOH # pragma: no cover\n+ # pylint: disable=possibly-used-before-assignment\n+ if client and not isinstance(client, httpx.AsyncClient):\n+ raise ValueError(\"session parameter must be an httpx.AsyncClient\")\n+ # pylint: enable=possibly-used-before-assignment\n+\n+ wire = q.to_wire()\n+ headers = {\"accept\": \"application/dns-message\"}\n+\n+ h1 = http_version in (HTTPVersion.H1, HTTPVersion.DEFAULT)\n+ h2 = http_version in (HTTPVersion.H2, HTTPVersion.DEFAULT)\n+\n backend = dns.asyncbackend.get_default_backend()\n \n if source is None:\n@@ -557,24 +615,23 @@ async def https(\n else:\n local_address = source\n local_port = source_port\n- transport = backend.get_transport_class()(\n- local_address=local_address,\n- http1=True,\n- http2=True,\n- verify=verify,\n- local_port=local_port,\n- bootstrap_address=bootstrap_address,\n- resolver=resolver,\n- family=family,\n- )\n \n if client:\n cm: contextlib.AbstractAsyncContextManager = NullContext(client)\n else:\n- cm = httpx.AsyncClient(\n- http1=True, http2=True, verify=verify, transport=transport\n+ transport = backend.get_transport_class()(\n+ local_address=local_address,\n+ http1=h1,\n+ http2=h2,\n+ verify=verify,\n+ local_port=local_port,\n+ bootstrap_address=bootstrap_address,\n+ resolver=resolver,\n+ family=family,\n )\n \n+ cm = httpx.AsyncClient(http1=h1, http2=h2, verify=verify, transport=transport)\n+\n async with cm as the_client:\n # see https://tools.ietf.org/html/rfc8484#section-4.1.1 for DoH\n # GET and POST examples\n@@ -586,23 +643,33 @@ async def https(\n }\n )\n response = await backend.wait_for(\n- the_client.post(url, headers=headers, content=wire), timeout\n+ the_client.post(\n+ url,\n+ headers=headers,\n+ content=wire,\n+ extensions=extensions,\n+ ),\n+ timeout,\n )\n else:\n wire = base64.urlsafe_b64encode(wire).rstrip(b\"=\")\n twire = wire.decode() # httpx does a repr() if we give it bytes\n response = await backend.wait_for(\n- the_client.get(url, headers=headers, params={\"dns\": twire}), timeout\n+ the_client.get(\n+ url,\n+ headers=headers,\n+ params={\"dns\": twire},\n+ extensions=extensions,\n+ ),\n+ timeout,\n )\n \n # see https://tools.ietf.org/html/rfc8484#section-4.2.1 for info about DoH\n # status codes\n if response.status_code < 200 or response.status_code > 299:\n raise ValueError(\n- \"{} responded with status code {}\"\n- \"\\nResponse body: {!r}\".format(\n- where, response.status_code, response.content\n- )\n+ f\"{where} responded with status code {response.status_code}\"\n+ f\"\\nResponse body: {response.content!r}\"\n )\n r = dns.message.from_wire(\n response.content,\n@@ -617,105 +684,57 @@ async def https(\n return r\n \n \n-async def inbound_xfr(\n+async def _http3(\n+ q: dns.message.Message,\n where: str,\n- txn_manager: dns.transaction.TransactionManager,\n- query: Optional[dns.message.Message] = None,\n- port: int = 53,\n+ url: str,\n timeout: Optional[float] = None,\n- lifetime: Optional[float] = None,\n+ port: int = 853,\n source: Optional[str] = None,\n source_port: int = 0,\n- udp_mode: UDPMode = UDPMode.NEVER,\n+ one_rr_per_rrset: bool = False,\n+ ignore_trailing: bool = False,\n+ verify: Union[bool, str] = True,\n backend: Optional[dns.asyncbackend.Backend] = None,\n-) -> None:\n- \"\"\"Conduct an inbound transfer and apply it via a transaction from the\n- txn_manager.\n+ hostname: Optional[str] = None,\n+ post: bool = True,\n+) -> dns.message.Message:\n+ if not dns.quic.have_quic:\n+ raise NoDOH(\"DNS-over-HTTP3 is not available.\") # pragma: no cover\n \n- *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``,\n- the default, then dnspython will use the default backend.\n+ url_parts = urllib.parse.urlparse(url)\n+ hostname = url_parts.hostname\n+ if url_parts.port is not None:\n+ port = url_parts.port\n \n- See :py:func:`dns.query.inbound_xfr()` for the documentation of\n- the other parameters, exceptions, and return type of this method.\n- \"\"\"\n- if query is None:\n- (query, serial) = dns.xfr.make_query(txn_manager)\n- else:\n- serial = dns.xfr.extract_serial_from_query(query)\n- rdtype = query.question[0].rdtype\n- is_ixfr = rdtype == dns.rdatatype.IXFR\n- origin = txn_manager.from_wire_origin()\n- wire = query.to_wire()\n- af = dns.inet.af_for_address(where)\n- stuple = _source_tuple(af, source, source_port)\n- dtuple = (where, port)\n- (_, expiration) = _compute_times(lifetime)\n- retry = True\n- while retry:\n- retry = False\n- if is_ixfr and udp_mode != UDPMode.NEVER:\n- sock_type = socket.SOCK_DGRAM\n- is_udp = True\n- else:\n- sock_type = socket.SOCK_STREAM\n- is_udp = False\n- if not backend:\n- backend = dns.asyncbackend.get_default_backend()\n- s = await backend.make_socket(\n- af, sock_type, 0, stuple, dtuple, _timeout(expiration)\n+ q.id = 0\n+ wire = q.to_wire()\n+ (cfactory, mfactory) = dns.quic.factories_for_backend(backend)\n+\n+ async with cfactory() as context:\n+ async with mfactory(\n+ context, verify_mode=verify, server_name=hostname, h3=True\n+ ) as the_manager:\n+ the_connection = the_manager.connect(where, port, source, source_port)\n+ (start, expiration) = _compute_times(timeout)\n+ stream = await the_connection.make_stream(timeout)\n+ async with stream:\n+ # note that send_h3() does not need await\n+ stream.send_h3(url, wire, post)\n+ wire = await stream.receive(_remaining(expiration))\n+ _check_status(stream.headers(), where, wire)\n+ finish = time.time()\n+ r = dns.message.from_wire(\n+ wire,\n+ keyring=q.keyring,\n+ request_mac=q.request_mac,\n+ one_rr_per_rrset=one_rr_per_rrset,\n+ ignore_trailing=ignore_trailing,\n )\n- async with s:\n- if is_udp:\n- await s.sendto(wire, dtuple, _timeout(expiration))\n- else:\n- tcpmsg = struct.pack(\"!H\", len(wire)) + wire\n- await s.sendall(tcpmsg, expiration)\n- with dns.xfr.Inbound(txn_manager, rdtype, serial, is_udp) as inbound:\n- done = False\n- tsig_ctx = None\n- while not done:\n- (_, mexpiration) = _compute_times(timeout)\n- if mexpiration is None or (\n- expiration is not None and mexpiration > expiration\n- ):\n- mexpiration = expiration\n- if is_udp:\n- destination = _lltuple((where, port), af)\n- while True:\n- timeout = _timeout(mexpiration)\n- (rwire, from_address) = await s.recvfrom(65535, timeout)\n- if _matches_destination(\n- af, from_address, destination, True\n- ):\n- break\n- else:\n- ldata = await _read_exactly(s, 2, mexpiration)\n- (l,) = struct.unpack(\"!H\", ldata)\n- rwire = await _read_exactly(s, l, mexpiration)\n- is_ixfr = rdtype == dns.rdatatype.IXFR\n- r = dns.message.from_wire(\n- rwire,\n- keyring=query.keyring,\n- request_mac=query.mac,\n- xfr=True,\n- origin=origin,\n- tsig_ctx=tsig_ctx,\n- multi=(not is_udp),\n- one_rr_per_rrset=is_ixfr,\n- )\n- try:\n- done = inbound.process_message(r)\n- except dns.xfr.UseTCP:\n- assert is_udp # should not happen if we used TCP!\n- if udp_mode == UDPMode.ONLY:\n- raise\n- done = True\n- retry = True\n- udp_mode = UDPMode.NEVER\n- continue\n- tsig_ctx = r.tsig_ctx\n- if not retry and query.keyring and not r.had_tsig:\n- raise dns.exception.FormError(\"missing TSIG\")\n+ r.time = max(finish - start, 0.0)\n+ if not q.is_response(r):\n+ raise BadResponse\n+ return r\n \n \n async def quic(\n@@ -730,6 +749,7 @@ async def quic(\n connection: Optional[dns.quic.AsyncQuicConnection] = None,\n verify: Union[bool, str] = True,\n backend: Optional[dns.asyncbackend.Backend] = None,\n+ hostname: Optional[str] = None,\n server_hostname: Optional[str] = None,\n ) -> dns.message.Message:\n \"\"\"Return the response obtained after sending an asynchronous query via\n@@ -745,6 +765,9 @@ async def quic(\n if not dns.quic.have_quic:\n raise NoDOQ(\"DNS-over-QUIC is not available.\") # pragma: no cover\n \n+ if server_hostname is not None and hostname is None:\n+ hostname = server_hostname\n+\n q.id = 0\n wire = q.to_wire()\n the_connection: dns.quic.AsyncQuicConnection\n@@ -757,7 +780,9 @@ async def quic(\n \n async with cfactory() as context:\n async with mfactory(\n- context, verify_mode=verify, server_name=server_hostname\n+ context,\n+ verify_mode=verify,\n+ server_name=server_hostname,\n ) as the_manager:\n if not connection:\n the_connection = the_manager.connect(where, port, source, source_port)\n@@ -778,3 +803,111 @@ async def quic(\n if not q.is_response(r):\n raise BadResponse\n return r\n+\n+\n+async def _inbound_xfr(\n+ txn_manager: dns.transaction.TransactionManager,\n+ s: dns.asyncbackend.Socket,\n+ query: dns.message.Message,\n+ serial: Optional[int],\n+ timeout: Optional[float],\n+ expiration: float,\n+) -> Any:\n+ \"\"\"Given a socket, does the zone transfer.\"\"\"\n+ rdtype = query.question[0].rdtype\n+ is_ixfr = rdtype == dns.rdatatype.IXFR\n+ origin = txn_manager.from_wire_origin()\n+ wire = query.to_wire()\n+ is_udp = s.type == socket.SOCK_DGRAM\n+ if is_udp:\n+ udp_sock = cast(dns.asyncbackend.DatagramSocket, s)\n+ await udp_sock.sendto(wire, None, _timeout(expiration))\n+ else:\n+ tcp_sock = cast(dns.asyncbackend.StreamSocket, s)\n+ tcpmsg = struct.pack(\"!H\", len(wire)) + wire\n+ await tcp_sock.sendall(tcpmsg, expiration)\n+ with dns.xfr.Inbound(txn_manager, rdtype, serial, is_udp) as inbound:\n+ done = False\n+ tsig_ctx = None\n+ while not done:\n+ (_, mexpiration) = _compute_times(timeout)\n+ if mexpiration is None or (\n+ expiration is not None and mexpiration > expiration\n+ ):\n+ mexpiration = expiration\n+ if is_udp:\n+ timeout = _timeout(mexpiration)\n+ (rwire, _) = await udp_sock.recvfrom(65535, timeout)\n+ else:\n+ ldata = await _read_exactly(tcp_sock, 2, mexpiration)\n+ (l,) = struct.unpack(\"!H\", ldata)\n+ rwire = await _read_exactly(tcp_sock, l, mexpiration)\n+ r = dns.message.from_wire(\n+ rwire,\n+ keyring=query.keyring,\n+ request_mac=query.mac,\n+ xfr=True,\n+ origin=origin,\n+ tsig_ctx=tsig_ctx,\n+ multi=(not is_udp),\n+ one_rr_per_rrset=is_ixfr,\n+ )\n+ done = inbound.process_message(r)\n+ yield r\n+ tsig_ctx = r.tsig_ctx\n+ if query.keyring and not r.had_tsig:\n+ raise dns.exception.FormError(\"missing TSIG\")\n+\n+\n+async def inbound_xfr(\n+ where: str,\n+ txn_manager: dns.transaction.TransactionManager,\n+ query: Optional[dns.message.Message] = None,\n+ port: int = 53,\n+ timeout: Optional[float] = None,\n+ lifetime: Optional[float] = None,\n+ source: Optional[str] = None,\n+ source_port: int = 0,\n+ udp_mode: UDPMode = UDPMode.NEVER,\n+ backend: Optional[dns.asyncbackend.Backend] = None,\n+) -> None:\n+ \"\"\"Conduct an inbound transfer and apply it via a transaction from the\n+ txn_manager.\n+\n+ *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``,\n+ the default, then dnspython will use the default backend.\n+\n+ See :py:func:`dns.query.inbound_xfr()` for the documentation of\n+ the other parameters, exceptions, and return type of this method.\n+ \"\"\"\n+ if query is None:\n+ (query, serial) = dns.xfr.make_query(txn_manager)\n+ else:\n+ serial = dns.xfr.extract_serial_from_query(query)\n+ af = dns.inet.af_for_address(where)\n+ stuple = _source_tuple(af, source, source_port)\n+ dtuple = (where, port)\n+ if not backend:\n+ backend = dns.asyncbackend.get_default_backend()\n+ (_, expiration) = _compute_times(lifetime)\n+ if query.question[0].rdtype == dns.rdatatype.IXFR and udp_mode != UDPMode.NEVER:\n+ s = await backend.make_socket(\n+ af, socket.SOCK_DGRAM, 0, stuple, dtuple, _timeout(expiration)\n+ )\n+ async with s:\n+ try:\n+ async for _ in _inbound_xfr(\n+ txn_manager, s, query, serial, timeout, expiration\n+ ):\n+ pass\n+ return\n+ except dns.xfr.UseTCP:\n+ if udp_mode == UDPMode.ONLY:\n+ raise\n+\n+ s = await backend.make_socket(\n+ af, socket.SOCK_STREAM, 0, stuple, dtuple, _timeout(expiration)\n+ )\n+ async with s:\n+ async for _ in _inbound_xfr(txn_manager, s, query, serial, timeout, expiration):\n+ pass"}, {"sha": "b69d0a1262ee28e4325c017e48d5188fb8961bbe", "filename": "lib/dns/dnssec.py", "status": "modified", "additions": 31, "deletions": 7, "changes": 38, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fdnssec.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fdnssec.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fdnssec.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -118,6 +118,7 @@ def key_id(key: Union[DNSKEY, CDNSKEY]) -> int:\n \"\"\"\n \n rdata = key.to_wire()\n+ assert rdata is not None # for mypy\n if key.algorithm == Algorithm.RSAMD5:\n return (rdata[-3] << 8) + rdata[-2]\n else:\n@@ -224,7 +225,7 @@ def make_ds(\n if isinstance(algorithm, str):\n algorithm = DSDigest[algorithm.upper()]\n except Exception:\n- raise UnsupportedAlgorithm('unsupported algorithm \"%s\"' % algorithm)\n+ raise UnsupportedAlgorithm(f'unsupported algorithm \"{algorithm}\"')\n if validating:\n check = policy.ok_to_validate_ds\n else:\n@@ -240,14 +241,15 @@ def make_ds(\n elif algorithm == DSDigest.SHA384:\n dshash = hashlib.sha384()\n else:\n- raise UnsupportedAlgorithm('unsupported algorithm \"%s\"' % algorithm)\n+ raise UnsupportedAlgorithm(f'unsupported algorithm \"{algorithm}\"')\n \n if isinstance(name, str):\n name = dns.name.from_text(name, origin)\n wire = name.canonicalize().to_wire()\n- assert wire is not None\n+ kwire = key.to_wire(origin=origin)\n+ assert wire is not None and kwire is not None # for mypy\n dshash.update(wire)\n- dshash.update(key.to_wire(origin=origin))\n+ dshash.update(kwire)\n digest = dshash.digest()\n \n dsrdata = struct.pack(\"!HBB\", key_id(key), key.algorithm, algorithm) + digest\n@@ -323,6 +325,7 @@ def _get_rrname_rdataset(\n \n \n def _validate_signature(sig: bytes, data: bytes, key: DNSKEY) -> None:\n+ # pylint: disable=possibly-used-before-assignment\n public_cls = get_algorithm_cls_from_dnskey(key).public_cls\n try:\n public_key = public_cls.from_dnskey(key)\n@@ -387,6 +390,7 @@ def _validate_rrsig(\n \n data = _make_rrsig_signature_data(rrset, rrsig, origin)\n \n+ # pylint: disable=possibly-used-before-assignment\n for candidate_key in candidate_keys:\n if not policy.ok_to_validate(candidate_key):\n continue\n@@ -484,6 +488,7 @@ def _sign(\n verify: bool = False,\n policy: Optional[Policy] = None,\n origin: Optional[dns.name.Name] = None,\n+ deterministic: bool = True,\n ) -> RRSIG:\n \"\"\"Sign RRset using private key.\n \n@@ -523,6 +528,10 @@ def _sign(\n names in the rrset (including its owner name) must be absolute; otherwise the\n specified origin will be used to make names absolute when signing.\n \n+ *deterministic*, a ``bool``. If ``True``, the default, use deterministic\n+ (reproducible) signatures when supported by the algorithm used for signing.\n+ Currently, this only affects ECDSA.\n+\n Raises ``DeniedByPolicy`` if the signature is denied by policy.\n \"\"\"\n \n@@ -580,6 +589,7 @@ def _sign(\n \n data = dns.dnssec._make_rrsig_signature_data(rrset, rrsig_template, origin)\n \n+ # pylint: disable=possibly-used-before-assignment\n if isinstance(private_key, GenericPrivateKey):\n signing_key = private_key\n else:\n@@ -589,7 +599,7 @@ def _sign(\n except UnsupportedAlgorithm:\n raise TypeError(\"Unsupported key algorithm\")\n \n- signature = signing_key.sign(data, verify)\n+ signature = signing_key.sign(data, verify, deterministic)\n \n return cast(RRSIG, rrsig_template.replace(signature=signature))\n \n@@ -629,7 +639,9 @@ def _make_rrsig_signature_data(\n rrname, rdataset = _get_rrname_rdataset(rrset)\n \n data = b\"\"\n- data += rrsig.to_wire(origin=signer)[:18]\n+ wire = rrsig.to_wire(origin=signer)\n+ assert wire is not None # for mypy\n+ data += wire[:18]\n data += rrsig.signer.to_digestable(signer)\n \n # Derelativize the name before considering labels.\n@@ -686,6 +698,7 @@ def _make_dnskey(\n \n algorithm = Algorithm.make(algorithm)\n \n+ # pylint: disable=possibly-used-before-assignment\n if isinstance(public_key, GenericPublicKey):\n return public_key.to_dnskey(flags=flags, protocol=protocol)\n else:\n@@ -832,7 +845,7 @@ def make_ds_rdataset(\n if isinstance(algorithm, str):\n algorithm = DSDigest[algorithm.upper()]\n except Exception:\n- raise UnsupportedAlgorithm('unsupported algorithm \"%s\"' % algorithm)\n+ raise UnsupportedAlgorithm(f'unsupported algorithm \"{algorithm}\"')\n _algorithms.add(algorithm)\n \n if rdataset.rdtype == dns.rdatatype.CDS:\n@@ -950,6 +963,7 @@ def default_rrset_signer(\n lifetime: Optional[int] = None,\n policy: Optional[Policy] = None,\n origin: Optional[dns.name.Name] = None,\n+ deterministic: bool = True,\n ) -> None:\n \"\"\"Default RRset signer\"\"\"\n \n@@ -975,6 +989,7 @@ def default_rrset_signer(\n signer=signer,\n policy=policy,\n origin=origin,\n+ deterministic=deterministic,\n )\n txn.add(rrset.name, rrset.ttl, rrsig)\n \n@@ -991,6 +1006,7 @@ def sign_zone(\n nsec3: Optional[NSEC3PARAM] = None,\n rrset_signer: Optional[RRsetSigner] = None,\n policy: Optional[Policy] = None,\n+ deterministic: bool = True,\n ) -> None:\n \"\"\"Sign zone.\n \n@@ -1030,6 +1046,10 @@ def sign_zone(\n function requires two arguments: transaction and RRset. If the not specified,\n ``dns.dnssec.default_rrset_signer`` will be used.\n \n+ *deterministic*, a ``bool``. If ``True``, the default, use deterministic\n+ (reproducible) signatures when supported by the algorithm used for signing.\n+ Currently, this only affects ECDSA.\n+\n Returns ``None``.\n \"\"\"\n \n@@ -1056,6 +1076,9 @@ def sign_zone(\n else:\n cm = zone.writer()\n \n+ if zone.origin is None:\n+ raise ValueError(\"no zone origin\")\n+\n with cm as _txn:\n if add_dnskey:\n if dnskey_ttl is None:\n@@ -1081,6 +1104,7 @@ def sign_zone(\n lifetime=lifetime,\n policy=policy,\n origin=zone.origin,\n+ deterministic=deterministic,\n )\n return _sign_zone_nsec(zone, _txn, _rrset_signer)\n "}, {"sha": "602367e3d200f308aa4ef5a4a480973af5713041", "filename": "lib/dns/dnssecalgs/__init__.py", "status": "modified", "additions": 2, "deletions": 1, "changes": 3, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fdnssecalgs%2F__init__.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fdnssecalgs%2F__init__.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fdnssecalgs%2F__init__.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -26,6 +26,7 @@\n \n algorithms: Dict[Tuple[Algorithm, AlgorithmPrefix], Type[GenericPrivateKey]] = {}\n if _have_cryptography:\n+ # pylint: disable=possibly-used-before-assignment\n algorithms.update(\n {\n (Algorithm.RSAMD5, None): PrivateRSAMD5,\n@@ -59,7 +60,7 @@ def get_algorithm_cls(\n if cls:\n return cls\n raise UnsupportedAlgorithm(\n- 'algorithm \"%s\" not supported by dnspython' % Algorithm.to_text(algorithm)\n+ f'algorithm \"{Algorithm.to_text(algorithm)}\" not supported by dnspython'\n )\n \n "}, {"sha": "752ee48069b4c2d366369d620eda8cff1f37160e", "filename": "lib/dns/dnssecalgs/base.py", "status": "modified", "additions": 6, "deletions": 1, "changes": 7, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fdnssecalgs%2Fbase.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fdnssecalgs%2Fbase.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fdnssecalgs%2Fbase.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -65,7 +65,12 @@ def __init__(self, key: Any) -> None:\n pass\n \n @abstractmethod\n- def sign(self, data: bytes, verify: bool = False) -> bytes:\n+ def sign(\n+ self,\n+ data: bytes,\n+ verify: bool = False,\n+ deterministic: bool = True,\n+ ) -> bytes:\n \"\"\"Sign DNSSEC data\"\"\"\n \n @abstractmethod"}, {"sha": "adca3def0151945093ed53a96d9c63d814d80534", "filename": "lib/dns/dnssecalgs/dsa.py", "status": "modified", "additions": 6, "deletions": 1, "changes": 7, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fdnssecalgs%2Fdsa.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fdnssecalgs%2Fdsa.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fdnssecalgs%2Fdsa.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -68,7 +68,12 @@ class PrivateDSA(CryptographyPrivateKey):\n key_cls = dsa.DSAPrivateKey\n public_cls = PublicDSA\n \n- def sign(self, data: bytes, verify: bool = False) -> bytes:\n+ def sign(\n+ self,\n+ data: bytes,\n+ verify: bool = False,\n+ deterministic: bool = True,\n+ ) -> bytes:\n \"\"\"Sign using a private key per RFC 2536, section 3.\"\"\"\n public_dsa_key = self.key.public_key()\n if public_dsa_key.key_size > 1024:"}, {"sha": "86d5764c919e616ad412e7e718472c7c01f4cf2a", "filename": "lib/dns/dnssecalgs/ecdsa.py", "status": "modified", "additions": 10, "deletions": 2, "changes": 12, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fdnssecalgs%2Fecdsa.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fdnssecalgs%2Fecdsa.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fdnssecalgs%2Fecdsa.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -47,9 +47,17 @@ class PrivateECDSA(CryptographyPrivateKey):\n key_cls = ec.EllipticCurvePrivateKey\n public_cls = PublicECDSA\n \n- def sign(self, data: bytes, verify: bool = False) -> bytes:\n+ def sign(\n+ self,\n+ data: bytes,\n+ verify: bool = False,\n+ deterministic: bool = True,\n+ ) -> bytes:\n \"\"\"Sign using a private key per RFC 6605, section 4.\"\"\"\n- der_signature = self.key.sign(data, ec.ECDSA(self.public_cls.chosen_hash))\n+ algorithm = ec.ECDSA(\n+ self.public_cls.chosen_hash, deterministic_signing=deterministic\n+ )\n+ der_signature = self.key.sign(data, algorithm)\n dsa_r, dsa_s = utils.decode_dss_signature(der_signature)\n signature = int.to_bytes(\n dsa_r, length=self.public_cls.octets, byteorder=\"big\""}, {"sha": "604bcbfeaf7938c781dbc1076c5758b03ab7b9ca", "filename": "lib/dns/dnssecalgs/eddsa.py", "status": "modified", "additions": 6, "deletions": 1, "changes": 7, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fdnssecalgs%2Feddsa.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fdnssecalgs%2Feddsa.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fdnssecalgs%2Feddsa.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -29,7 +29,12 @@ def from_dnskey(cls, key: DNSKEY) -> \"PublicEDDSA\":\n class PrivateEDDSA(CryptographyPrivateKey):\n public_cls: Type[PublicEDDSA]\n \n- def sign(self, data: bytes, verify: bool = False) -> bytes:\n+ def sign(\n+ self,\n+ data: bytes,\n+ verify: bool = False,\n+ deterministic: bool = True,\n+ ) -> bytes:\n \"\"\"Sign using a private key per RFC 8080, section 4.\"\"\"\n signature = self.key.sign(data)\n if verify:"}, {"sha": "27537aad0c1857276d62c476f583a3e093e34e0f", "filename": "lib/dns/dnssecalgs/rsa.py", "status": "modified", "additions": 6, "deletions": 1, "changes": 7, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fdnssecalgs%2Frsa.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fdnssecalgs%2Frsa.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fdnssecalgs%2Frsa.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -56,7 +56,12 @@ class PrivateRSA(CryptographyPrivateKey):\n public_cls = PublicRSA\n default_public_exponent = 65537\n \n- def sign(self, data: bytes, verify: bool = False) -> bytes:\n+ def sign(\n+ self,\n+ data: bytes,\n+ verify: bool = False,\n+ deterministic: bool = True,\n+ ) -> bytes:\n \"\"\"Sign using a private key per RFC 3110, section 3.\"\"\"\n signature = self.key.sign(data, padding.PKCS1v15(), self.public_cls.chosen_hash)\n if verify:"}, {"sha": "f7d9ff996b5ac4e68798c67d2c8c96195d0a5dad", "filename": "lib/dns/edns.py", "status": "modified", "additions": 64, "deletions": 8, "changes": 72, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fedns.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fedns.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fedns.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -52,6 +52,8 @@ class OptionType(dns.enum.IntEnum):\n CHAIN = 13\n #: EDE (extended-dns-error)\n EDE = 15\n+ #: REPORTCHANNEL\n+ REPORTCHANNEL = 18\n \n @classmethod\n def _maximum(cls):\n@@ -222,7 +224,7 @@ def __init__(self, address: str, srclen: Optional[int] = None, scopelen: int = 0\n self.addrdata = self.addrdata[:-1] + last\n \n def to_text(self) -> str:\n- return \"ECS {}/{} scope/{}\".format(self.address, self.srclen, self.scopelen)\n+ return f\"ECS {self.address}/{self.srclen} scope/{self.scopelen}\"\n \n @staticmethod\n def from_text(text: str) -> Option:\n@@ -255,29 +257,27 @@ def from_text(text: str) -> Option:\n ecs_text = tokens[0]\n elif len(tokens) == 2:\n if tokens[0] != optional_prefix:\n- raise ValueError('could not parse ECS from \"{}\"'.format(text))\n+ raise ValueError(f'could not parse ECS from \"{text}\"')\n ecs_text = tokens[1]\n else:\n- raise ValueError('could not parse ECS from \"{}\"'.format(text))\n+ raise ValueError(f'could not parse ECS from \"{text}\"')\n n_slashes = ecs_text.count(\"/\")\n if n_slashes == 1:\n address, tsrclen = ecs_text.split(\"/\")\n tscope = \"0\"\n elif n_slashes == 2:\n address, tsrclen, tscope = ecs_text.split(\"/\")\n else:\n- raise ValueError('could not parse ECS from \"{}\"'.format(text))\n+ raise ValueError(f'could not parse ECS from \"{text}\"')\n try:\n scope = int(tscope)\n except ValueError:\n- raise ValueError(\n- \"invalid scope \" + '\"{}\": scope must be an integer'.format(tscope)\n- )\n+ raise ValueError(\"invalid scope \" + f'\"{tscope}\": scope must be an integer')\n try:\n srclen = int(tsrclen)\n except ValueError:\n raise ValueError(\n- \"invalid srclen \" + '\"{}\": srclen must be an integer'.format(tsrclen)\n+ \"invalid srclen \" + f'\"{tsrclen}\": srclen must be an integer'\n )\n return ECSOption(address, srclen, scope)\n \n@@ -430,10 +430,65 @@ def from_wire_parser(\n return cls(parser.get_remaining())\n \n \n+class CookieOption(Option):\n+ def __init__(self, client: bytes, server: bytes):\n+ super().__init__(dns.edns.OptionType.COOKIE)\n+ self.client = client\n+ self.server = server\n+ if len(client) != 8:\n+ raise ValueError(\"client cookie must be 8 bytes\")\n+ if len(server) != 0 and (len(server) < 8 or len(server) > 32):\n+ raise ValueError(\"server cookie must be empty or between 8 and 32 bytes\")\n+\n+ def to_wire(self, file: Any = None) -> Optional[bytes]:\n+ if file:\n+ file.write(self.client)\n+ if len(self.server) > 0:\n+ file.write(self.server)\n+ return None\n+ else:\n+ return self.client + self.server\n+\n+ def to_text(self) -> str:\n+ client = binascii.hexlify(self.client).decode()\n+ if len(self.server) > 0:\n+ server = binascii.hexlify(self.server).decode()\n+ else:\n+ server = \"\"\n+ return f\"COOKIE {client}{server}\"\n+\n+ @classmethod\n+ def from_wire_parser(\n+ cls, otype: Union[OptionType, str], parser: dns.wire.Parser\n+ ) -> Option:\n+ return cls(parser.get_bytes(8), parser.get_remaining())\n+\n+\n+class ReportChannelOption(Option):\n+ # RFC 9567\n+ def __init__(self, agent_domain: dns.name.Name):\n+ super().__init__(OptionType.REPORTCHANNEL)\n+ self.agent_domain = agent_domain\n+\n+ def to_wire(self, file: Any = None) -> Optional[bytes]:\n+ return self.agent_domain.to_wire(file)\n+\n+ def to_text(self) -> str:\n+ return \"REPORTCHANNEL \" + self.agent_domain.to_text()\n+\n+ @classmethod\n+ def from_wire_parser(\n+ cls, otype: Union[OptionType, str], parser: dns.wire.Parser\n+ ) -> Option:\n+ return cls(parser.get_name())\n+\n+\n _type_to_class: Dict[OptionType, Any] = {\n OptionType.ECS: ECSOption,\n OptionType.EDE: EDEOption,\n OptionType.NSID: NSIDOption,\n+ OptionType.COOKIE: CookieOption,\n+ OptionType.REPORTCHANNEL: ReportChannelOption,\n }\n \n \n@@ -512,5 +567,6 @@ def register_type(implementation: Any, otype: OptionType) -> None:\n PADDING = OptionType.PADDING\n CHAIN = OptionType.CHAIN\n EDE = OptionType.EDE\n+REPORTCHANNEL = OptionType.REPORTCHANNEL\n \n ### END generated OptionType constants"}, {"sha": "223f2d68797aaab53009641c2a700b9df411c8b6", "filename": "lib/dns/exception.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fexception.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fexception.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fexception.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -81,7 +81,7 @@ def _check_kwargs(self, **kwargs):\n if kwargs:\n assert (\n set(kwargs.keys()) == self.supp_kwargs\n- ), \"following set of keyword args is required: %s\" % (self.supp_kwargs)\n+ ), f\"following set of keyword args is required: {self.supp_kwargs}\"\n return kwargs\n \n def _fmt_kwargs(self, **kwargs):"}, {"sha": "a967ca41c63ac99d237619d884da4f1b5d0bc21e", "filename": "lib/dns/grange.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fgrange.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fgrange.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fgrange.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -54,7 +54,7 @@ def from_text(text: str) -> Tuple[int, int, int]:\n elif c.isdigit():\n cur += c\n else:\n- raise dns.exception.SyntaxError(\"Could not parse %s\" % (c))\n+ raise dns.exception.SyntaxError(f\"Could not parse {c}\")\n \n if state == 0:\n raise dns.exception.SyntaxError(\"no stop value specified\")"}, {"sha": "4dd1d1cade2ddc126234825a75eef66a7c574896", "filename": "lib/dns/ipv6.py", "status": "modified", "additions": 1, "deletions": 3, "changes": 4, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fipv6.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fipv6.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fipv6.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -143,9 +143,7 @@ def inet_aton(text: Union[str, bytes], ignore_scope: bool = False) -> bytes:\n if m is not None:\n b = dns.ipv4.inet_aton(m.group(2))\n btext = (\n- \"{}:{:02x}{:02x}:{:02x}{:02x}\".format(\n- m.group(1).decode(), b[0], b[1], b[2], b[3]\n- )\n+ f\"{m.group(1).decode()}:{b[0]:02x}{b[1]:02x}:{b[2]:02x}{b[3]:02x}\"\n ).encode()\n #\n # Try to turn '::<whatever>' into ':<whatever>'; if no match try to"}, {"sha": "e978a0a2e1d8ce681b4d353dc9d4dcb74c97006d", "filename": "lib/dns/message.py", "status": "modified", "additions": 78, "deletions": 33, "changes": 111, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fmessage.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fmessage.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fmessage.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -18,9 +18,10 @@\n \"\"\"DNS Messages\"\"\"\n \n import contextlib\n+import enum\n import io\n import time\n-from typing import Any, Dict, List, Optional, Tuple, Union\n+from typing import Any, Dict, List, Optional, Tuple, Union, cast\n \n import dns.edns\n import dns.entropy\n@@ -161,6 +162,7 @@ def __init__(self, id: Optional[int] = None):\n self.index: IndexType = {}\n self.errors: List[MessageError] = []\n self.time = 0.0\n+ self.wire: Optional[bytes] = None\n \n @property\n def question(self) -> List[dns.rrset.RRset]:\n@@ -220,16 +222,16 @@ def to_text(\n \n s = io.StringIO()\n s.write(\"id %d\\n\" % self.id)\n- s.write(\"opcode %s\\n\" % dns.opcode.to_text(self.opcode()))\n- s.write(\"rcode %s\\n\" % dns.rcode.to_text(self.rcode()))\n- s.write(\"flags %s\\n\" % dns.flags.to_text(self.flags))\n+ s.write(f\"opcode {dns.opcode.to_text(self.opcode())}\\n\")\n+ s.write(f\"rcode {dns.rcode.to_text(self.rcode())}\\n\")\n+ s.write(f\"flags {dns.flags.to_text(self.flags)}\\n\")\n if self.edns >= 0:\n- s.write(\"edns %s\\n\" % self.edns)\n+ s.write(f\"edns {self.edns}\\n\")\n if self.ednsflags != 0:\n- s.write(\"eflags %s\\n\" % dns.flags.edns_to_text(self.ednsflags))\n+ s.write(f\"eflags {dns.flags.edns_to_text(self.ednsflags)}\\n\")\n s.write(\"payload %d\\n\" % self.payload)\n for opt in self.options:\n- s.write(\"option %s\\n\" % opt.to_text())\n+ s.write(f\"option {opt.to_text()}\\n\")\n for name, which in self._section_enum.__members__.items():\n s.write(f\";{name}\\n\")\n for rrset in self.section_from_number(which):\n@@ -645,6 +647,7 @@ def to_wire(\n if multi:\n self.tsig_ctx = ctx\n wire = r.get_wire()\n+ self.wire = wire\n if prepend_length:\n wire = len(wire).to_bytes(2, \"big\") + wire\n return wire\n@@ -912,6 +915,14 @@ def set_opcode(self, opcode: dns.opcode.Opcode) -> None:\n self.flags &= 0x87FF\n self.flags |= dns.opcode.to_flags(opcode)\n \n+ def get_options(self, otype: dns.edns.OptionType) -> List[dns.edns.Option]:\n+ \"\"\"Return the list of options of the specified type.\"\"\"\n+ return [option for option in self.options if option.otype == otype]\n+\n+ def extended_errors(self) -> List[dns.edns.EDEOption]:\n+ \"\"\"Return the list of Extended DNS Error (EDE) options in the message\"\"\"\n+ return cast(List[dns.edns.EDEOption], self.get_options(dns.edns.OptionType.EDE))\n+\n def _get_one_rr_per_rrset(self, value):\n # What the caller picked is fine.\n return value\n@@ -1192,9 +1203,9 @@ def _get_section(self, section_number, count):\n if rdtype == dns.rdatatype.OPT:\n self.message.opt = dns.rrset.from_rdata(name, ttl, rd)\n elif rdtype == dns.rdatatype.TSIG:\n- if self.keyring is None:\n+ if self.keyring is None or self.keyring is True:\n raise UnknownTSIGKey(\"got signed message without keyring\")\n- if isinstance(self.keyring, dict):\n+ elif isinstance(self.keyring, dict):\n key = self.keyring.get(absolute_name)\n if isinstance(key, bytes):\n key = dns.tsig.Key(absolute_name, key, rd.algorithm)\n@@ -1203,19 +1214,20 @@ def _get_section(self, section_number, count):\n else:\n key = self.keyring\n if key is None:\n- raise UnknownTSIGKey(\"key '%s' unknown\" % name)\n- self.message.keyring = key\n- self.message.tsig_ctx = dns.tsig.validate(\n- self.parser.wire,\n- key,\n- absolute_name,\n- rd,\n- int(time.time()),\n- self.message.request_mac,\n- rr_start,\n- self.message.tsig_ctx,\n- self.multi,\n- )\n+ raise UnknownTSIGKey(f\"key '{name}' unknown\")\n+ if key:\n+ self.message.keyring = key\n+ self.message.tsig_ctx = dns.tsig.validate(\n+ self.parser.wire,\n+ key,\n+ absolute_name,\n+ rd,\n+ int(time.time()),\n+ self.message.request_mac,\n+ rr_start,\n+ self.message.tsig_ctx,\n+ self.multi,\n+ )\n self.message.tsig = dns.rrset.from_rdata(absolute_name, 0, rd)\n else:\n rrset = self.message.find_rrset(\n@@ -1251,6 +1263,7 @@ def read(self):\n factory = _message_factory_from_opcode(dns.opcode.from_flags(flags))\n self.message = factory(id=id)\n self.message.flags = dns.flags.Flag(flags)\n+ self.message.wire = self.parser.wire\n self.initialize_message(self.message)\n self.one_rr_per_rrset = self.message._get_one_rr_per_rrset(\n self.one_rr_per_rrset\n@@ -1290,8 +1303,10 @@ def from_wire(\n ) -> Message:\n \"\"\"Convert a DNS wire format message into a message object.\n \n- *keyring*, a ``dns.tsig.Key`` or ``dict``, the key or keyring to use if the message\n- is signed.\n+ *keyring*, a ``dns.tsig.Key``, ``dict``, ``bool``, or ``None``, the key or keyring\n+ to use if the message is signed. If ``None`` or ``True``, then trying to decode\n+ a message with a TSIG will fail as it cannot be validated. If ``False``, then\n+ TSIG validation is disabled.\n \n *request_mac*, a ``bytes`` or ``None``. If the message is a response to a\n TSIG-signed request, *request_mac* should be set to the MAC of that request.\n@@ -1811,20 +1826,31 @@ def make_query(\n return m\n \n \n+class CopyMode(enum.Enum):\n+ \"\"\"\n+ How should sections be copied when making an update response?\n+ \"\"\"\n+\n+ NOTHING = 0\n+ QUESTION = 1\n+ EVERYTHING = 2\n+\n+\n def make_response(\n query: Message,\n recursion_available: bool = False,\n our_payload: int = 8192,\n fudge: int = 300,\n tsig_error: int = 0,\n pad: Optional[int] = None,\n+ copy_mode: Optional[CopyMode] = None,\n ) -> Message:\n \"\"\"Make a message which is a response for the specified query.\n The message returned is really a response skeleton; it has all of the infrastructure\n required of a response, but none of the content.\n \n- The response's question section is a shallow copy of the query's question section,\n- so the query's question RRsets should not be changed.\n+ Response section(s) which are copied are shallow copies of the matching section(s)\n+ in the query, so the query's RRsets should not be changed.\n \n *query*, a ``dns.message.Message``, the query to respond to.\n \n@@ -1837,25 +1863,44 @@ def make_response(\n *tsig_error*, an ``int``, the TSIG error.\n \n *pad*, a non-negative ``int`` or ``None``. If 0, the default, do not pad; otherwise\n- if not ``None`` add padding bytes to make the message size a multiple of *pad*.\n- Note that if padding is non-zero, an EDNS PADDING option will always be added to the\n+ if not ``None`` add padding bytes to make the message size a multiple of *pad*. Note\n+ that if padding is non-zero, an EDNS PADDING option will always be added to the\n message. If ``None``, add padding following RFC 8467, namely if the request is\n padded, pad the response to 468 otherwise do not pad.\n \n+ *copy_mode*, a ``dns.message.CopyMode`` or ``None``, determines how sections are\n+ copied. The default, ``None`` copies sections according to the default for the\n+ message's opcode, which is currently ``dns.message.CopyMode.QUESTION`` for all\n+ opcodes. ``dns.message.CopyMode.QUESTION`` copies only the question section.\n+ ``dns.message.CopyMode.EVERYTHING`` copies all sections other than OPT or TSIG\n+ records, which are created appropriately if needed. ``dns.message.CopyMode.NOTHING``\n+ copies no sections; note that this mode is for server testing purposes and is\n+ otherwise not recommended for use. In particular, ``dns.message.is_response()``\n+ will be ``False`` if you create a response this way and the rcode is not\n+ ``FORMERR``, ``SERVFAIL``, ``NOTIMP``, or ``REFUSED``.\n+\n Returns a ``dns.message.Message`` object whose specific class is appropriate for the\n- query. For example, if query is a ``dns.update.UpdateMessage``, response will be\n- too.\n+ query. For example, if query is a ``dns.update.UpdateMessage``, the response will\n+ be one too.\n \"\"\"\n \n if query.flags & dns.flags.QR:\n raise dns.exception.FormError(\"specified query message is not a query\")\n- factory = _message_factory_from_opcode(query.opcode())\n+ opcode = query.opcode()\n+ factory = _message_factory_from_opcode(opcode)\n response = factory(id=query.id)\n response.flags = dns.flags.QR | (query.flags & dns.flags.RD)\n if recursion_available:\n response.flags |= dns.flags.RA\n- response.set_opcode(query.opcode())\n- response.question = list(query.question)\n+ response.set_opcode(opcode)\n+ if copy_mode is None:\n+ copy_mode = CopyMode.QUESTION\n+ if copy_mode != CopyMode.NOTHING:\n+ response.question = list(query.question)\n+ if copy_mode == CopyMode.EVERYTHING:\n+ response.answer = list(query.answer)\n+ response.authority = list(query.authority)\n+ response.additional = list(query.additional)\n if query.edns >= 0:\n if pad is None:\n # Set response padding per RFC 8467"}, {"sha": "f79f0d0f6f16f95228ae111b85d6a9e368b98345", "filename": "lib/dns/name.py", "status": "modified", "additions": 3, "deletions": 2, "changes": 5, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fname.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fname.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fname.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -59,11 +59,11 @@ class NameRelation(dns.enum.IntEnum):\n \n @classmethod\n def _maximum(cls):\n- return cls.COMMONANCESTOR\n+ return cls.COMMONANCESTOR # pragma: no cover\n \n @classmethod\n def _short_name(cls):\n- return cls.__name__\n+ return cls.__name__ # pragma: no cover\n \n \n # Backwards compatibility\n@@ -277,6 +277,7 @@ def encode(self, label: str) -> bytes:\n raise NoIDNA2008\n try:\n if self.uts_46:\n+ # pylint: disable=possibly-used-before-assignment\n label = idna.uts46_remap(label, False, self.transitional)\n return idna.alabel(label)\n except idna.IDNAError as e:"}, {"sha": "b02a239b3c5886a47aeeb09b68c235c59fb91b95", "filename": "lib/dns/nameserver.py", "status": "modified", "additions": 4, "deletions": 0, "changes": 4, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fnameserver.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fnameserver.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fnameserver.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -168,12 +168,14 @@ def __init__(\n bootstrap_address: Optional[str] = None,\n verify: Union[bool, str] = True,\n want_get: bool = False,\n+ http_version: dns.query.HTTPVersion = dns.query.HTTPVersion.DEFAULT,\n ):\n super().__init__()\n self.url = url\n self.bootstrap_address = bootstrap_address\n self.verify = verify\n self.want_get = want_get\n+ self.http_version = http_version\n \n def kind(self):\n return \"DoH\"\n@@ -214,6 +216,7 @@ def query(\n ignore_trailing=ignore_trailing,\n verify=self.verify,\n post=(not self.want_get),\n+ http_version=self.http_version,\n )\n \n async def async_query(\n@@ -238,6 +241,7 @@ async def async_query(\n ignore_trailing=ignore_trailing,\n verify=self.verify,\n post=(not self.want_get),\n+ http_version=self.http_version,\n )\n \n "}, {"sha": "0d8a977abdd2351addfad50f379173f02fe7b80e", "filename": "lib/dns/query.py", "status": "modified", "additions": 315, "deletions": 228, "changes": 543, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fquery.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fquery.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fquery.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -23,11 +23,13 @@\n import errno\n import os\n import os.path\n+import random\n import selectors\n import socket\n import struct\n import time\n-from typing import Any, Dict, Optional, Tuple, Union\n+import urllib.parse\n+from typing import Any, Dict, Optional, Tuple, Union, cast\n \n import dns._features\n import dns.exception\n@@ -129,7 +131,7 @@ def __init__(\n family=socket.AF_UNSPEC,\n **kwargs,\n ):\n- if resolver is None:\n+ if resolver is None and bootstrap_address is None:\n # pylint: disable=import-outside-toplevel,redefined-outer-name\n import dns.resolver\n \n@@ -217,7 +219,7 @@ def _wait_for(fd, readable, writable, _, expiration):\n \n if readable and isinstance(fd, ssl.SSLSocket) and fd.pending() > 0:\n return True\n- sel = _selector_class()\n+ sel = selectors.DefaultSelector()\n events = 0\n if readable:\n events |= selectors.EVENT_READ\n@@ -235,26 +237,6 @@ def _wait_for(fd, readable, writable, _, expiration):\n raise dns.exception.Timeout\n \n \n-def _set_selector_class(selector_class):\n- # Internal API. Do not use.\n-\n- global _selector_class\n-\n- _selector_class = selector_class\n-\n-\n-if hasattr(selectors, \"PollSelector\"):\n- # Prefer poll() on platforms that support it because it has no\n- # limits on the maximum value of a file descriptor (plus it will\n- # be more efficient for high values).\n- #\n- # We ignore typing here as we can't say _selector_class is Any\n- # on python < 3.8 due to a bug.\n- _selector_class = selectors.PollSelector # type: ignore\n-else:\n- _selector_class = selectors.SelectSelector # type: ignore\n-\n-\n def _wait_for_readable(s, expiration):\n _wait_for(s, True, False, True, expiration)\n \n@@ -355,6 +337,36 @@ def _make_socket(af, type, source, ssl_context=None, server_hostname=None):\n raise\n \n \n+def _maybe_get_resolver(\n+ resolver: Optional[\"dns.resolver.Resolver\"],\n+) -> \"dns.resolver.Resolver\":\n+ # We need a separate method for this to avoid overriding the global\n+ # variable \"dns\" with the as-yet undefined local variable \"dns\"\n+ # in https().\n+ if resolver is None:\n+ # pylint: disable=import-outside-toplevel,redefined-outer-name\n+ import dns.resolver\n+\n+ resolver = dns.resolver.Resolver()\n+ return resolver\n+\n+\n+class HTTPVersion(enum.IntEnum):\n+ \"\"\"Which version of HTTP should be used?\n+\n+ DEFAULT will select the first version from the list [2, 1.1, 3] that\n+ is available.\n+ \"\"\"\n+\n+ DEFAULT = 0\n+ HTTP_1 = 1\n+ H1 = 1\n+ HTTP_2 = 2\n+ H2 = 2\n+ HTTP_3 = 3\n+ H3 = 3\n+\n+\n def https(\n q: dns.message.Message,\n where: str,\n@@ -370,7 +382,8 @@ def https(\n bootstrap_address: Optional[str] = None,\n verify: Union[bool, str] = True,\n resolver: Optional[\"dns.resolver.Resolver\"] = None,\n- family: Optional[int] = socket.AF_UNSPEC,\n+ family: int = socket.AF_UNSPEC,\n+ http_version: HTTPVersion = HTTPVersion.DEFAULT,\n ) -> dns.message.Message:\n \"\"\"Return the response obtained after sending a query via DNS-over-HTTPS.\n \n@@ -420,28 +433,67 @@ def https(\n *family*, an ``int``, the address family. If socket.AF_UNSPEC (the default), both A\n and AAAA records will be retrieved.\n \n+ *http_version*, a ``dns.query.HTTPVersion``, indicating which HTTP version to use.\n+\n Returns a ``dns.message.Message``.\n \"\"\"\n \n- if not have_doh:\n- raise NoDOH # pragma: no cover\n- if session and not isinstance(session, httpx.Client):\n- raise ValueError(\"session parameter must be an httpx.Client\")\n-\n- wire = q.to_wire()\n (af, _, the_source) = _destination_and_source(\n where, port, source, source_port, False\n )\n- transport = None\n- headers = {\"accept\": \"application/dns-message\"}\n if af is not None and dns.inet.is_address(where):\n if af == socket.AF_INET:\n- url = \"https://{}:{}{}\".format(where, port, path)\n+ url = f\"https://{where}:{port}{path}\"\n elif af == socket.AF_INET6:\n- url = \"https://[{}]:{}{}\".format(where, port, path)\n+ url = f\"https://[{where}]:{port}{path}\"\n else:\n url = where\n \n+ extensions = {}\n+ if bootstrap_address is None:\n+ # pylint: disable=possibly-used-before-assignment\n+ parsed = urllib.parse.urlparse(url)\n+ if parsed.hostname is None:\n+ raise ValueError(\"no hostname in URL\")\n+ if dns.inet.is_address(parsed.hostname):\n+ bootstrap_address = parsed.hostname\n+ extensions[\"sni_hostname\"] = parsed.hostname\n+ if parsed.port is not None:\n+ port = parsed.port\n+\n+ if http_version == HTTPVersion.H3 or (\n+ http_version == HTTPVersion.DEFAULT and not have_doh\n+ ):\n+ if bootstrap_address is None:\n+ resolver = _maybe_get_resolver(resolver)\n+ assert parsed.hostname is not None # for mypy\n+ answers = resolver.resolve_name(parsed.hostname, family)\n+ bootstrap_address = random.choice(list(answers.addresses()))\n+ return _http3(\n+ q,\n+ bootstrap_address,\n+ url,\n+ timeout,\n+ port,\n+ source,\n+ source_port,\n+ one_rr_per_rrset,\n+ ignore_trailing,\n+ verify=verify,\n+ post=post,\n+ )\n+\n+ if not have_doh:\n+ raise NoDOH # pragma: no cover\n+ if session and not isinstance(session, httpx.Client):\n+ raise ValueError(\"session parameter must be an httpx.Client\")\n+\n+ wire = q.to_wire()\n+ headers = {\"accept\": \"application/dns-message\"}\n+\n+ h1 = http_version in (HTTPVersion.H1, HTTPVersion.DEFAULT)\n+ h2 = http_version in (HTTPVersion.H2, HTTPVersion.DEFAULT)\n+\n # set source port and source address\n \n if the_source is None:\n@@ -450,21 +502,22 @@ def https(\n else:\n local_address = the_source[0]\n local_port = the_source[1]\n- transport = _HTTPTransport(\n- local_address=local_address,\n- http1=True,\n- http2=True,\n- verify=verify,\n- local_port=local_port,\n- bootstrap_address=bootstrap_address,\n- resolver=resolver,\n- family=family,\n- )\n \n if session:\n cm: contextlib.AbstractContextManager = contextlib.nullcontext(session)\n else:\n- cm = httpx.Client(http1=True, http2=True, verify=verify, transport=transport)\n+ transport = _HTTPTransport(\n+ local_address=local_address,\n+ http1=h1,\n+ http2=h2,\n+ verify=verify,\n+ local_port=local_port,\n+ bootstrap_address=bootstrap_address,\n+ resolver=resolver,\n+ family=family,\n+ )\n+\n+ cm = httpx.Client(http1=h1, http2=h2, verify=verify, transport=transport)\n with cm as session:\n # see https://tools.ietf.org/html/rfc8484#section-4.1.1 for DoH\n # GET and POST examples\n@@ -475,20 +528,30 @@ def https(\n \"content-length\": str(len(wire)),\n }\n )\n- response = session.post(url, headers=headers, content=wire, timeout=timeout)\n+ response = session.post(\n+ url,\n+ headers=headers,\n+ content=wire,\n+ timeout=timeout,\n+ extensions=extensions,\n+ )\n else:\n wire = base64.urlsafe_b64encode(wire).rstrip(b\"=\")\n twire = wire.decode() # httpx does a repr() if we give it bytes\n response = session.get(\n- url, headers=headers, timeout=timeout, params={\"dns\": twire}\n+ url,\n+ headers=headers,\n+ timeout=timeout,\n+ params={\"dns\": twire},\n+ extensions=extensions,\n )\n \n # see https://tools.ietf.org/html/rfc8484#section-4.2.1 for info about DoH\n # status codes\n if response.status_code < 200 or response.status_code > 299:\n raise ValueError(\n- \"{} responded with status code {}\"\n- \"\\nResponse body: {}\".format(where, response.status_code, response.content)\n+ f\"{where} responded with status code {response.status_code}\"\n+ f\"\\nResponse body: {response.content}\"\n )\n r = dns.message.from_wire(\n response.content,\n@@ -503,6 +566,81 @@ def https(\n return r\n \n \n+def _find_header(headers: dns.quic.Headers, name: bytes) -> bytes:\n+ if headers is None:\n+ raise KeyError\n+ for header, value in headers:\n+ if header == name:\n+ return value\n+ raise KeyError\n+\n+\n+def _check_status(headers: dns.quic.Headers, peer: str, wire: bytes) -> None:\n+ value = _find_header(headers, b\":status\")\n+ if value is None:\n+ raise SyntaxError(\"no :status header in response\")\n+ status = int(value)\n+ if status < 0:\n+ raise SyntaxError(\"status is negative\")\n+ if status < 200 or status > 299:\n+ error = \"\"\n+ if len(wire) > 0:\n+ try:\n+ error = \": \" + wire.decode()\n+ except Exception:\n+ pass\n+ raise ValueError(f\"{peer} responded with status code {status}{error}\")\n+\n+\n+def _http3(\n+ q: dns.message.Message,\n+ where: str,\n+ url: str,\n+ timeout: Optional[float] = None,\n+ port: int = 853,\n+ source: Optional[str] = None,\n+ source_port: int = 0,\n+ one_rr_per_rrset: bool = False,\n+ ignore_trailing: bool = False,\n+ verify: Union[bool, str] = True,\n+ hostname: Optional[str] = None,\n+ post: bool = True,\n+) -> dns.message.Message:\n+ if not dns.quic.have_quic:\n+ raise NoDOH(\"DNS-over-HTTP3 is not available.\") # pragma: no cover\n+\n+ url_parts = urllib.parse.urlparse(url)\n+ hostname = url_parts.hostname\n+ if url_parts.port is not None:\n+ port = url_parts.port\n+\n+ q.id = 0\n+ wire = q.to_wire()\n+ manager = dns.quic.SyncQuicManager(\n+ verify_mode=verify, server_name=hostname, h3=True\n+ )\n+\n+ with manager:\n+ connection = manager.connect(where, port, source, source_port)\n+ (start, expiration) = _compute_times(timeout)\n+ with connection.make_stream(timeout) as stream:\n+ stream.send_h3(url, wire, post)\n+ wire = stream.receive(_remaining(expiration))\n+ _check_status(stream.headers(), where, wire)\n+ finish = time.time()\n+ r = dns.message.from_wire(\n+ wire,\n+ keyring=q.keyring,\n+ request_mac=q.request_mac,\n+ one_rr_per_rrset=one_rr_per_rrset,\n+ ignore_trailing=ignore_trailing,\n+ )\n+ r.time = max(finish - start, 0.0)\n+ if not q.is_response(r):\n+ raise BadResponse\n+ return r\n+\n+\n def _udp_recv(sock, max_size, expiration):\n \"\"\"Reads a datagram from the socket.\n A Timeout exception will be raised if the operation is not completed\n@@ -855,7 +993,7 @@ def _net_read(sock, count, expiration):\n try:\n n = sock.recv(count)\n if n == b\"\":\n- raise EOFError\n+ raise EOFError(\"EOF\")\n count -= len(n)\n s += n\n except (BlockingIOError, ssl.SSLWantReadError):\n@@ -1023,6 +1161,7 @@ def tcp(\n cm = _make_socket(af, socket.SOCK_STREAM, source)\n with cm as s:\n if not sock:\n+ # pylint: disable=possibly-used-before-assignment\n _connect(s, destination, expiration)\n send_tcp(s, wire, expiration)\n (r, received_time) = receive_tcp(\n@@ -1188,6 +1327,7 @@ def quic(\n ignore_trailing: bool = False,\n connection: Optional[dns.quic.SyncQuicConnection] = None,\n verify: Union[bool, str] = True,\n+ hostname: Optional[str] = None,\n server_hostname: Optional[str] = None,\n ) -> dns.message.Message:\n \"\"\"Return the response obtained after sending a query via DNS-over-QUIC.\n@@ -1212,24 +1352,31 @@ def quic(\n *ignore_trailing*, a ``bool``. If ``True``, ignore trailing junk at end of the\n received message.\n \n- *connection*, a ``dns.quic.SyncQuicConnection``. If provided, the\n- connection to use to send the query.\n+ *connection*, a ``dns.quic.SyncQuicConnection``. If provided, the connection to use\n+ to send the query.\n \n *verify*, a ``bool`` or ``str``. If a ``True``, then TLS certificate verification\n of the server is done using the default CA bundle; if ``False``, then no\n verification is done; if a `str` then it specifies the path to a certificate file or\n directory which will be used for verification.\n \n- *server_hostname*, a ``str`` containing the server's hostname. The\n- default is ``None``, which means that no hostname is known, and if an\n- SSL context is created, hostname checking will be disabled.\n+ *hostname*, a ``str`` containing the server's hostname or ``None``. The default is\n+ ``None``, which means that no hostname is known, and if an SSL context is created,\n+ hostname checking will be disabled. This value is ignored if *url* is not\n+ ``None``.\n+\n+ *server_hostname*, a ``str`` or ``None``. This item is for backwards compatibility\n+ only, and has the same meaning as *hostname*.\n \n Returns a ``dns.message.Message``.\n \"\"\"\n \n if not dns.quic.have_quic:\n raise NoDOQ(\"DNS-over-QUIC is not available.\") # pragma: no cover\n \n+ if server_hostname is not None and hostname is None:\n+ hostname = server_hostname\n+\n q.id = 0\n wire = q.to_wire()\n the_connection: dns.quic.SyncQuicConnection\n@@ -1238,9 +1385,7 @@ def quic(\n manager: contextlib.AbstractContextManager = contextlib.nullcontext(None)\n the_connection = connection\n else:\n- manager = dns.quic.SyncQuicManager(\n- verify_mode=verify, server_name=server_hostname\n- )\n+ manager = dns.quic.SyncQuicManager(verify_mode=verify, server_name=hostname)\n the_manager = manager # for type checking happiness\n \n with manager:\n@@ -1264,6 +1409,70 @@ def quic(\n return r\n \n \n+class UDPMode(enum.IntEnum):\n+ \"\"\"How should UDP be used in an IXFR from :py:func:`inbound_xfr()`?\n+\n+ NEVER means \"never use UDP; always use TCP\"\n+ TRY_FIRST means \"try to use UDP but fall back to TCP if needed\"\n+ ONLY means \"raise ``dns.xfr.UseTCP`` if trying UDP does not succeed\"\n+ \"\"\"\n+\n+ NEVER = 0\n+ TRY_FIRST = 1\n+ ONLY = 2\n+\n+\n+def _inbound_xfr(\n+ txn_manager: dns.transaction.TransactionManager,\n+ s: socket.socket,\n+ query: dns.message.Message,\n+ serial: Optional[int],\n+ timeout: Optional[float],\n+ expiration: float,\n+) -> Any:\n+ \"\"\"Given a socket, does the zone transfer.\"\"\"\n+ rdtype = query.question[0].rdtype\n+ is_ixfr = rdtype == dns.rdatatype.IXFR\n+ origin = txn_manager.from_wire_origin()\n+ wire = query.to_wire()\n+ is_udp = s.type == socket.SOCK_DGRAM\n+ if is_udp:\n+ _udp_send(s, wire, None, expiration)\n+ else:\n+ tcpmsg = struct.pack(\"!H\", len(wire)) + wire\n+ _net_write(s, tcpmsg, expiration)\n+ with dns.xfr.Inbound(txn_manager, rdtype, serial, is_udp) as inbound:\n+ done = False\n+ tsig_ctx = None\n+ while not done:\n+ (_, mexpiration) = _compute_times(timeout)\n+ if mexpiration is None or (\n+ expiration is not None and mexpiration > expiration\n+ ):\n+ mexpiration = expiration\n+ if is_udp:\n+ (rwire, _) = _udp_recv(s, 65535, mexpiration)\n+ else:\n+ ldata = _net_read(s, 2, mexpiration)\n+ (l,) = struct.unpack(\"!H\", ldata)\n+ rwire = _net_read(s, l, mexpiration)\n+ r = dns.message.from_wire(\n+ rwire,\n+ keyring=query.keyring,\n+ request_mac=query.mac,\n+ xfr=True,\n+ origin=origin,\n+ tsig_ctx=tsig_ctx,\n+ multi=(not is_udp),\n+ one_rr_per_rrset=is_ixfr,\n+ )\n+ done = inbound.process_message(r)\n+ yield r\n+ tsig_ctx = r.tsig_ctx\n+ if query.keyring and not r.had_tsig:\n+ raise dns.exception.FormError(\"missing TSIG\")\n+\n+\n def xfr(\n where: str,\n zone: Union[dns.name.Name, str],\n@@ -1333,134 +1542,52 @@ def xfr(\n Returns a generator of ``dns.message.Message`` objects.\n \"\"\"\n \n+ class DummyTransactionManager(dns.transaction.TransactionManager):\n+ def __init__(self, origin, relativize):\n+ self.info = (origin, relativize, dns.name.empty if relativize else origin)\n+\n+ def origin_information(self):\n+ return self.info\n+\n+ def get_class(self) -> dns.rdataclass.RdataClass:\n+ raise NotImplementedError # pragma: no cover\n+\n+ def reader(self):\n+ raise NotImplementedError # pragma: no cover\n+\n+ def writer(self, replacement: bool = False) -> dns.transaction.Transaction:\n+ class DummyTransaction:\n+ def nop(self, *args, **kw):\n+ pass\n+\n+ def __getattr__(self, _):\n+ return self.nop\n+\n+ return cast(dns.transaction.Transaction, DummyTransaction())\n+\n if isinstance(zone, str):\n zone = dns.name.from_text(zone)\n rdtype = dns.rdatatype.RdataType.make(rdtype)\n q = dns.message.make_query(zone, rdtype, rdclass)\n if rdtype == dns.rdatatype.IXFR:\n- rrset = dns.rrset.from_text(zone, 0, \"IN\", \"SOA\", \". . %u 0 0 0 0\" % serial)\n- q.authority.append(rrset)\n+ rrset = q.find_rrset(\n+ q.authority, zone, dns.rdataclass.IN, dns.rdatatype.SOA, create=True\n+ )\n+ soa = dns.rdata.from_text(\"IN\", \"SOA\", \". . %u 0 0 0 0\" % serial)\n+ rrset.add(soa, 0)\n if keyring is not None:\n q.use_tsig(keyring, keyname, algorithm=keyalgorithm)\n- wire = q.to_wire()\n (af, destination, source) = _destination_and_source(\n where, port, source, source_port\n )\n+ (_, expiration) = _compute_times(lifetime)\n+ tm = DummyTransactionManager(zone, relativize)\n if use_udp and rdtype != dns.rdatatype.IXFR:\n raise ValueError(\"cannot do a UDP AXFR\")\n sock_type = socket.SOCK_DGRAM if use_udp else socket.SOCK_STREAM\n with _make_socket(af, sock_type, source) as s:\n- (_, expiration) = _compute_times(lifetime)\n _connect(s, destination, expiration)\n- l = len(wire)\n- if use_udp:\n- _udp_send(s, wire, None, expiration)\n- else:\n- tcpmsg = struct.pack(\"!H\", l) + wire\n- _net_write(s, tcpmsg, expiration)\n- done = False\n- delete_mode = True\n- expecting_SOA = False\n- soa_rrset = None\n- if relativize:\n- origin = zone\n- oname = dns.name.empty\n- else:\n- origin = None\n- oname = zone\n- tsig_ctx = None\n- while not done:\n- (_, mexpiration) = _compute_times(timeout)\n- if mexpiration is None or (\n- expiration is not None and mexpiration > expiration\n- ):\n- mexpiration = expiration\n- if use_udp:\n- (wire, _) = _udp_recv(s, 65535, mexpiration)\n- else:\n- ldata = _net_read(s, 2, mexpiration)\n- (l,) = struct.unpack(\"!H\", ldata)\n- wire = _net_read(s, l, mexpiration)\n- is_ixfr = rdtype == dns.rdatatype.IXFR\n- r = dns.message.from_wire(\n- wire,\n- keyring=q.keyring,\n- request_mac=q.mac,\n- xfr=True,\n- origin=origin,\n- tsig_ctx=tsig_ctx,\n- multi=True,\n- one_rr_per_rrset=is_ixfr,\n- )\n- rcode = r.rcode()\n- if rcode != dns.rcode.NOERROR:\n- raise TransferError(rcode)\n- tsig_ctx = r.tsig_ctx\n- answer_index = 0\n- if soa_rrset is None:\n- if not r.answer or r.answer[0].name != oname:\n- raise dns.exception.FormError(\"No answer or RRset not for qname\")\n- rrset = r.answer[0]\n- if rrset.rdtype != dns.rdatatype.SOA:\n- raise dns.exception.FormError(\"first RRset is not an SOA\")\n- answer_index = 1\n- soa_rrset = rrset.copy()\n- if rdtype == dns.rdatatype.IXFR:\n- if dns.serial.Serial(soa_rrset[0].serial) <= serial:\n- #\n- # We're already up-to-date.\n- #\n- done = True\n- else:\n- expecting_SOA = True\n- #\n- # Process SOAs in the answer section (other than the initial\n- # SOA in the first message).\n- #\n- for rrset in r.answer[answer_index:]:\n- if done:\n- raise dns.exception.FormError(\"answers after final SOA\")\n- if rrset.rdtype == dns.rdatatype.SOA and rrset.name == oname:\n- if expecting_SOA:\n- if rrset[0].serial != serial:\n- raise dns.exception.FormError(\"IXFR base serial mismatch\")\n- expecting_SOA = False\n- elif rdtype == dns.rdatatype.IXFR:\n- delete_mode = not delete_mode\n- #\n- # If this SOA RRset is equal to the first we saw then we're\n- # finished. If this is an IXFR we also check that we're\n- # seeing the record in the expected part of the response.\n- #\n- if rrset == soa_rrset and (\n- rdtype == dns.rdatatype.AXFR\n- or (rdtype == dns.rdatatype.IXFR and delete_mode)\n- ):\n- done = True\n- elif expecting_SOA:\n- #\n- # We made an IXFR request and are expecting another\n- # SOA RR, but saw something else, so this must be an\n- # AXFR response.\n- #\n- rdtype = dns.rdatatype.AXFR\n- expecting_SOA = False\n- if done and q.keyring and not r.had_tsig:\n- raise dns.exception.FormError(\"missing TSIG\")\n- yield r\n-\n-\n-class UDPMode(enum.IntEnum):\n- \"\"\"How should UDP be used in an IXFR from :py:func:`inbound_xfr()`?\n-\n- NEVER means \"never use UDP; always use TCP\"\n- TRY_FIRST means \"try to use UDP but fall back to TCP if needed\"\n- ONLY means \"raise ``dns.xfr.UseTCP`` if trying UDP does not succeed\"\n- \"\"\"\n-\n- NEVER = 0\n- TRY_FIRST = 1\n- ONLY = 2\n+ yield from _inbound_xfr(tm, s, q, serial, timeout, expiration)\n \n \n def inbound_xfr(\n@@ -1514,65 +1641,25 @@ def inbound_xfr(\n (query, serial) = dns.xfr.make_query(txn_manager)\n else:\n serial = dns.xfr.extract_serial_from_query(query)\n- rdtype = query.question[0].rdtype\n- is_ixfr = rdtype == dns.rdatatype.IXFR\n- origin = txn_manager.from_wire_origin()\n- wire = query.to_wire()\n+\n (af, destination, source) = _destination_and_source(\n where, port, source, source_port\n )\n (_, expiration) = _compute_times(lifetime)\n- retry = True\n- while retry:\n- retry = False\n- if is_ixfr and udp_mode != UDPMode.NEVER:\n- sock_type = socket.SOCK_DGRAM\n- is_udp = True\n- else:\n- sock_type = socket.SOCK_STREAM\n- is_udp = False\n- with _make_socket(af, sock_type, source) as s:\n+ if query.question[0].rdtype == dns.rdatatype.IXFR and udp_mode != UDPMode.NEVER:\n+ with _make_socket(af, socket.SOCK_DGRAM, source) as s:\n _connect(s, destination, expiration)\n- if is_udp:\n- _udp_send(s, wire, None, expiration)\n- else:\n- tcpmsg = struct.pack(\"!H\", len(wire)) + wire\n- _net_write(s, tcpmsg, expiration)\n- with dns.xfr.Inbound(txn_manager, rdtype, serial, is_udp) as inbound:\n- done = False\n- tsig_ctx = None\n- while not done:\n- (_, mexpiration) = _compute_times(timeout)\n- if mexpiration is None or (\n- expiration is not None and mexpiration > expiration\n- ):\n- mexpiration = expiration\n- if is_udp:\n- (rwire, _) = _udp_recv(s, 65535, mexpiration)\n- else:\n- ldata = _net_read(s, 2, mexpiration)\n- (l,) = struct.unpack(\"!H\", ldata)\n- rwire = _net_read(s, l, mexpiration)\n- r = dns.message.from_wire(\n- rwire,\n- keyring=query.keyring,\n- request_mac=query.mac,\n- xfr=True,\n- origin=origin,\n- tsig_ctx=tsig_ctx,\n- multi=(not is_udp),\n- one_rr_per_rrset=is_ixfr,\n- )\n- try:\n- done = inbound.process_message(r)\n- except dns.xfr.UseTCP:\n- assert is_udp # should not happen if we used TCP!\n- if udp_mode == UDPMode.ONLY:\n- raise\n- done = True\n- retry = True\n- udp_mode = UDPMode.NEVER\n- continue\n- tsig_ctx = r.tsig_ctx\n- if not retry and query.keyring and not r.had_tsig:\n- raise dns.exception.FormError(\"missing TSIG\")\n+ try:\n+ for _ in _inbound_xfr(\n+ txn_manager, s, query, serial, timeout, expiration\n+ ):\n+ pass\n+ return\n+ except dns.xfr.UseTCP:\n+ if udp_mode == UDPMode.ONLY:\n+ raise\n+\n+ with _make_socket(af, socket.SOCK_STREAM, source) as s:\n+ _connect(s, destination, expiration)\n+ for _ in _inbound_xfr(txn_manager, s, query, serial, timeout, expiration):\n+ pass"}, {"sha": "0750e729b4401e77bf3da1f8716e23e4538c1d24", "filename": "lib/dns/quic/__init__.py", "status": "modified", "additions": 5, "deletions": 0, "changes": 5, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fquic%2F__init__.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fquic%2F__init__.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fquic%2F__init__.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,5 +1,7 @@\n # Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n \n+from typing import List, Tuple\n+\n import dns._features\n import dns.asyncbackend\n \n@@ -73,3 +75,6 @@ class SyncQuicStream: # type: ignore\n class SyncQuicConnection: # type: ignore\n def make_stream(self) -> Any:\n raise NotImplementedError\n+\n+\n+Headers = List[Tuple[bytes, bytes]]"}, {"sha": "f87515dacfd2252fbb8204afdd25b8c91dff0207", "filename": "lib/dns/quic/_asyncio.py", "status": "modified", "additions": 57, "deletions": 18, "changes": 75, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fquic%2F_asyncio.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fquic%2F_asyncio.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fquic%2F_asyncio.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -43,12 +43,26 @@ async def wait_for(self, amount, expiration):\n raise dns.exception.Timeout\n self._expecting = 0\n \n+ async def wait_for_end(self, expiration):\n+ while True:\n+ timeout = self._timeout_from_expiration(expiration)\n+ if self._buffer.seen_end():\n+ return\n+ try:\n+ await asyncio.wait_for(self._wait_for_wake_up(), timeout)\n+ except TimeoutError:\n+ raise dns.exception.Timeout\n+\n async def receive(self, timeout=None):\n expiration = self._expiration_from_timeout(timeout)\n- await self.wait_for(2, expiration)\n- (size,) = struct.unpack(\"!H\", self._buffer.get(2))\n- await self.wait_for(size, expiration)\n- return self._buffer.get(size)\n+ if self._connection.is_h3():\n+ await self.wait_for_end(expiration)\n+ return self._buffer.get_all()\n+ else:\n+ await self.wait_for(2, expiration)\n+ (size,) = struct.unpack(\"!H\", self._buffer.get(2))\n+ await self.wait_for(size, expiration)\n+ return self._buffer.get(size)\n \n async def send(self, datagram, is_end=False):\n data = self._encapsulate(datagram)\n@@ -83,6 +97,7 @@ def __init__(self, connection, address, port, source, source_port, manager=None)\n self._wake_timer = asyncio.Condition()\n self._receiver_task = None\n self._sender_task = None\n+ self._wake_pending = False\n \n async def _receiver(self):\n try:\n@@ -104,19 +119,24 @@ async def _receiver(self):\n self._connection.receive_datagram(datagram, address, time.time())\n # Wake up the timer in case the sender is sleeping, as there may be\n # stuff to send now.\n- async with self._wake_timer:\n- self._wake_timer.notify_all()\n+ await self._wakeup()\n except Exception:\n pass\n finally:\n self._done = True\n- async with self._wake_timer:\n- self._wake_timer.notify_all()\n+ await self._wakeup()\n self._handshake_complete.set()\n \n+ async def _wakeup(self):\n+ self._wake_pending = True\n+ async with self._wake_timer:\n+ self._wake_timer.notify_all()\n+\n async def _wait_for_wake_timer(self):\n async with self._wake_timer:\n- await self._wake_timer.wait()\n+ if not self._wake_pending:\n+ await self._wake_timer.wait()\n+ self._wake_pending = False\n \n async def _sender(self):\n await self._socket_created.wait()\n@@ -140,9 +160,28 @@ async def _handle_events(self):\n if event is None:\n return\n if isinstance(event, aioquic.quic.events.StreamDataReceived):\n- stream = self._streams.get(event.stream_id)\n- if stream:\n- await stream._add_input(event.data, event.end_stream)\n+ if self.is_h3():\n+ h3_events = self._h3_conn.handle_event(event)\n+ for h3_event in h3_events:\n+ if isinstance(h3_event, aioquic.h3.events.HeadersReceived):\n+ stream = self._streams.get(event.stream_id)\n+ if stream:\n+ if stream._headers is None:\n+ stream._headers = h3_event.headers\n+ elif stream._trailers is None:\n+ stream._trailers = h3_event.headers\n+ if h3_event.stream_ended:\n+ await stream._add_input(b\"\", True)\n+ elif isinstance(h3_event, aioquic.h3.events.DataReceived):\n+ stream = self._streams.get(event.stream_id)\n+ if stream:\n+ await stream._add_input(\n+ h3_event.data, h3_event.stream_ended\n+ )\n+ else:\n+ stream = self._streams.get(event.stream_id)\n+ if stream:\n+ await stream._add_input(event.data, event.end_stream)\n elif isinstance(event, aioquic.quic.events.HandshakeCompleted):\n self._handshake_complete.set()\n elif isinstance(event, aioquic.quic.events.ConnectionTerminated):\n@@ -161,8 +200,7 @@ async def _handle_events(self):\n \n async def write(self, stream, data, is_end=False):\n self._connection.send_stream_data(stream, data, is_end)\n- async with self._wake_timer:\n- self._wake_timer.notify_all()\n+ await self._wakeup()\n \n def run(self):\n if self._closed:\n@@ -189,8 +227,7 @@ async def close(self):\n self._connection.close()\n # sender might be blocked on this, so set it\n self._socket_created.set()\n- async with self._wake_timer:\n- self._wake_timer.notify_all()\n+ await self._wakeup()\n try:\n await self._receiver_task\n except asyncio.CancelledError:\n@@ -203,8 +240,10 @@ async def close(self):\n \n \n class AsyncioQuicManager(AsyncQuicManager):\n- def __init__(self, conf=None, verify_mode=ssl.CERT_REQUIRED, server_name=None):\n- super().__init__(conf, verify_mode, AsyncioQuicConnection, server_name)\n+ def __init__(\n+ self, conf=None, verify_mode=ssl.CERT_REQUIRED, server_name=None, h3=False\n+ ):\n+ super().__init__(conf, verify_mode, AsyncioQuicConnection, server_name, h3)\n \n def connect(\n self, address, port=853, source=None, source_port=0, want_session_ticket=True"}, {"sha": "ce575b038959e56380c3d33e53bb778fb5b5684d", "filename": "lib/dns/quic/_common.py", "status": "modified", "additions": 121, "deletions": 6, "changes": 127, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fquic%2F_common.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fquic%2F_common.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fquic%2F_common.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,12 +1,16 @@\n # Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n \n+import base64\n import copy\n import functools\n import socket\n import struct\n import time\n+import urllib\n from typing import Any, Optional\n \n+import aioquic.h3.connection # type: ignore\n+import aioquic.h3.events # type: ignore\n import aioquic.quic.configuration # type: ignore\n import aioquic.quic.connection # type: ignore\n \n@@ -51,17 +55,31 @@ def get(self, amount):\n self._buffer = self._buffer[amount:]\n return data\n \n+ def get_all(self):\n+ assert self.seen_end()\n+ data = self._buffer\n+ self._buffer = b\"\"\n+ return data\n+\n \n class BaseQuicStream:\n def __init__(self, connection, stream_id):\n self._connection = connection\n self._stream_id = stream_id\n self._buffer = Buffer()\n self._expecting = 0\n+ self._headers = None\n+ self._trailers = None\n \n def id(self):\n return self._stream_id\n \n+ def headers(self):\n+ return self._headers\n+\n+ def trailers(self):\n+ return self._trailers\n+\n def _expiration_from_timeout(self, timeout):\n if timeout is not None:\n expiration = time.time() + timeout\n@@ -77,16 +95,51 @@ def _timeout_from_expiration(self, expiration):\n return timeout\n \n # Subclass must implement receive() as sync / async and which returns a message\n- # or raises UnexpectedEOF.\n+ # or raises.\n+\n+ # Subclass must implement send() as sync / async and which takes a message and\n+ # an EOF indicator.\n+\n+ def send_h3(self, url, datagram, post=True):\n+ if not self._connection.is_h3():\n+ raise SyntaxError(\"cannot send H3 to a non-H3 connection\")\n+ url_parts = urllib.parse.urlparse(url)\n+ path = url_parts.path.encode()\n+ if post:\n+ method = b\"POST\"\n+ else:\n+ method = b\"GET\"\n+ path += b\"?dns=\" + base64.urlsafe_b64encode(datagram).rstrip(b\"=\")\n+ headers = [\n+ (b\":method\", method),\n+ (b\":scheme\", url_parts.scheme.encode()),\n+ (b\":authority\", url_parts.netloc.encode()),\n+ (b\":path\", path),\n+ (b\"accept\", b\"application/dns-message\"),\n+ ]\n+ if post:\n+ headers.extend(\n+ [\n+ (b\"content-type\", b\"application/dns-message\"),\n+ (b\"content-length\", str(len(datagram)).encode()),\n+ ]\n+ )\n+ self._connection.send_headers(self._stream_id, headers, not post)\n+ if post:\n+ self._connection.send_data(self._stream_id, datagram, True)\n \n def _encapsulate(self, datagram):\n+ if self._connection.is_h3():\n+ return datagram\n l = len(datagram)\n return struct.pack(\"!H\", l) + datagram\n \n def _common_add_input(self, data, is_end):\n self._buffer.put(data, is_end)\n try:\n- return self._expecting > 0 and self._buffer.have(self._expecting)\n+ return (\n+ self._expecting > 0 and self._buffer.have(self._expecting)\n+ ) or self._buffer.seen_end\n except UnexpectedEOF:\n return True\n \n@@ -97,7 +150,13 @@ def _close(self):\n \n class BaseQuicConnection:\n def __init__(\n- self, connection, address, port, source=None, source_port=0, manager=None\n+ self,\n+ connection,\n+ address,\n+ port,\n+ source=None,\n+ source_port=0,\n+ manager=None,\n ):\n self._done = False\n self._connection = connection\n@@ -106,6 +165,10 @@ def __init__(\n self._closed = False\n self._manager = manager\n self._streams = {}\n+ if manager.is_h3():\n+ self._h3_conn = aioquic.h3.connection.H3Connection(connection, False)\n+ else:\n+ self._h3_conn = None\n self._af = dns.inet.af_for_address(address)\n self._peer = dns.inet.low_level_address_tuple((address, port))\n if source is None and source_port != 0:\n@@ -120,9 +183,18 @@ def __init__(\n else:\n self._source = None\n \n+ def is_h3(self):\n+ return self._h3_conn is not None\n+\n def close_stream(self, stream_id):\n del self._streams[stream_id]\n \n+ def send_headers(self, stream_id, headers, is_end=False):\n+ self._h3_conn.send_headers(stream_id, headers, is_end)\n+\n+ def send_data(self, stream_id, data, is_end=False):\n+ self._h3_conn.send_data(stream_id, data, is_end)\n+\n def _get_timer_values(self, closed_is_special=True):\n now = time.time()\n expiration = self._connection.get_timer()\n@@ -148,17 +220,25 @@ async def make_stream(self, timeout: Optional[float] = None) -> Any:\n \n \n class BaseQuicManager:\n- def __init__(self, conf, verify_mode, connection_factory, server_name=None):\n+ def __init__(\n+ self, conf, verify_mode, connection_factory, server_name=None, h3=False\n+ ):\n self._connections = {}\n self._connection_factory = connection_factory\n self._session_tickets = {}\n+ self._tokens = {}\n+ self._h3 = h3\n if conf is None:\n verify_path = None\n if isinstance(verify_mode, str):\n verify_path = verify_mode\n verify_mode = True\n+ if h3:\n+ alpn_protocols = [\"h3\"]\n+ else:\n+ alpn_protocols = [\"doq\", \"doq-i03\"]\n conf = aioquic.quic.configuration.QuicConfiguration(\n- alpn_protocols=[\"doq\", \"doq-i03\"],\n+ alpn_protocols=alpn_protocols,\n verify_mode=verify_mode,\n server_name=server_name,\n )\n@@ -167,7 +247,13 @@ def __init__(self, conf, verify_mode, connection_factory, server_name=None):\n self._conf = conf\n \n def _connect(\n- self, address, port=853, source=None, source_port=0, want_session_ticket=True\n+ self,\n+ address,\n+ port=853,\n+ source=None,\n+ source_port=0,\n+ want_session_ticket=True,\n+ want_token=True,\n ):\n connection = self._connections.get((address, port))\n if connection is not None:\n@@ -189,9 +275,24 @@ def _connect(\n )\n else:\n session_ticket_handler = None\n+ if want_token:\n+ try:\n+ token = self._tokens.pop((address, port))\n+ # We found a token, so make a configuration that uses it.\n+ conf = copy.copy(conf)\n+ conf.token = token\n+ except KeyError:\n+ # No token\n+ pass\n+ # Whether or not we found a token, we want a handler to save # one.\n+ token_handler = functools.partial(self.save_token, address, port)\n+ else:\n+ token_handler = None\n+\n qconn = aioquic.quic.connection.QuicConnection(\n configuration=conf,\n session_ticket_handler=session_ticket_handler,\n+ token_handler=token_handler,\n )\n lladdress = dns.inet.low_level_address_tuple((address, port))\n qconn.connect(lladdress, time.time())\n@@ -207,6 +308,9 @@ def closed(self, address, port):\n except KeyError:\n pass\n \n+ def is_h3(self):\n+ return self._h3\n+\n def save_session_ticket(self, address, port, ticket):\n # We rely on dictionaries keys() being in insertion order here. We\n # can't just popitem() as that would be LIFO which is the opposite of\n@@ -218,6 +322,17 @@ def save_session_ticket(self, address, port, ticket):\n del self._session_tickets[key]\n self._session_tickets[(address, port)] = ticket\n \n+ def save_token(self, address, port, token):\n+ # We rely on dictionaries keys() being in insertion order here. We\n+ # can't just popitem() as that would be LIFO which is the opposite of\n+ # what we want.\n+ l = len(self._tokens)\n+ if l >= MAX_SESSION_TICKETS:\n+ keys_to_delete = list(self._tokens.keys())[0:SESSIONS_TO_DELETE]\n+ for key in keys_to_delete:\n+ del self._tokens[key]\n+ self._tokens[(address, port)] = token\n+\n \n class AsyncQuicManager(BaseQuicManager):\n def connect(self, address, port=853, source=None, source_port=0):"}, {"sha": "473d1f4811e4a11dc67ac88acec282bb68ddb805", "filename": "lib/dns/quic/_sync.py", "status": "modified", "additions": 78, "deletions": 21, "changes": 99, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fquic%2F_sync.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fquic%2F_sync.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fquic%2F_sync.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -21,11 +21,9 @@\n UnexpectedEOF,\n )\n \n-# Avoid circularity with dns.query\n-if hasattr(selectors, \"PollSelector\"):\n- _selector_class = selectors.PollSelector # type: ignore\n-else:\n- _selector_class = selectors.SelectSelector # type: ignore\n+# Function used to create a socket. Can be overridden if needed in special\n+# situations.\n+socket_factory = socket.socket\n \n \n class SyncQuicStream(BaseQuicStream):\n@@ -46,14 +44,29 @@ def wait_for(self, amount, expiration):\n raise dns.exception.Timeout\n self._expecting = 0\n \n+ def wait_for_end(self, expiration):\n+ while True:\n+ timeout = self._timeout_from_expiration(expiration)\n+ with self._lock:\n+ if self._buffer.seen_end():\n+ return\n+ with self._wake_up:\n+ if not self._wake_up.wait(timeout):\n+ raise dns.exception.Timeout\n+\n def receive(self, timeout=None):\n expiration = self._expiration_from_timeout(timeout)\n- self.wait_for(2, expiration)\n- with self._lock:\n- (size,) = struct.unpack(\"!H\", self._buffer.get(2))\n- self.wait_for(size, expiration)\n- with self._lock:\n- return self._buffer.get(size)\n+ if self._connection.is_h3():\n+ self.wait_for_end(expiration)\n+ with self._lock:\n+ return self._buffer.get_all()\n+ else:\n+ self.wait_for(2, expiration)\n+ with self._lock:\n+ (size,) = struct.unpack(\"!H\", self._buffer.get(2))\n+ self.wait_for(size, expiration)\n+ with self._lock:\n+ return self._buffer.get(size)\n \n def send(self, datagram, is_end=False):\n data = self._encapsulate(datagram)\n@@ -81,7 +94,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):\n class SyncQuicConnection(BaseQuicConnection):\n def __init__(self, connection, address, port, source, source_port, manager):\n super().__init__(connection, address, port, source, source_port, manager)\n- self._socket = socket.socket(self._af, socket.SOCK_DGRAM, 0)\n+ self._socket = socket_factory(self._af, socket.SOCK_DGRAM, 0)\n if self._source is not None:\n try:\n self._socket.bind(\n@@ -118,7 +131,7 @@ def _drain_wakeup(self):\n \n def _worker(self):\n try:\n- sel = _selector_class()\n+ sel = selectors.DefaultSelector()\n sel.register(self._socket, selectors.EVENT_READ, self._read)\n sel.register(self._receive_wakeup, selectors.EVENT_READ, self._drain_wakeup)\n while not self._done:\n@@ -140,6 +153,7 @@ def _worker(self):\n finally:\n with self._lock:\n self._done = True\n+ self._socket.close()\n # Ensure anyone waiting for this gets woken up.\n self._handshake_complete.set()\n \n@@ -150,10 +164,29 @@ def _handle_events(self):\n if event is None:\n return\n if isinstance(event, aioquic.quic.events.StreamDataReceived):\n- with self._lock:\n- stream = self._streams.get(event.stream_id)\n- if stream:\n- stream._add_input(event.data, event.end_stream)\n+ if self.is_h3():\n+ h3_events = self._h3_conn.handle_event(event)\n+ for h3_event in h3_events:\n+ if isinstance(h3_event, aioquic.h3.events.HeadersReceived):\n+ with self._lock:\n+ stream = self._streams.get(event.stream_id)\n+ if stream:\n+ if stream._headers is None:\n+ stream._headers = h3_event.headers\n+ elif stream._trailers is None:\n+ stream._trailers = h3_event.headers\n+ if h3_event.stream_ended:\n+ stream._add_input(b\"\", True)\n+ elif isinstance(h3_event, aioquic.h3.events.DataReceived):\n+ with self._lock:\n+ stream = self._streams.get(event.stream_id)\n+ if stream:\n+ stream._add_input(h3_event.data, h3_event.stream_ended)\n+ else:\n+ with self._lock:\n+ stream = self._streams.get(event.stream_id)\n+ if stream:\n+ stream._add_input(event.data, event.end_stream)\n elif isinstance(event, aioquic.quic.events.HandshakeCompleted):\n self._handshake_complete.set()\n elif isinstance(event, aioquic.quic.events.ConnectionTerminated):\n@@ -170,6 +203,18 @@ def write(self, stream, data, is_end=False):\n self._connection.send_stream_data(stream, data, is_end)\n self._send_wakeup.send(b\"\\x01\")\n \n+ def send_headers(self, stream_id, headers, is_end=False):\n+ with self._lock:\n+ super().send_headers(stream_id, headers, is_end)\n+ if is_end:\n+ self._send_wakeup.send(b\"\\x01\")\n+\n+ def send_data(self, stream_id, data, is_end=False):\n+ with self._lock:\n+ super().send_data(stream_id, data, is_end)\n+ if is_end:\n+ self._send_wakeup.send(b\"\\x01\")\n+\n def run(self):\n if self._closed:\n return\n@@ -203,16 +248,24 @@ def close(self):\n \n \n class SyncQuicManager(BaseQuicManager):\n- def __init__(self, conf=None, verify_mode=ssl.CERT_REQUIRED, server_name=None):\n- super().__init__(conf, verify_mode, SyncQuicConnection, server_name)\n+ def __init__(\n+ self, conf=None, verify_mode=ssl.CERT_REQUIRED, server_name=None, h3=False\n+ ):\n+ super().__init__(conf, verify_mode, SyncQuicConnection, server_name, h3)\n self._lock = threading.Lock()\n \n def connect(\n- self, address, port=853, source=None, source_port=0, want_session_ticket=True\n+ self,\n+ address,\n+ port=853,\n+ source=None,\n+ source_port=0,\n+ want_session_ticket=True,\n+ want_token=True,\n ):\n with self._lock:\n (connection, start) = self._connect(\n- address, port, source, source_port, want_session_ticket\n+ address, port, source, source_port, want_session_ticket, want_token\n )\n if start:\n connection.run()\n@@ -226,6 +279,10 @@ def save_session_ticket(self, address, port, ticket):\n with self._lock:\n super().save_session_ticket(address, port, ticket)\n \n+ def save_token(self, address, port, token):\n+ with self._lock:\n+ super().save_token(address, port, token)\n+\n def __enter__(self):\n return self\n "}, {"sha": "ae79f36957c20aed90f785720d9a767c85a9825c", "filename": "lib/dns/quic/_trio.py", "status": "modified", "additions": 45, "deletions": 9, "changes": 54, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fquic%2F_trio.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fquic%2F_trio.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fquic%2F_trio.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -36,16 +36,27 @@ async def wait_for(self, amount):\n await self._wake_up.wait()\n self._expecting = 0\n \n+ async def wait_for_end(self):\n+ while True:\n+ if self._buffer.seen_end():\n+ return\n+ async with self._wake_up:\n+ await self._wake_up.wait()\n+\n async def receive(self, timeout=None):\n if timeout is None:\n context = NullContext(None)\n else:\n context = trio.move_on_after(timeout)\n with context:\n- await self.wait_for(2)\n- (size,) = struct.unpack(\"!H\", self._buffer.get(2))\n- await self.wait_for(size)\n- return self._buffer.get(size)\n+ if self._connection.is_h3():\n+ await self.wait_for_end()\n+ return self._buffer.get_all()\n+ else:\n+ await self.wait_for(2)\n+ (size,) = struct.unpack(\"!H\", self._buffer.get(2))\n+ await self.wait_for(size)\n+ return self._buffer.get(size)\n raise dns.exception.Timeout\n \n async def send(self, datagram, is_end=False):\n@@ -115,6 +126,7 @@ async def _worker(self):\n await self._socket.send(datagram)\n finally:\n self._done = True\n+ self._socket.close()\n self._handshake_complete.set()\n \n async def _handle_events(self):\n@@ -124,9 +136,28 @@ async def _handle_events(self):\n if event is None:\n return\n if isinstance(event, aioquic.quic.events.StreamDataReceived):\n- stream = self._streams.get(event.stream_id)\n- if stream:\n- await stream._add_input(event.data, event.end_stream)\n+ if self.is_h3():\n+ h3_events = self._h3_conn.handle_event(event)\n+ for h3_event in h3_events:\n+ if isinstance(h3_event, aioquic.h3.events.HeadersReceived):\n+ stream = self._streams.get(event.stream_id)\n+ if stream:\n+ if stream._headers is None:\n+ stream._headers = h3_event.headers\n+ elif stream._trailers is None:\n+ stream._trailers = h3_event.headers\n+ if h3_event.stream_ended:\n+ await stream._add_input(b\"\", True)\n+ elif isinstance(h3_event, aioquic.h3.events.DataReceived):\n+ stream = self._streams.get(event.stream_id)\n+ if stream:\n+ await stream._add_input(\n+ h3_event.data, h3_event.stream_ended\n+ )\n+ else:\n+ stream = self._streams.get(event.stream_id)\n+ if stream:\n+ await stream._add_input(event.data, event.end_stream)\n elif isinstance(event, aioquic.quic.events.HandshakeCompleted):\n self._handshake_complete.set()\n elif isinstance(event, aioquic.quic.events.ConnectionTerminated):\n@@ -183,9 +214,14 @@ async def close(self):\n \n class TrioQuicManager(AsyncQuicManager):\n def __init__(\n- self, nursery, conf=None, verify_mode=ssl.CERT_REQUIRED, server_name=None\n+ self,\n+ nursery,\n+ conf=None,\n+ verify_mode=ssl.CERT_REQUIRED,\n+ server_name=None,\n+ h3=False,\n ):\n- super().__init__(conf, verify_mode, TrioQuicConnection, server_name)\n+ super().__init__(conf, verify_mode, TrioQuicConnection, server_name, h3)\n self._nursery = nursery\n \n def connect("}, {"sha": "8099c26aab9029b454a8709575d6f1fd7aba108d", "filename": "lib/dns/rdata.py", "status": "modified", "additions": 42, "deletions": 15, "changes": 57, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdata.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdata.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Frdata.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -214,7 +214,7 @@ def _to_wire(\n compress: Optional[dns.name.CompressType] = None,\n origin: Optional[dns.name.Name] = None,\n canonicalize: bool = False,\n- ) -> bytes:\n+ ) -> None:\n raise NotImplementedError # pragma: no cover\n \n def to_wire(\n@@ -223,14 +223,19 @@ def to_wire(\n compress: Optional[dns.name.CompressType] = None,\n origin: Optional[dns.name.Name] = None,\n canonicalize: bool = False,\n- ) -> bytes:\n+ ) -> Optional[bytes]:\n \"\"\"Convert an rdata to wire format.\n \n- Returns a ``bytes`` or ``None``.\n+ Returns a ``bytes`` if no output file was specified, or ``None`` otherwise.\n \"\"\"\n \n if file:\n- return self._to_wire(file, compress, origin, canonicalize)\n+ # We call _to_wire() and then return None explicitly instead of\n+ # of just returning the None from _to_wire() as mypy's func-returns-value\n+ # unhelpfully errors out with \"error: \"_to_wire\" of \"Rdata\" does not return\n+ # a value (it only ever returns None)\"\n+ self._to_wire(file, compress, origin, canonicalize)\n+ return None\n else:\n f = io.BytesIO()\n self._to_wire(f, compress, origin, canonicalize)\n@@ -253,8 +258,9 @@ def to_digestable(self, origin: Optional[dns.name.Name] = None) -> bytes:\n \n Returns a ``bytes``.\n \"\"\"\n-\n- return self.to_wire(origin=origin, canonicalize=True)\n+ wire = self.to_wire(origin=origin, canonicalize=True)\n+ assert wire is not None # for mypy\n+ return wire\n \n def __repr__(self):\n covers = self.covers()\n@@ -434,15 +440,11 @@ def replace(self, **kwargs: Any) -> \"Rdata\":\n continue\n if key not in parameters:\n raise AttributeError(\n- \"'{}' object has no attribute '{}'\".format(\n- self.__class__.__name__, key\n- )\n+ f\"'{self.__class__.__name__}' object has no attribute '{key}'\"\n )\n if key in (\"rdclass\", \"rdtype\"):\n raise AttributeError(\n- \"Cannot overwrite '{}' attribute '{}'\".format(\n- self.__class__.__name__, key\n- )\n+ f\"Cannot overwrite '{self.__class__.__name__}' attribute '{key}'\"\n )\n \n # Construct the parameter list. For each field, use the value in\n@@ -646,13 +648,14 @@ def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):\n {}\n )\n _module_prefix = \"dns.rdtypes\"\n+_dynamic_load_allowed = True\n \n \n-def get_rdata_class(rdclass, rdtype):\n+def get_rdata_class(rdclass, rdtype, use_generic=True):\n cls = _rdata_classes.get((rdclass, rdtype))\n if not cls:\n cls = _rdata_classes.get((dns.rdatatype.ANY, rdtype))\n- if not cls:\n+ if not cls and _dynamic_load_allowed:\n rdclass_text = dns.rdataclass.to_text(rdclass)\n rdtype_text = dns.rdatatype.to_text(rdtype)\n rdtype_text = rdtype_text.replace(\"-\", \"_\")\n@@ -670,12 +673,36 @@ def get_rdata_class(rdclass, rdtype):\n _rdata_classes[(rdclass, rdtype)] = cls\n except ImportError:\n pass\n- if not cls:\n+ if not cls and use_generic:\n cls = GenericRdata\n _rdata_classes[(rdclass, rdtype)] = cls\n return cls\n \n \n+def load_all_types(disable_dynamic_load=True):\n+ \"\"\"Load all rdata types for which dnspython has a non-generic implementation.\n+\n+ Normally dnspython loads DNS rdatatype implementations on demand, but in some\n+ specialized cases loading all types at an application-controlled time is preferred.\n+\n+ If *disable_dynamic_load*, a ``bool``, is ``True`` then dnspython will not attempt\n+ to use its dynamic loading mechanism if an unknown type is subsequently encountered,\n+ and will simply use the ``GenericRdata`` class.\n+ \"\"\"\n+ # Load class IN and ANY types.\n+ for rdtype in dns.rdatatype.RdataType:\n+ get_rdata_class(dns.rdataclass.IN, rdtype, False)\n+ # Load the one non-ANY implementation we have in CH. Everything\n+ # else in CH is an ANY type, and we'll discover those on demand but won't\n+ # have to import anything.\n+ get_rdata_class(dns.rdataclass.CH, dns.rdatatype.A, False)\n+ if disable_dynamic_load:\n+ # Now disable dynamic loading so any subsequent unknown type immediately becomes\n+ # GenericRdata without a load attempt.\n+ global _dynamic_load_allowed\n+ _dynamic_load_allowed = False\n+\n+\n def from_text(\n rdclass: Union[dns.rdataclass.RdataClass, str],\n rdtype: Union[dns.rdatatype.RdataType, str],"}, {"sha": "39cab2365aee8757615cfe0113c53857cf29d184", "filename": "lib/dns/rdataset.py", "status": "modified", "additions": 3, "deletions": 7, "changes": 10, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdataset.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdataset.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Frdataset.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -160,7 +160,7 @@ def maybe_truncate(s):\n return s[:100] + \"...\"\n return s\n \n- return \"[%s]\" % \", \".join(\"<%s>\" % maybe_truncate(str(rr)) for rr in self)\n+ return \"[\" + \", \".join(f\"<{maybe_truncate(str(rr))}>\" for rr in self) + \"]\"\n \n def __repr__(self):\n if self.covers == 0:\n@@ -248,12 +248,8 @@ def to_text(\n # (which is meaningless anyway).\n #\n s.write(\n- \"{}{}{} {}\\n\".format(\n- ntext,\n- pad,\n- dns.rdataclass.to_text(rdclass),\n- dns.rdatatype.to_text(self.rdtype),\n- )\n+ f\"{ntext}{pad}{dns.rdataclass.to_text(rdclass)} \"\n+ f\"{dns.rdatatype.to_text(self.rdtype)}\\n\"\n )\n else:\n for rd in self:"}, {"sha": "aa9e561c2209d4fa9834c5a7d18666278168cfe0", "filename": "lib/dns/rdatatype.py", "status": "modified", "additions": 5, "deletions": 1, "changes": 6, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdatatype.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdatatype.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Frdatatype.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -105,6 +105,8 @@ class RdataType(dns.enum.IntEnum):\n CAA = 257\n AVC = 258\n AMTRELAY = 260\n+ RESINFO = 261\n+ WALLET = 262\n TA = 32768\n DLV = 32769\n \n@@ -125,7 +127,7 @@ def _extra_from_text(cls, text):\n if text.find(\"-\") >= 0:\n try:\n return cls[text.replace(\"-\", \"_\")]\n- except KeyError:\n+ except KeyError: # pragma: no cover\n pass\n return _registered_by_text.get(text)\n \n@@ -326,6 +328,8 @@ def register_type(\n CAA = RdataType.CAA\n AVC = RdataType.AVC\n AMTRELAY = RdataType.AMTRELAY\n+RESINFO = RdataType.RESINFO\n+WALLET = RdataType.WALLET\n TA = RdataType.TA\n DLV = RdataType.DLV\n "}, {"sha": "d79f4a0669bb5a6afda6e8621c290d45a932a2d9", "filename": "lib/dns/rdtypes/ANY/GPOS.py", "status": "modified", "additions": 3, "deletions": 2, "changes": 5, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FANY%2FGPOS.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FANY%2FGPOS.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Frdtypes%2FANY%2FGPOS.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -75,8 +75,9 @@ def __init__(self, rdclass, rdtype, latitude, longitude, altitude):\n raise dns.exception.FormError(\"bad longitude\")\n \n def to_text(self, origin=None, relativize=True, **kw):\n- return \"{} {} {}\".format(\n- self.latitude.decode(), self.longitude.decode(), self.altitude.decode()\n+ return (\n+ f\"{self.latitude.decode()} {self.longitude.decode()} \"\n+ f\"{self.altitude.decode()}\"\n )\n \n @classmethod"}, {"sha": "06ad3487cf8c37a48f4ab53240986287798b9e79", "filename": "lib/dns/rdtypes/ANY/HINFO.py", "status": "modified", "additions": 1, "deletions": 3, "changes": 4, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FANY%2FHINFO.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FANY%2FHINFO.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Frdtypes%2FANY%2FHINFO.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -37,9 +37,7 @@ def __init__(self, rdclass, rdtype, cpu, os):\n self.os = self._as_bytes(os, True, 255)\n \n def to_text(self, origin=None, relativize=True, **kw):\n- return '\"{}\" \"{}\"'.format(\n- dns.rdata._escapify(self.cpu), dns.rdata._escapify(self.os)\n- )\n+ return f'\"{dns.rdata._escapify(self.cpu)}\" \"{dns.rdata._escapify(self.os)}\"'\n \n @classmethod\n def from_text("}, {"sha": "f3157da743234f9c42e87b931a0345f860156147", "filename": "lib/dns/rdtypes/ANY/HIP.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FANY%2FHIP.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FANY%2FHIP.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Frdtypes%2FANY%2FHIP.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -48,7 +48,7 @@ def to_text(self, origin=None, relativize=True, **kw):\n for server in self.servers:\n servers.append(server.choose_relativity(origin, relativize))\n if len(servers) > 0:\n- text += \" \" + \" \".join((x.to_unicode() for x in servers))\n+ text += \" \" + \" \".join(x.to_unicode() for x in servers)\n return \"%u %s %s%s\" % (self.algorithm, hit, key, text)\n \n @classmethod"}, {"sha": "6428a0a822f1808f3997a3253d24b815e56a21ec", "filename": "lib/dns/rdtypes/ANY/ISDN.py", "status": "modified", "additions": 4, "deletions": 3, "changes": 7, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FANY%2FISDN.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FANY%2FISDN.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Frdtypes%2FANY%2FISDN.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -38,11 +38,12 @@ def __init__(self, rdclass, rdtype, address, subaddress):\n \n def to_text(self, origin=None, relativize=True, **kw):\n if self.subaddress:\n- return '\"{}\" \"{}\"'.format(\n- dns.rdata._escapify(self.address), dns.rdata._escapify(self.subaddress)\n+ return (\n+ f'\"{dns.rdata._escapify(self.address)}\" '\n+ f'\"{dns.rdata._escapify(self.subaddress)}\"'\n )\n else:\n- return '\"%s\"' % dns.rdata._escapify(self.address)\n+ return f'\"{dns.rdata._escapify(self.address)}\"'\n \n @classmethod\n def from_text("}, {"sha": "1153cf0399b39e150802b3f83a49963f66f41cc9", "filename": "lib/dns/rdtypes/ANY/LOC.py", "status": "modified", "additions": 6, "deletions": 7, "changes": 13, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FANY%2FLOC.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FANY%2FLOC.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Frdtypes%2FANY%2FLOC.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -44,7 +44,7 @@ def _exponent_of(what, desc):\n exp = i - 1\n break\n if exp is None or exp < 0:\n- raise dns.exception.SyntaxError(\"%s value out of bounds\" % desc)\n+ raise dns.exception.SyntaxError(f\"{desc} value out of bounds\")\n return exp\n \n \n@@ -83,10 +83,10 @@ def _encode_size(what, desc):\n def _decode_size(what, desc):\n exponent = what & 0x0F\n if exponent > 9:\n- raise dns.exception.FormError(\"bad %s exponent\" % desc)\n+ raise dns.exception.FormError(f\"bad {desc} exponent\")\n base = (what & 0xF0) >> 4\n if base > 9:\n- raise dns.exception.FormError(\"bad %s base\" % desc)\n+ raise dns.exception.FormError(f\"bad {desc} base\")\n return base * pow(10, exponent)\n \n \n@@ -184,10 +184,9 @@ def to_text(self, origin=None, relativize=True, **kw):\n or self.horizontal_precision != _default_hprec\n or self.vertical_precision != _default_vprec\n ):\n- text += \" {:0.2f}m {:0.2f}m {:0.2f}m\".format(\n- self.size / 100.0,\n- self.horizontal_precision / 100.0,\n- self.vertical_precision / 100.0,\n+ text += (\n+ f\" {self.size / 100.0:0.2f}m {self.horizontal_precision / 100.0:0.2f}m\"\n+ f\" {self.vertical_precision / 100.0:0.2f}m\"\n )\n return text\n "}, {"sha": "3c78b72288b75a65b7fe3eaaaf7177d296f33b5d", "filename": "lib/dns/rdtypes/ANY/NSEC.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FANY%2FNSEC.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FANY%2FNSEC.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Frdtypes%2FANY%2FNSEC.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -44,7 +44,7 @@ def __init__(self, rdclass, rdtype, next, windows):\n def to_text(self, origin=None, relativize=True, **kw):\n next = self.next.choose_relativity(origin, relativize)\n text = Bitmap(self.windows).to_text()\n- return \"{}{}\".format(next, text)\n+ return f\"{next}{text}\"\n \n @classmethod\n def from_text("}, {"sha": "76c8ea2ac47e8af24e927ec6ce05b113e33404c3", "filename": "lib/dns/rdtypes/ANY/RESINFO.py", "status": "added", "additions": 24, "deletions": 0, "changes": 24, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FANY%2FRESINFO.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FANY%2FRESINFO.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Frdtypes%2FANY%2FRESINFO.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -0,0 +1,24 @@\n+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n+\n+# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.\n+#\n+# Permission to use, copy, modify, and distribute this software and its\n+# documentation for any purpose with or without fee is hereby granted,\n+# provided that the above copyright notice and this permission notice\n+# appear in all copies.\n+#\n+# THE SOFTWARE IS PROVIDED \"AS IS\" AND NOMINUM DISCLAIMS ALL WARRANTIES\n+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR\n+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\n+# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n+\n+import dns.immutable\n+import dns.rdtypes.txtbase\n+\n+\n+@dns.immutable.immutable\n+class RESINFO(dns.rdtypes.txtbase.TXTBase):\n+ \"\"\"RESINFO record\"\"\""}, {"sha": "a66cfc50f5e9854f556a727544ee0114f887b45d", "filename": "lib/dns/rdtypes/ANY/RP.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FANY%2FRP.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FANY%2FRP.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Frdtypes%2FANY%2FRP.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -37,7 +37,7 @@ def __init__(self, rdclass, rdtype, mbox, txt):\n def to_text(self, origin=None, relativize=True, **kw):\n mbox = self.mbox.choose_relativity(origin, relativize)\n txt = self.txt.choose_relativity(origin, relativize)\n- return \"{} {}\".format(str(mbox), str(txt))\n+ return f\"{str(mbox)} {str(txt)}\"\n \n @classmethod\n def from_text("}, {"sha": "75f62249e1daf7961370349752215fd367ed8b29", "filename": "lib/dns/rdtypes/ANY/TKEY.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FANY%2FTKEY.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FANY%2FTKEY.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Frdtypes%2FANY%2FTKEY.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -69,7 +69,7 @@ def to_text(self, origin=None, relativize=True, **kw):\n dns.rdata._base64ify(self.key, 0),\n )\n if len(self.other) > 0:\n- text += \" %s\" % (dns.rdata._base64ify(self.other, 0))\n+ text += f\" {dns.rdata._base64ify(self.other, 0)}\"\n \n return text\n "}, {"sha": "ff4647632bdf1418d515c58afb37e8a071ef042a", "filename": "lib/dns/rdtypes/ANY/WALLET.py", "status": "added", "additions": 9, "deletions": 0, "changes": 9, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FANY%2FWALLET.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FANY%2FWALLET.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Frdtypes%2FANY%2FWALLET.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -0,0 +1,9 @@\n+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license\n+\n+import dns.immutable\n+import dns.rdtypes.txtbase\n+\n+\n+@dns.immutable.immutable\n+class WALLET(dns.rdtypes.txtbase.TXTBase):\n+ \"\"\"WALLET record\"\"\""}, {"sha": "2436ddb62ec23d52a69c092e1194b599618681f8", "filename": "lib/dns/rdtypes/ANY/X25.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FANY%2FX25.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FANY%2FX25.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Frdtypes%2FANY%2FX25.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -36,7 +36,7 @@ def __init__(self, rdclass, rdtype, address):\n self.address = self._as_bytes(address, True, 255)\n \n def to_text(self, origin=None, relativize=True, **kw):\n- return '\"%s\"' % dns.rdata._escapify(self.address)\n+ return f'\"{dns.rdata._escapify(self.address)}\"'\n \n @classmethod\n def from_text("}, {"sha": "647b215bc9a08cecb22c026464e31623edcb0df8", "filename": "lib/dns/rdtypes/ANY/__init__.py", "status": "modified", "additions": 2, "deletions": 0, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FANY%2F__init__.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FANY%2F__init__.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Frdtypes%2FANY%2F__init__.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -51,6 +51,7 @@\n \"OPENPGPKEY\",\n \"OPT\",\n \"PTR\",\n+ \"RESINFO\",\n \"RP\",\n \"RRSIG\",\n \"RT\",\n@@ -63,6 +64,7 @@\n \"TSIG\",\n \"TXT\",\n \"URI\",\n+ \"WALLET\",\n \"X25\",\n \"ZONEMD\",\n ]"}, {"sha": "832e8d3a5b86c3badce75c98833636b1f2ef4ba7", "filename": "lib/dns/rdtypes/CH/A.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FCH%2FA.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FCH%2FA.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Frdtypes%2FCH%2FA.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -37,7 +37,7 @@ def __init__(self, rdclass, rdtype, domain, address):\n \n def to_text(self, origin=None, relativize=True, **kw):\n domain = self.domain.choose_relativity(origin, relativize)\n- return \"%s %o\" % (domain, self.address)\n+ return f\"{domain} {self.address:o}\"\n \n @classmethod\n def from_text("}, {"sha": "d55edb7372afb143c72665775a3755a202969fc7", "filename": "lib/dns/rdtypes/IN/NSAP.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FIN%2FNSAP.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2FIN%2FNSAP.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Frdtypes%2FIN%2FNSAP.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -36,7 +36,7 @@ def __init__(self, rdclass, rdtype, address):\n self.address = self._as_bytes(address)\n \n def to_text(self, origin=None, relativize=True, **kw):\n- return \"0x%s\" % binascii.hexlify(self.address).decode()\n+ return f\"0x{binascii.hexlify(self.address).decode()}\"\n \n @classmethod\n def from_text("}, {"sha": "a39c166b98fe2973fc64835c1209c12417535079", "filename": "lib/dns/rdtypes/euibase.py", "status": "modified", "additions": 4, "deletions": 4, "changes": 8, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2Feuibase.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2Feuibase.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Frdtypes%2Feuibase.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -36,7 +36,7 @@ def __init__(self, rdclass, rdtype, eui):\n self.eui = self._as_bytes(eui)\n if len(self.eui) != self.byte_len:\n raise dns.exception.FormError(\n- \"EUI%s rdata has to have %s bytes\" % (self.byte_len * 8, self.byte_len)\n+ f\"EUI{self.byte_len * 8} rdata has to have {self.byte_len} bytes\"\n )\n \n def to_text(self, origin=None, relativize=True, **kw):\n@@ -49,16 +49,16 @@ def from_text(\n text = tok.get_string()\n if len(text) != cls.text_len:\n raise dns.exception.SyntaxError(\n- \"Input text must have %s characters\" % cls.text_len\n+ f\"Input text must have {cls.text_len} characters\"\n )\n for i in range(2, cls.byte_len * 3 - 1, 3):\n if text[i] != \"-\":\n- raise dns.exception.SyntaxError(\"Dash expected at position %s\" % i)\n+ raise dns.exception.SyntaxError(f\"Dash expected at position {i}\")\n text = text.replace(\"-\", \"\")\n try:\n data = binascii.unhexlify(text.encode())\n except (ValueError, TypeError) as ex:\n- raise dns.exception.SyntaxError(\"Hex decoding error: %s\" % str(ex))\n+ raise dns.exception.SyntaxError(f\"Hex decoding error: {str(ex)}\")\n return cls(rdclass, rdtype, data)\n \n def _to_wire(self, file, compress=None, origin=None, canonicalize=False):"}, {"sha": "a2b15b922abed495f739a6f9bafbba88f6d8bdab", "filename": "lib/dns/rdtypes/svcbbase.py", "status": "modified", "additions": 32, "deletions": 0, "changes": 32, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2Fsvcbbase.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2Fsvcbbase.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Frdtypes%2Fsvcbbase.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -35,6 +35,7 @@ class ParamKey(dns.enum.IntEnum):\n ECH = 5\n IPV6HINT = 6\n DOHPATH = 7\n+ OHTTP = 8\n \n @classmethod\n def _maximum(cls):\n@@ -396,6 +397,36 @@ def to_wire(self, file, origin=None): # pylint: disable=W0613\n file.write(self.ech)\n \n \n+@dns.immutable.immutable\n+class OHTTPParam(Param):\n+ # We don't ever expect to instantiate this class, but we need\n+ # a from_value() and a from_wire_parser(), so we just return None\n+ # from the class methods when things are OK.\n+\n+ @classmethod\n+ def emptiness(cls):\n+ return Emptiness.ALWAYS\n+\n+ @classmethod\n+ def from_value(cls, value):\n+ if value is None or value == \"\":\n+ return None\n+ else:\n+ raise ValueError(\"ohttp with non-empty value\")\n+\n+ def to_text(self):\n+ raise NotImplementedError # pragma: no cover\n+\n+ @classmethod\n+ def from_wire_parser(cls, parser, origin=None): # pylint: disable=W0613\n+ if parser.remaining() != 0:\n+ raise dns.exception.FormError\n+ return None\n+\n+ def to_wire(self, file, origin=None): # pylint: disable=W0613\n+ raise NotImplementedError # pragma: no cover\n+\n+\n _class_for_key = {\n ParamKey.MANDATORY: MandatoryParam,\n ParamKey.ALPN: ALPNParam,\n@@ -404,6 +435,7 @@ def to_wire(self, file, origin=None): # pylint: disable=W0613\n ParamKey.IPV4HINT: IPv4HintParam,\n ParamKey.ECH: ECHParam,\n ParamKey.IPV6HINT: IPv6HintParam,\n+ ParamKey.OHTTP: OHTTPParam,\n }\n \n "}, {"sha": "73db6d9e25dcadab2dacb57001b490ac91d40dba", "filename": "lib/dns/rdtypes/txtbase.py", "status": "modified", "additions": 3, "deletions": 1, "changes": 4, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2Ftxtbase.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2Ftxtbase.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Frdtypes%2Ftxtbase.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -50,6 +50,8 @@ def __init__(\n self.strings: Tuple[bytes] = self._as_tuple(\n strings, lambda x: self._as_bytes(x, True, 255)\n )\n+ if len(self.strings) == 0:\n+ raise ValueError(\"the list of strings must not be empty\")\n \n def to_text(\n self,\n@@ -60,7 +62,7 @@ def to_text(\n txt = \"\"\n prefix = \"\"\n for s in self.strings:\n- txt += '{}\"{}\"'.format(prefix, dns.rdata._escapify(s))\n+ txt += f'{prefix}\"{dns.rdata._escapify(s)}\"'\n prefix = \" \"\n return txt\n "}, {"sha": "653a0bf2e7eb03829905927a1c21d584c250793a", "filename": "lib/dns/rdtypes/util.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2Futil.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Frdtypes%2Futil.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Frdtypes%2Futil.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -231,7 +231,7 @@ def weighted_processing_order(iterable):\n total = sum(rdata._processing_weight() or _no_weight for rdata in rdatas)\n while len(rdatas) > 1:\n r = random.uniform(0, total)\n- for n, rdata in enumerate(rdatas):\n+ for n, rdata in enumerate(rdatas): # noqa: B007\n weight = rdata._processing_weight() or _no_weight\n if weight > r:\n break"}, {"sha": "3ba76e31e6db99b47108609312ddfdd6e68dfa4f", "filename": "lib/dns/resolver.py", "status": "modified", "additions": 19, "deletions": 20, "changes": 39, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fresolver.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fresolver.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fresolver.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -36,6 +36,7 @@\n import dns.ipv6\n import dns.message\n import dns.name\n+import dns.rdata\n import dns.nameserver\n import dns.query\n import dns.rcode\n@@ -45,7 +46,7 @@\n import dns.reversename\n import dns.tsig\n \n-if sys.platform == \"win32\":\n+if sys.platform == \"win32\": # pragma: no cover\n import dns.win32util\n \n \n@@ -83,7 +84,7 @@ def __str__(self) -> str:\n else:\n msg = \"The DNS query name does not exist\"\n qnames = \", \".join(map(str, qnames))\n- return \"{}: {}\".format(msg, qnames)\n+ return f\"{msg}: {qnames}\"\n \n @property\n def canonical_name(self):\n@@ -96,7 +97,7 @@ def canonical_name(self):\n cname = response.canonical_name()\n if cname != qname:\n return cname\n- except Exception:\n+ except Exception: # pragma: no cover\n # We can just eat this exception as it means there was\n # something wrong with the response.\n pass\n@@ -154,15 +155,15 @@ def _errors_to_text(errors: List[ErrorTuple]) -> List[str]:\n \"\"\"Turn a resolution errors trace into a list of text.\"\"\"\n texts = []\n for err in errors:\n- texts.append(\"Server {} answered {}\".format(err[0], err[3]))\n+ texts.append(f\"Server {err[0]} answered {err[3]}\")\n return texts\n \n \n class LifetimeTimeout(dns.exception.Timeout):\n \"\"\"The resolution lifetime expired.\"\"\"\n \n msg = \"The resolution lifetime expired.\"\n- fmt = \"%s after {timeout:.3f} seconds: {errors}\" % msg[:-1]\n+ fmt = f\"{msg[:-1]} after {{timeout:.3f}} seconds: {{errors}}\"\n supp_kwargs = {\"timeout\", \"errors\"}\n \n # We do this as otherwise mypy complains about unexpected keyword argument\n@@ -211,7 +212,7 @@ class NoNameservers(dns.exception.DNSException):\n \"\"\"\n \n msg = \"All nameservers failed to answer the query.\"\n- fmt = \"%s {query}: {errors}\" % msg[:-1]\n+ fmt = f\"{msg[:-1]} {{query}}: {{errors}}\"\n supp_kwargs = {\"request\", \"errors\"}\n \n # We do this as otherwise mypy complains about unexpected keyword argument\n@@ -297,7 +298,7 @@ def __getattr__(self, attr): # pragma: no cover\n def __len__(self) -> int:\n return self.rrset and len(self.rrset) or 0\n \n- def __iter__(self):\n+ def __iter__(self) -> Iterator[dns.rdata.Rdata]:\n return self.rrset and iter(self.rrset) or iter(tuple())\n \n def __getitem__(self, i):\n@@ -334,7 +335,7 @@ def make(\n answers[dns.rdatatype.A] = v4\n return answers\n \n- # Returns pairs of (address, family) from this result, potentiallys\n+ # Returns pairs of (address, family) from this result, potentially\n # filtering by address family.\n def addresses_and_families(\n self, family: int = socket.AF_UNSPEC\n@@ -347,7 +348,7 @@ def addresses_and_families(\n answer = self.get(dns.rdatatype.AAAA)\n elif family == socket.AF_INET:\n answer = self.get(dns.rdatatype.A)\n- else:\n+ else: # pragma: no cover\n raise NotImplementedError(f\"unknown address family {family}\")\n if answer:\n for rdata in answer:\n@@ -938,7 +939,7 @@ def __init__(\n \n self.reset()\n if configure:\n- if sys.platform == \"win32\":\n+ if sys.platform == \"win32\": # pragma: no cover\n self.read_registry()\n elif filename:\n self.read_resolv_conf(filename)\n@@ -947,7 +948,7 @@ def reset(self) -> None:\n \"\"\"Reset all resolver configuration to the defaults.\"\"\"\n \n self.domain = dns.name.Name(dns.name.from_text(socket.gethostname())[1:])\n- if len(self.domain) == 0:\n+ if len(self.domain) == 0: # pragma: no cover\n self.domain = dns.name.root\n self._nameservers = []\n self.nameserver_ports = {}\n@@ -1040,7 +1041,7 @@ def read_resolv_conf(self, f: Any) -> None:\n # setter logic, with additonal checking and enrichment.\n self.nameservers = nameservers\n \n- def read_registry(self) -> None:\n+ def read_registry(self) -> None: # pragma: no cover\n \"\"\"Extract resolver configuration from the Windows registry.\"\"\"\n try:\n info = dns.win32util.get_dns_info() # type: ignore\n@@ -1205,9 +1206,7 @@ def _enrich_nameservers(\n enriched_nameservers.append(enriched_nameserver)\n else:\n raise ValueError(\n- \"nameservers must be a list or tuple (not a {})\".format(\n- type(nameservers)\n- )\n+ f\"nameservers must be a list or tuple (not a {type(nameservers)})\"\n )\n return enriched_nameservers\n \n@@ -1431,7 +1430,7 @@ def resolve_name(\n elif family == socket.AF_INET6:\n v6 = self.resolve(name, dns.rdatatype.AAAA, **modified_kwargs)\n return HostAnswers.make(v6=v6)\n- elif family != socket.AF_UNSPEC:\n+ elif family != socket.AF_UNSPEC: # pragma: no cover\n raise NotImplementedError(f\"unknown address family {family}\")\n \n raise_on_no_answer = modified_kwargs.pop(\"raise_on_no_answer\", True)\n@@ -1515,7 +1514,7 @@ def try_ddr(self, lifetime: float = 5.0) -> None:\n nameservers = dns._ddr._get_nameservers_sync(answer, timeout)\n if len(nameservers) > 0:\n self.nameservers = nameservers\n- except Exception:\n+ except Exception: # pragma: no cover\n pass\n \n \n@@ -1640,7 +1639,7 @@ def canonical_name(name: Union[dns.name.Name, str]) -> dns.name.Name:\n return get_default_resolver().canonical_name(name)\n \n \n-def try_ddr(lifetime: float = 5.0) -> None:\n+def try_ddr(lifetime: float = 5.0) -> None: # pragma: no cover\n \"\"\"Try to update the default resolver's nameservers using Discovery of Designated\n Resolvers (DDR). If successful, the resolver will subsequently use\n DNS-over-HTTPS or DNS-over-TLS for future queries.\n@@ -1926,7 +1925,7 @@ def _getnameinfo(sockaddr, flags=0):\n family = socket.AF_INET\n tuples = _getaddrinfo(host, port, family, socket.SOCK_STREAM, socket.SOL_TCP, 0)\n if len(tuples) > 1:\n- raise socket.error(\"sockaddr resolved to multiple addresses\")\n+ raise OSError(\"sockaddr resolved to multiple addresses\")\n addr = tuples[0][4][0]\n if flags & socket.NI_DGRAM:\n pname = \"udp\"\n@@ -1961,7 +1960,7 @@ def _getfqdn(name=None):\n (name, _, _) = _gethostbyaddr(name)\n # Python's version checks aliases too, but our gethostbyname\n # ignores them, so we do so here as well.\n- except Exception:\n+ except Exception: # pragma: no cover\n pass\n return name\n "}, {"sha": "ae8f0dd5039693a24a14bf9cff638ee740798a07", "filename": "lib/dns/set.py", "status": "modified", "additions": 6, "deletions": 5, "changes": 11, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fset.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fset.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fset.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -21,10 +21,11 @@\n class Set:\n \"\"\"A simple set class.\n \n- This class was originally used to deal with sets being missing in\n- ancient versions of python, but dnspython will continue to use it\n- as these sets are based on lists and are thus indexable, and this\n- ability is widely used in dnspython applications.\n+ This class was originally used to deal with python not having a set class, and\n+ originally the class used lists in its implementation. The ordered and indexable\n+ nature of RRsets and Rdatasets is unfortunately widely used in dnspython\n+ applications, so for backwards compatibility sets continue to be a custom class, now\n+ based on an ordered dictionary.\n \"\"\"\n \n __slots__ = [\"items\"]\n@@ -43,7 +44,7 @@ def __init__(self, items=None):\n self.add(item) # lgtm[py/init-calls-subclass]\n \n def __repr__(self):\n- return \"dns.set.Set(%s)\" % repr(list(self.items.keys()))\n+ return f\"dns.set.Set({repr(list(self.items.keys()))})\" # pragma: no cover\n \n def add(self, item):\n \"\"\"Add an item to the set.\"\"\""}, {"sha": "ab205bc39af26fba21255dc0e5cb124ddba0b10b", "filename": "lib/dns/tokenizer.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Ftokenizer.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Ftokenizer.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Ftokenizer.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -528,7 +528,7 @@ def get_uint16(self, base: int = 10) -> int:\n if value < 0 or value > 65535:\n if base == 8:\n raise dns.exception.SyntaxError(\n- \"%o is not an octal unsigned 16-bit integer\" % value\n+ f\"{value:o} is not an octal unsigned 16-bit integer\"\n )\n else:\n raise dns.exception.SyntaxError("}, {"sha": "aa2e1160336b6450525f1748bdc05c05e08acca4", "filename": "lib/dns/transaction.py", "status": "modified", "additions": 1, "deletions": 3, "changes": 4, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Ftransaction.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Ftransaction.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Ftransaction.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -486,7 +486,7 @@ def _delete(self, exact, args):\n if exact:\n raise DeleteNotExact(f\"{method}: missing rdataset\")\n else:\n- self._delete_rdataset(name, rdtype, covers)\n+ self._checked_delete_rdataset(name, rdtype, covers)\n return\n else:\n rdataset = self._rdataset_from_args(method, True, args)\n@@ -529,8 +529,6 @@ def _check_ended(self):\n \n def _end(self, commit):\n self._check_ended()\n- if self._ended:\n- raise AlreadyEnded\n try:\n self._end_transaction(commit)\n finally:"}, {"sha": "b9a99fe3c2246ba13e9a9d27b7f0c79cf2c1afce", "filename": "lib/dns/ttl.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fttl.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fttl.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fttl.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -73,7 +73,7 @@ def from_text(text: str) -> int:\n elif c == \"s\":\n total += current\n else:\n- raise BadTTL(\"unknown unit '%s'\" % c)\n+ raise BadTTL(f\"unknown unit '{c}'\")\n current = 0\n need_digit = True\n if not current == 0:"}, {"sha": "9ed2ce19b2421f30c81ab6243e0120eb846ff9b7", "filename": "lib/dns/version.py", "status": "modified", "additions": 2, "deletions": 2, "changes": 4, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fversion.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fversion.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fversion.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -20,9 +20,9 @@\n #: MAJOR\n MAJOR = 2\n #: MINOR\n-MINOR = 6\n+MINOR = 7\n #: MICRO\n-MICRO = 1\n+MICRO = 0\n #: RELEASELEVEL\n RELEASELEVEL = 0x0F\n #: SERIAL"}, {"sha": "9ed3f11bcba03fc57de939aca45a82a1d067bae0", "filename": "lib/dns/win32util.py", "status": "modified", "additions": 15, "deletions": 25, "changes": 40, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fwin32util.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fwin32util.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fwin32util.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -13,8 +13,8 @@\n \n # Keep pylint quiet on non-windows.\n try:\n- WindowsError is None # pylint: disable=used-before-assignment\n- except KeyError:\n+ _ = WindowsError # pylint: disable=used-before-assignment\n+ except NameError:\n WindowsError = Exception\n \n if dns._features.have(\"wmi\"):\n@@ -44,6 +44,7 @@ def __init__(self):\n if _have_wmi:\n \n class _WMIGetter(threading.Thread):\n+ # pylint: disable=possibly-used-before-assignment\n def __init__(self):\n super().__init__()\n self.info = DnsInfo()\n@@ -82,32 +83,21 @@ class _RegistryGetter:\n def __init__(self):\n self.info = DnsInfo()\n \n- def _determine_split_char(self, entry):\n- #\n- # The windows registry irritatingly changes the list element\n- # delimiter in between ' ' and ',' (and vice-versa) in various\n- # versions of windows.\n- #\n- if entry.find(\" \") >= 0:\n- split_char = \" \"\n- elif entry.find(\",\") >= 0:\n- split_char = \",\"\n- else:\n- # probably a singleton; treat as a space-separated list.\n- split_char = \" \"\n- return split_char\n+ def _split(self, text):\n+ # The windows registry has used both \" \" and \",\" as a delimiter, and while\n+ # it is currently using \",\" in Windows 10 and later, updates can seemingly\n+ # leave a space in too, e.g. \"a, b\". So we just convert all commas to\n+ # spaces, and use split() in its default configuration, which splits on\n+ # all whitespace and ignores empty strings.\n+ return text.replace(\",\", \" \").split()\n \n def _config_nameservers(self, nameservers):\n- split_char = self._determine_split_char(nameservers)\n- ns_list = nameservers.split(split_char)\n- for ns in ns_list:\n+ for ns in self._split(nameservers):\n if ns not in self.info.nameservers:\n self.info.nameservers.append(ns)\n \n def _config_search(self, search):\n- split_char = self._determine_split_char(search)\n- search_list = search.split(split_char)\n- for s in search_list:\n+ for s in self._split(search):\n s = _config_domain(s)\n if s not in self.info.search:\n self.info.search.append(s)\n@@ -164,7 +154,7 @@ def _is_nic_enabled(self, lm, guid):\n lm,\n r\"SYSTEM\\CurrentControlSet\\Control\\Network\"\n r\"\\{4D36E972-E325-11CE-BFC1-08002BE10318}\"\n- r\"\\%s\\Connection\" % guid,\n+ rf\"\\{guid}\\Connection\",\n )\n \n try:\n@@ -177,7 +167,7 @@ def _is_nic_enabled(self, lm, guid):\n raise ValueError # pragma: no cover\n \n device_key = winreg.OpenKey(\n- lm, r\"SYSTEM\\CurrentControlSet\\Enum\\%s\" % pnp_id\n+ lm, rf\"SYSTEM\\CurrentControlSet\\Enum\\{pnp_id}\"\n )\n \n try:\n@@ -232,7 +222,7 @@ def get(self):\n self._config_fromkey(key, False)\n finally:\n key.Close()\n- except EnvironmentError:\n+ except OSError:\n break\n finally:\n interfaces.Close()"}, {"sha": "520aa32ddc32ea50b090ff4b08b9450709a5fb93", "filename": "lib/dns/xfr.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fxfr.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fxfr.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fxfr.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -33,7 +33,7 @@ class TransferError(dns.exception.DNSException):\n \"\"\"A zone transfer response got a non-zero rcode.\"\"\"\n \n def __init__(self, rcode):\n- message = \"Zone transfer error: %s\" % dns.rcode.to_text(rcode)\n+ message = f\"Zone transfer error: {dns.rcode.to_text(rcode)}\"\n super().__init__(message)\n self.rcode = rcode\n "}, {"sha": "d74510b29f008c0ab03e16a5ac2bd211c81fde5f", "filename": "lib/dns/zonefile.py", "status": "modified", "additions": 32, "deletions": 34, "changes": 66, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fzonefile.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fdns%2Fzonefile.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fdns%2Fzonefile.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -230,7 +230,7 @@ def _rr_line(self):\n try:\n rdtype = dns.rdatatype.from_text(token.value)\n except Exception:\n- raise dns.exception.SyntaxError(\"unknown rdatatype '%s'\" % token.value)\n+ raise dns.exception.SyntaxError(f\"unknown rdatatype '{token.value}'\")\n \n try:\n rd = dns.rdata.from_text(\n@@ -251,9 +251,7 @@ def _rr_line(self):\n # We convert them to syntax errors so that we can emit\n # helpful filename:line info.\n (ty, va) = sys.exc_info()[:2]\n- raise dns.exception.SyntaxError(\n- \"caught exception {}: {}\".format(str(ty), str(va))\n- )\n+ raise dns.exception.SyntaxError(f\"caught exception {str(ty)}: {str(va)}\")\n \n if not self.default_ttl_known and rdtype == dns.rdatatype.SOA:\n # The pre-RFC2308 and pre-BIND9 behavior inherits the zone default\n@@ -281,41 +279,41 @@ def _parse_modify(self, side: str) -> Tuple[str, str, int, int, str]:\n # Sometimes there are modifiers in the hostname. These come after\n # the dollar sign. They are in the form: ${offset[,width[,base]]}.\n # Make names\n+ mod = \"\"\n+ sign = \"+\"\n+ offset = \"0\"\n+ width = \"0\"\n+ base = \"d\"\n g1 = is_generate1.match(side)\n if g1:\n mod, sign, offset, width, base = g1.groups()\n if sign == \"\":\n sign = \"+\"\n- g2 = is_generate2.match(side)\n- if g2:\n- mod, sign, offset = g2.groups()\n- if sign == \"\":\n- sign = \"+\"\n- width = 0\n- base = \"d\"\n- g3 = is_generate3.match(side)\n- if g3:\n- mod, sign, offset, width = g3.groups()\n- if sign == \"\":\n- sign = \"+\"\n- base = \"d\"\n-\n- if not (g1 or g2 or g3):\n- mod = \"\"\n- sign = \"+\"\n- offset = 0\n- width = 0\n- base = \"d\"\n+ else:\n+ g2 = is_generate2.match(side)\n+ if g2:\n+ mod, sign, offset = g2.groups()\n+ if sign == \"\":\n+ sign = \"+\"\n+ width = \"0\"\n+ base = \"d\"\n+ else:\n+ g3 = is_generate3.match(side)\n+ if g3:\n+ mod, sign, offset, width = g3.groups()\n+ if sign == \"\":\n+ sign = \"+\"\n+ base = \"d\"\n \n- offset = int(offset)\n- width = int(width)\n+ ioffset = int(offset)\n+ iwidth = int(width)\n \n if sign not in [\"+\", \"-\"]:\n- raise dns.exception.SyntaxError(\"invalid offset sign %s\" % sign)\n+ raise dns.exception.SyntaxError(f\"invalid offset sign {sign}\")\n if base not in [\"d\", \"o\", \"x\", \"X\", \"n\", \"N\"]:\n- raise dns.exception.SyntaxError(\"invalid type %s\" % base)\n+ raise dns.exception.SyntaxError(f\"invalid type {base}\")\n \n- return mod, sign, offset, width, base\n+ return mod, sign, ioffset, iwidth, base\n \n def _generate_line(self):\n # range lhs [ttl] [class] type rhs [ comment ]\n@@ -377,7 +375,7 @@ def _generate_line(self):\n if not token.is_identifier():\n raise dns.exception.SyntaxError\n except Exception:\n- raise dns.exception.SyntaxError(\"unknown rdatatype '%s'\" % token.value)\n+ raise dns.exception.SyntaxError(f\"unknown rdatatype '{token.value}'\")\n \n # rhs (required)\n rhs = token.value\n@@ -412,8 +410,8 @@ def _format_index(index: int, base: str, width: int) -> str:\n lzfindex = _format_index(lindex, lbase, lwidth)\n rzfindex = _format_index(rindex, rbase, rwidth)\n \n- name = lhs.replace(\"$%s\" % (lmod), lzfindex)\n- rdata = rhs.replace(\"$%s\" % (rmod), rzfindex)\n+ name = lhs.replace(f\"${lmod}\", lzfindex)\n+ rdata = rhs.replace(f\"${rmod}\", rzfindex)\n \n self.last_name = dns.name.from_text(\n name, self.current_origin, self.tok.idna_codec\n@@ -445,7 +443,7 @@ def _format_index(index: int, base: str, width: int) -> str:\n # helpful filename:line info.\n (ty, va) = sys.exc_info()[:2]\n raise dns.exception.SyntaxError(\n- \"caught exception %s: %s\" % (str(ty), str(va))\n+ f\"caught exception {str(ty)}: {str(va)}\"\n )\n \n self.txn.add(name, ttl, rd)\n@@ -528,7 +526,7 @@ def read(self) -> None:\n self.default_ttl_known,\n )\n )\n- self.current_file = open(filename, \"r\")\n+ self.current_file = open(filename)\n self.tok = dns.tokenizer.Tokenizer(self.current_file, filename)\n self.current_origin = new_origin\n elif c == \"$GENERATE\":"}, {"sha": "cfdc030a751b089fc7e38fc88093b791605d501d", "filename": "lib/idna/__init__.py", "status": "modified", "additions": 2, "deletions": 1, "changes": 3, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fidna%2F__init__.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fidna%2F__init__.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fidna%2F__init__.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,4 +1,3 @@\n-from .package_data import __version__\n from .core import (\n IDNABidiError,\n IDNAError,\n@@ -20,8 +19,10 @@\n valid_string_length,\n )\n from .intranges import intranges_contain\n+from .package_data import __version__\n \n __all__ = [\n+ \"__version__\",\n \"IDNABidiError\",\n \"IDNAError\",\n \"InvalidCodepoint\","}, {"sha": "913abfd6a23ce547f84de2adc41221012f1007d6", "filename": "lib/idna/codec.py", "status": "modified", "additions": 31, "deletions": 27, "changes": 58, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fidna%2Fcodec.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fidna%2Fcodec.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fidna%2Fcodec.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,49 +1,51 @@\n-from .core import encode, decode, alabel, ulabel, IDNAError\n import codecs\n import re\n-from typing import Any, Tuple, Optional\n+from typing import Any, Optional, Tuple\n \n-_unicode_dots_re = re.compile('[\\u002e\\u3002\\uff0e\\uff61]')\n+from .core import IDNAError, alabel, decode, encode, ulabel\n+\n+_unicode_dots_re = re.compile(\"[\\u002e\\u3002\\uff0e\\uff61]\")\n \n-class Codec(codecs.Codec):\n \n- def encode(self, data: str, errors: str = 'strict') -> Tuple[bytes, int]:\n- if errors != 'strict':\n- raise IDNAError('Unsupported error handling \\\"{}\\\"'.format(errors))\n+class Codec(codecs.Codec):\n+ def encode(self, data: str, errors: str = \"strict\") -> Tuple[bytes, int]:\n+ if errors != \"strict\":\n+ raise IDNAError('Unsupported error handling \"{}\"'.format(errors))\n \n if not data:\n return b\"\", 0\n \n return encode(data), len(data)\n \n- def decode(self, data: bytes, errors: str = 'strict') -> Tuple[str, int]:\n- if errors != 'strict':\n- raise IDNAError('Unsupported error handling \\\"{}\\\"'.format(errors))\n+ def decode(self, data: bytes, errors: str = \"strict\") -> Tuple[str, int]:\n+ if errors != \"strict\":\n+ raise IDNAError('Unsupported error handling \"{}\"'.format(errors))\n \n if not data:\n- return '', 0\n+ return \"\", 0\n \n return decode(data), len(data)\n \n+\n class IncrementalEncoder(codecs.BufferedIncrementalEncoder):\n def _buffer_encode(self, data: str, errors: str, final: bool) -> Tuple[bytes, int]:\n- if errors != 'strict':\n- raise IDNAError('Unsupported error handling \\\"{}\\\"'.format(errors))\n+ if errors != \"strict\":\n+ raise IDNAError('Unsupported error handling \"{}\"'.format(errors))\n \n if not data:\n- return b'', 0\n+ return b\"\", 0\n \n labels = _unicode_dots_re.split(data)\n- trailing_dot = b''\n+ trailing_dot = b\"\"\n if labels:\n if not labels[-1]:\n- trailing_dot = b'.'\n+ trailing_dot = b\".\"\n del labels[-1]\n elif not final:\n # Keep potentially unfinished label until the next call\n del labels[-1]\n if labels:\n- trailing_dot = b'.'\n+ trailing_dot = b\".\"\n \n result = []\n size = 0\n@@ -54,32 +56,33 @@ def _buffer_encode(self, data: str, errors: str, final: bool) -> Tuple[bytes, in\n size += len(label)\n \n # Join with U+002E\n- result_bytes = b'.'.join(result) + trailing_dot\n+ result_bytes = b\".\".join(result) + trailing_dot\n size += len(trailing_dot)\n return result_bytes, size\n \n+\n class IncrementalDecoder(codecs.BufferedIncrementalDecoder):\n def _buffer_decode(self, data: Any, errors: str, final: bool) -> Tuple[str, int]:\n- if errors != 'strict':\n- raise IDNAError('Unsupported error handling \\\"{}\\\"'.format(errors))\n+ if errors != \"strict\":\n+ raise IDNAError('Unsupported error handling \"{}\"'.format(errors))\n \n if not data:\n- return ('', 0)\n+ return (\"\", 0)\n \n if not isinstance(data, str):\n- data = str(data, 'ascii')\n+ data = str(data, \"ascii\")\n \n labels = _unicode_dots_re.split(data)\n- trailing_dot = ''\n+ trailing_dot = \"\"\n if labels:\n if not labels[-1]:\n- trailing_dot = '.'\n+ trailing_dot = \".\"\n del labels[-1]\n elif not final:\n # Keep potentially unfinished label until the next call\n del labels[-1]\n if labels:\n- trailing_dot = '.'\n+ trailing_dot = \".\"\n \n result = []\n size = 0\n@@ -89,7 +92,7 @@ def _buffer_decode(self, data: Any, errors: str, final: bool) -> Tuple[str, int]\n size += 1\n size += len(label)\n \n- result_str = '.'.join(result) + trailing_dot\n+ result_str = \".\".join(result) + trailing_dot\n size += len(trailing_dot)\n return (result_str, size)\n \n@@ -103,7 +106,7 @@ class StreamReader(Codec, codecs.StreamReader):\n \n \n def search_function(name: str) -> Optional[codecs.CodecInfo]:\n- if name != 'idna2008':\n+ if name != \"idna2008\":\n return None\n return codecs.CodecInfo(\n name=name,\n@@ -115,4 +118,5 @@ def search_function(name: str) -> Optional[codecs.CodecInfo]:\n streamreader=StreamReader,\n )\n \n+\n codecs.register(search_function)"}, {"sha": "1df9f2a70e6815908f2784e88897a9a359eef84c", "filename": "lib/idna/compat.py", "status": "modified", "additions": 6, "deletions": 4, "changes": 10, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fidna%2Fcompat.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fidna%2Fcompat.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fidna%2Fcompat.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,13 +1,15 @@\n-from .core import *\n-from .codec import *\n from typing import Any, Union\n \n+from .core import decode, encode\n+\n+\n def ToASCII(label: str) -> bytes:\n return encode(label)\n \n+\n def ToUnicode(label: Union[bytes, bytearray]) -> str:\n return decode(label)\n \n-def nameprep(s: Any) -> None:\n- raise NotImplementedError('IDNA 2008 does not utilise nameprep protocol')\n \n+def nameprep(s: Any) -> None:\n+ raise NotImplementedError(\"IDNA 2008 does not utilise nameprep protocol\")"}, {"sha": "9115f123f0274832af5ba1cf3c5481cc5353eecd", "filename": "lib/idna/core.py", "status": "modified", "additions": 161, "deletions": 119, "changes": 280, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fidna%2Fcore.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fidna%2Fcore.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fidna%2Fcore.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,49 +1,58 @@\n-from . import idnadata\n import bisect\n-import unicodedata\n import re\n-from typing import Union, Optional\n+import unicodedata\n+from typing import Optional, Union\n+\n+from . import idnadata\n from .intranges import intranges_contain\n \n _virama_combining_class = 9\n-_alabel_prefix = b'xn--'\n-_unicode_dots_re = re.compile('[\\u002e\\u3002\\uff0e\\uff61]')\n+_alabel_prefix = b\"xn--\"\n+_unicode_dots_re = re.compile(\"[\\u002e\\u3002\\uff0e\\uff61]\")\n+\n \n class IDNAError(UnicodeError):\n- \"\"\" Base exception for all IDNA-encoding related problems \"\"\"\n+ \"\"\"Base exception for all IDNA-encoding related problems\"\"\"\n+\n pass\n \n \n class IDNABidiError(IDNAError):\n- \"\"\" Exception when bidirectional requirements are not satisfied \"\"\"\n+ \"\"\"Exception when bidirectional requirements are not satisfied\"\"\"\n+\n pass\n \n \n class InvalidCodepoint(IDNAError):\n- \"\"\" Exception when a disallowed or unallocated codepoint is used \"\"\"\n+ \"\"\"Exception when a disallowed or unallocated codepoint is used\"\"\"\n+\n pass\n \n \n class InvalidCodepointContext(IDNAError):\n- \"\"\" Exception when the codepoint is not valid in the context it is used \"\"\"\n+ \"\"\"Exception when the codepoint is not valid in the context it is used\"\"\"\n+\n pass\n \n \n def _combining_class(cp: int) -> int:\n v = unicodedata.combining(chr(cp))\n if v == 0:\n if not unicodedata.name(chr(cp)):\n- raise ValueError('Unknown character in unicodedata')\n+ raise ValueError(\"Unknown character in unicodedata\")\n return v\n \n+\n def _is_script(cp: str, script: str) -> bool:\n return intranges_contain(ord(cp), idnadata.scripts[script])\n \n+\n def _punycode(s: str) -> bytes:\n- return s.encode('punycode')\n+ return s.encode(\"punycode\")\n+\n \n def _unot(s: int) -> str:\n- return 'U+{:04X}'.format(s)\n+ return \"U+{:04X}\".format(s)\n \n \n def valid_label_length(label: Union[bytes, str]) -> bool:\n@@ -61,96 +70,106 @@ def valid_string_length(label: Union[bytes, str], trailing_dot: bool) -> bool:\n def check_bidi(label: str, check_ltr: bool = False) -> bool:\n # Bidi rules should only be applied if string contains RTL characters\n bidi_label = False\n- for (idx, cp) in enumerate(label, 1):\n+ for idx, cp in enumerate(label, 1):\n direction = unicodedata.bidirectional(cp)\n- if direction == '':\n+ if direction == \"\":\n # String likely comes from a newer version of Unicode\n- raise IDNABidiError('Unknown directionality in label {} at position {}'.format(repr(label), idx))\n- if direction in ['R', 'AL', 'AN']:\n+ raise IDNABidiError(\"Unknown directionality in label {} at position {}\".format(repr(label), idx))\n+ if direction in [\"R\", \"AL\", \"AN\"]:\n bidi_label = True\n if not bidi_label and not check_ltr:\n return True\n \n # Bidi rule 1\n direction = unicodedata.bidirectional(label[0])\n- if direction in ['R', 'AL']:\n+ if direction in [\"R\", \"AL\"]:\n rtl = True\n- elif direction == 'L':\n+ elif direction == \"L\":\n rtl = False\n else:\n- raise IDNABidiError('First codepoint in label {} must be directionality L, R or AL'.format(repr(label)))\n+ raise IDNABidiError(\"First codepoint in label {} must be directionality L, R or AL\".format(repr(label)))\n \n valid_ending = False\n- number_type = None # type: Optional[str]\n- for (idx, cp) in enumerate(label, 1):\n+ number_type: Optional[str] = None\n+ for idx, cp in enumerate(label, 1):\n direction = unicodedata.bidirectional(cp)\n \n if rtl:\n # Bidi rule 2\n- if not direction in ['R', 'AL', 'AN', 'EN', 'ES', 'CS', 'ET', 'ON', 'BN', 'NSM']:\n- raise IDNABidiError('Invalid direction for codepoint at position {} in a right-to-left label'.format(idx))\n+ if direction not in [\n+ \"R\",\n+ \"AL\",\n+ \"AN\",\n+ \"EN\",\n+ \"ES\",\n+ \"CS\",\n+ \"ET\",\n+ \"ON\",\n+ \"BN\",\n+ \"NSM\",\n+ ]:\n+ raise IDNABidiError(\"Invalid direction for codepoint at position {} in a right-to-left label\".format(idx))\n # Bidi rule 3\n- if direction in ['R', 'AL', 'EN', 'AN']:\n+ if direction in [\"R\", \"AL\", \"EN\", \"AN\"]:\n valid_ending = True\n- elif direction != 'NSM':\n+ elif direction != \"NSM\":\n valid_ending = False\n # Bidi rule 4\n- if direction in ['AN', 'EN']:\n+ if direction in [\"AN\", \"EN\"]:\n if not number_type:\n number_type = direction\n else:\n if number_type != direction:\n- raise IDNABidiError('Can not mix numeral types in a right-to-left label')\n+ raise IDNABidiError(\"Can not mix numeral types in a right-to-left label\")\n else:\n # Bidi rule 5\n- if not direction in ['L', 'EN', 'ES', 'CS', 'ET', 'ON', 'BN', 'NSM']:\n- raise IDNABidiError('Invalid direction for codepoint at position {} in a left-to-right label'.format(idx))\n+ if direction not in [\"L\", \"EN\", \"ES\", \"CS\", \"ET\", \"ON\", \"BN\", \"NSM\"]:\n+ raise IDNABidiError(\"Invalid direction for codepoint at position {} in a left-to-right label\".format(idx))\n # Bidi rule 6\n- if direction in ['L', 'EN']:\n+ if direction in [\"L\", \"EN\"]:\n valid_ending = True\n- elif direction != 'NSM':\n+ elif direction != \"NSM\":\n valid_ending = False\n \n if not valid_ending:\n- raise IDNABidiError('Label ends with illegal codepoint directionality')\n+ raise IDNABidiError(\"Label ends with illegal codepoint directionality\")\n \n return True\n \n \n def check_initial_combiner(label: str) -> bool:\n- if unicodedata.category(label[0])[0] == 'M':\n- raise IDNAError('Label begins with an illegal combining character')\n+ if unicodedata.category(label[0])[0] == \"M\":\n+ raise IDNAError(\"Label begins with an illegal combining character\")\n return True\n \n \n def check_hyphen_ok(label: str) -> bool:\n- if label[2:4] == '--':\n- raise IDNAError('Label has disallowed hyphens in 3rd and 4th position')\n- if label[0] == '-' or label[-1] == '-':\n- raise IDNAError('Label must not start or end with a hyphen')\n+ if label[2:4] == \"--\":\n+ raise IDNAError(\"Label has disallowed hyphens in 3rd and 4th position\")\n+ if label[0] == \"-\" or label[-1] == \"-\":\n+ raise IDNAError(\"Label must not start or end with a hyphen\")\n return True\n \n \n def check_nfc(label: str) -> None:\n- if unicodedata.normalize('NFC', label) != label:\n- raise IDNAError('Label must be in Normalization Form C')\n+ if unicodedata.normalize(\"NFC\", label) != label:\n+ raise IDNAError(\"Label must be in Normalization Form C\")\n \n \n def valid_contextj(label: str, pos: int) -> bool:\n cp_value = ord(label[pos])\n \n- if cp_value == 0x200c:\n-\n+ if cp_value == 0x200C:\n if pos > 0:\n if _combining_class(ord(label[pos - 1])) == _virama_combining_class:\n return True\n \n ok = False\n- for i in range(pos-1, -1, -1):\n+ for i in range(pos - 1, -1, -1):\n joining_type = idnadata.joining_types.get(ord(label[i]))\n- if joining_type == ord('T'):\n+ if joining_type == ord(\"T\"):\n continue\n- elif joining_type in [ord('L'), ord('D')]:\n+ elif joining_type in [ord(\"L\"), ord(\"D\")]:\n ok = True\n break\n else:\n@@ -160,63 +179,61 @@ def valid_contextj(label: str, pos: int) -> bool:\n return False\n \n ok = False\n- for i in range(pos+1, len(label)):\n+ for i in range(pos + 1, len(label)):\n joining_type = idnadata.joining_types.get(ord(label[i]))\n- if joining_type == ord('T'):\n+ if joining_type == ord(\"T\"):\n continue\n- elif joining_type in [ord('R'), ord('D')]:\n+ elif joining_type in [ord(\"R\"), ord(\"D\")]:\n ok = True\n break\n else:\n break\n return ok\n \n- if cp_value == 0x200d:\n-\n+ if cp_value == 0x200D:\n if pos > 0:\n if _combining_class(ord(label[pos - 1])) == _virama_combining_class:\n return True\n return False\n \n else:\n-\n return False\n \n \n def valid_contexto(label: str, pos: int, exception: bool = False) -> bool:\n cp_value = ord(label[pos])\n \n- if cp_value == 0x00b7:\n- if 0 < pos < len(label)-1:\n- if ord(label[pos - 1]) == 0x006c and ord(label[pos + 1]) == 0x006c:\n+ if cp_value == 0x00B7:\n+ if 0 < pos < len(label) - 1:\n+ if ord(label[pos - 1]) == 0x006C and ord(label[pos + 1]) == 0x006C:\n return True\n return False\n \n elif cp_value == 0x0375:\n- if pos < len(label)-1 and len(label) > 1:\n- return _is_script(label[pos + 1], 'Greek')\n+ if pos < len(label) - 1 and len(label) > 1:\n+ return _is_script(label[pos + 1], \"Greek\")\n return False\n \n- elif cp_value == 0x05f3 or cp_value == 0x05f4:\n+ elif cp_value == 0x05F3 or cp_value == 0x05F4:\n if pos > 0:\n- return _is_script(label[pos - 1], 'Hebrew')\n+ return _is_script(label[pos - 1], \"Hebrew\")\n return False\n \n- elif cp_value == 0x30fb:\n+ elif cp_value == 0x30FB:\n for cp in label:\n- if cp == '\\u30fb':\n+ if cp == \"\\u30fb\":\n continue\n- if _is_script(cp, 'Hiragana') or _is_script(cp, 'Katakana') or _is_script(cp, 'Han'):\n+ if _is_script(cp, \"Hiragana\") or _is_script(cp, \"Katakana\") or _is_script(cp, \"Han\"):\n return True\n return False\n \n elif 0x660 <= cp_value <= 0x669:\n for cp in label:\n- if 0x6f0 <= ord(cp) <= 0x06f9:\n+ if 0x6F0 <= ord(cp) <= 0x06F9:\n return False\n return True\n \n- elif 0x6f0 <= cp_value <= 0x6f9:\n+ elif 0x6F0 <= cp_value <= 0x6F9:\n for cp in label:\n if 0x660 <= ord(cp) <= 0x0669:\n return False\n@@ -227,37 +244,49 @@ def valid_contexto(label: str, pos: int, exception: bool = False) -> bool:\n \n def check_label(label: Union[str, bytes, bytearray]) -> None:\n if isinstance(label, (bytes, bytearray)):\n- label = label.decode('utf-8')\n+ label = label.decode(\"utf-8\")\n if len(label) == 0:\n- raise IDNAError('Empty Label')\n+ raise IDNAError(\"Empty Label\")\n \n check_nfc(label)\n check_hyphen_ok(label)\n check_initial_combiner(label)\n \n- for (pos, cp) in enumerate(label):\n+ for pos, cp in enumerate(label):\n cp_value = ord(cp)\n- if intranges_contain(cp_value, idnadata.codepoint_classes['PVALID']):\n+ if intranges_contain(cp_value, idnadata.codepoint_classes[\"PVALID\"]):\n continue\n- elif intranges_contain(cp_value, idnadata.codepoint_classes['CONTEXTJ']):\n- if not valid_contextj(label, pos):\n- raise InvalidCodepointContext('Joiner {} not allowed at position {} in {}'.format(\n- _unot(cp_value), pos+1, repr(label)))\n- elif intranges_contain(cp_value, idnadata.codepoint_classes['CONTEXTO']):\n+ elif intranges_contain(cp_value, idnadata.codepoint_classes[\"CONTEXTJ\"]):\n+ try:\n+ if not valid_contextj(label, pos):\n+ raise InvalidCodepointContext(\n+ \"Joiner {} not allowed at position {} in {}\".format(_unot(cp_value), pos + 1, repr(label))\n+ )\n+ except ValueError:\n+ raise IDNAError(\n+ \"Unknown codepoint adjacent to joiner {} at position {} in {}\".format(\n+ _unot(cp_value), pos + 1, repr(label)\n+ )\n+ )\n+ elif intranges_contain(cp_value, idnadata.codepoint_classes[\"CONTEXTO\"]):\n if not valid_contexto(label, pos):\n- raise InvalidCodepointContext('Codepoint {} not allowed at position {} in {}'.format(_unot(cp_value), pos+1, repr(label)))\n+ raise InvalidCodepointContext(\n+ \"Codepoint {} not allowed at position {} in {}\".format(_unot(cp_value), pos + 1, repr(label))\n+ )\n else:\n- raise InvalidCodepoint('Codepoint {} at position {} of {} not allowed'.format(_unot(cp_value), pos+1, repr(label)))\n+ raise InvalidCodepoint(\n+ \"Codepoint {} at position {} of {} not allowed\".format(_unot(cp_value), pos + 1, repr(label))\n+ )\n \n check_bidi(label)\n \n \n def alabel(label: str) -> bytes:\n try:\n- label_bytes = label.encode('ascii')\n+ label_bytes = label.encode(\"ascii\")\n ulabel(label_bytes)\n if not valid_label_length(label_bytes):\n- raise IDNAError('Label too long')\n+ raise IDNAError(\"Label too long\")\n return label_bytes\n except UnicodeEncodeError:\n pass\n@@ -266,15 +295,15 @@ def alabel(label: str) -> bytes:\n label_bytes = _alabel_prefix + _punycode(label)\n \n if not valid_label_length(label_bytes):\n- raise IDNAError('Label too long')\n+ raise IDNAError(\"Label too long\")\n \n return label_bytes\n \n \n def ulabel(label: Union[str, bytes, bytearray]) -> str:\n if not isinstance(label, (bytes, bytearray)):\n try:\n- label_bytes = label.encode('ascii')\n+ label_bytes = label.encode(\"ascii\")\n except UnicodeEncodeError:\n check_label(label)\n return label\n@@ -283,104 +312,117 @@ def ulabel(label: Union[str, bytes, bytearray]) -> str:\n \n label_bytes = label_bytes.lower()\n if label_bytes.startswith(_alabel_prefix):\n- label_bytes = label_bytes[len(_alabel_prefix):]\n+ label_bytes = label_bytes[len(_alabel_prefix) :]\n if not label_bytes:\n- raise IDNAError('Malformed A-label, no Punycode eligible content found')\n- if label_bytes.decode('ascii')[-1] == '-':\n- raise IDNAError('A-label must not end with a hyphen')\n+ raise IDNAError(\"Malformed A-label, no Punycode eligible content found\")\n+ if label_bytes.decode(\"ascii\")[-1] == \"-\":\n+ raise IDNAError(\"A-label must not end with a hyphen\")\n else:\n check_label(label_bytes)\n- return label_bytes.decode('ascii')\n+ return label_bytes.decode(\"ascii\")\n \n try:\n- label = label_bytes.decode('punycode')\n+ label = label_bytes.decode(\"punycode\")\n except UnicodeError:\n- raise IDNAError('Invalid A-label')\n+ raise IDNAError(\"Invalid A-label\")\n check_label(label)\n return label\n \n \n def uts46_remap(domain: str, std3_rules: bool = True, transitional: bool = False) -> str:\n \"\"\"Re-map the characters in the string according to UTS46 processing.\"\"\"\n from .uts46data import uts46data\n- output = ''\n+\n+ output = \"\"\n \n for pos, char in enumerate(domain):\n code_point = ord(char)\n try:\n- uts46row = uts46data[code_point if code_point < 256 else\n- bisect.bisect_left(uts46data, (code_point, 'Z')) - 1]\n+ uts46row = uts46data[code_point if code_point < 256 else bisect.bisect_left(uts46data, (code_point, \"Z\")) - 1]\n status = uts46row[1]\n- replacement = None # type: Optional[str]\n+ replacement: Optional[str] = None\n if len(uts46row) == 3:\n replacement = uts46row[2]\n- if (status == 'V' or\n- (status == 'D' and not transitional) or\n- (status == '3' and not std3_rules and replacement is None)):\n+ if (\n+ status == \"V\"\n+ or (status == \"D\" and not transitional)\n+ or (status == \"3\" and not std3_rules and replacement is None)\n+ ):\n output += char\n- elif replacement is not None and (status == 'M' or\n- (status == '3' and not std3_rules) or\n- (status == 'D' and transitional)):\n+ elif replacement is not None and (\n+ status == \"M\" or (status == \"3\" and not std3_rules) or (status == \"D\" and transitional)\n+ ):\n output += replacement\n- elif status != 'I':\n+ elif status != \"I\":\n raise IndexError()\n except IndexError:\n raise InvalidCodepoint(\n- 'Codepoint {} not allowed at position {} in {}'.format(\n- _unot(code_point), pos + 1, repr(domain)))\n+ \"Codepoint {} not allowed at position {} in {}\".format(_unot(code_point), pos + 1, repr(domain))\n+ )\n \n- return unicodedata.normalize('NFC', output)\n+ return unicodedata.normalize(\"NFC\", output)\n \n \n-def encode(s: Union[str, bytes, bytearray], strict: bool = False, uts46: bool = False, std3_rules: bool = False, transitional: bool = False) -> bytes:\n+def encode(\n+ s: Union[str, bytes, bytearray],\n+ strict: bool = False,\n+ uts46: bool = False,\n+ std3_rules: bool = False,\n+ transitional: bool = False,\n+) -> bytes:\n if not isinstance(s, str):\n try:\n- s = str(s, 'ascii')\n+ s = str(s, \"ascii\")\n except UnicodeDecodeError:\n- raise IDNAError('should pass a unicode string to the function rather than a byte string.')\n+ raise IDNAError(\"should pass a unicode string to the function rather than a byte string.\")\n if uts46:\n s = uts46_remap(s, std3_rules, transitional)\n trailing_dot = False\n result = []\n if strict:\n- labels = s.split('.')\n+ labels = s.split(\".\")\n else:\n labels = _unicode_dots_re.split(s)\n- if not labels or labels == ['']:\n- raise IDNAError('Empty domain')\n- if labels[-1] == '':\n+ if not labels or labels == [\"\"]:\n+ raise IDNAError(\"Empty domain\")\n+ if labels[-1] == \"\":\n del labels[-1]\n trailing_dot = True\n for label in labels:\n s = alabel(label)\n if s:\n result.append(s)\n else:\n- raise IDNAError('Empty label')\n+ raise IDNAError(\"Empty label\")\n if trailing_dot:\n- result.append(b'')\n- s = b'.'.join(result)\n+ result.append(b\"\")\n+ s = b\".\".join(result)\n if not valid_string_length(s, trailing_dot):\n- raise IDNAError('Domain too long')\n+ raise IDNAError(\"Domain too long\")\n return s\n \n \n-def decode(s: Union[str, bytes, bytearray], strict: bool = False, uts46: bool = False, std3_rules: bool = False) -> str:\n+def decode(\n+ s: Union[str, bytes, bytearray],\n+ strict: bool = False,\n+ uts46: bool = False,\n+ std3_rules: bool = False,\n+) -> str:\n try:\n if not isinstance(s, str):\n- s = str(s, 'ascii')\n+ s = str(s, \"ascii\")\n except UnicodeDecodeError:\n- raise IDNAError('Invalid ASCII in A-label')\n+ raise IDNAError(\"Invalid ASCII in A-label\")\n if uts46:\n s = uts46_remap(s, std3_rules, False)\n trailing_dot = False\n result = []\n if not strict:\n labels = _unicode_dots_re.split(s)\n else:\n- labels = s.split('.')\n- if not labels or labels == ['']:\n- raise IDNAError('Empty domain')\n+ labels = s.split(\".\")\n+ if not labels or labels == [\"\"]:\n+ raise IDNAError(\"Empty domain\")\n if not labels[-1]:\n del labels[-1]\n trailing_dot = True\n@@ -389,7 +431,7 @@ def decode(s: Union[str, bytes, bytearray], strict: bool = False, uts46: bool =\n if s:\n result.append(s)\n else:\n- raise IDNAError('Empty label')\n+ raise IDNAError(\"Empty label\")\n if trailing_dot:\n- result.append('')\n- return '.'.join(result)\n+ result.append(\"\")\n+ return \".\".join(result)"}, {"sha": "4be6004622efcdc36a8d15efc0ac3e138a4bae02", "filename": "lib/idna/idnadata.py", "status": "modified", "additions": 3537, "deletions": 3539, "changes": 7076, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fidna%2Fidnadata.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fidna%2Fidnadata.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fidna%2Fidnadata.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "7bfaa8d80d7dc471d572db0f949460901126e8bd", "filename": "lib/idna/intranges.py", "status": "modified", "additions": 7, "deletions": 4, "changes": 11, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fidna%2Fintranges.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fidna%2Fintranges.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fidna%2Fintranges.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -8,6 +8,7 @@\n import bisect\n from typing import List, Tuple\n \n+\n def intranges_from_list(list_: List[int]) -> Tuple[int, ...]:\n \"\"\"Represent a list of integers as a sequence of ranges:\n ((start_0, end_0), (start_1, end_1), ...), such that the original\n@@ -20,18 +21,20 @@ def intranges_from_list(list_: List[int]) -> Tuple[int, ...]:\n ranges = []\n last_write = -1\n for i in range(len(sorted_list)):\n- if i+1 < len(sorted_list):\n- if sorted_list[i] == sorted_list[i+1]-1:\n+ if i + 1 < len(sorted_list):\n+ if sorted_list[i] == sorted_list[i + 1] - 1:\n continue\n- current_range = sorted_list[last_write+1:i+1]\n+ current_range = sorted_list[last_write + 1 : i + 1]\n ranges.append(_encode_range(current_range[0], current_range[-1] + 1))\n last_write = i\n \n return tuple(ranges)\n \n+\n def _encode_range(start: int, end: int) -> int:\n return (start << 32) | end\n \n+\n def _decode_range(r: int) -> Tuple[int, int]:\n return (r >> 32), (r & ((1 << 32) - 1))\n \n@@ -43,7 +46,7 @@ def intranges_contain(int_: int, ranges: Tuple[int, ...]) -> bool:\n # we could be immediately ahead of a tuple (start, end)\n # with start < int_ <= end\n if pos > 0:\n- left, right = _decode_range(ranges[pos-1])\n+ left, right = _decode_range(ranges[pos - 1])\n if left <= int_ < right:\n return True\n # or we could be immediately behind a tuple (int_, end)"}, {"sha": "514ff7e2e68b65f309d30a0b06e6b290d2c353a8", "filename": "lib/idna/package_data.py", "status": "modified", "additions": 1, "deletions": 2, "changes": 3, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fidna%2Fpackage_data.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fidna%2Fpackage_data.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fidna%2Fpackage_data.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,2 +1 @@\n-__version__ = '3.7'\n-\n+__version__ = \"3.10\""}, {"sha": "eb894327410debecb64ddf40eddc3131cf8344de", "filename": "lib/idna/uts46data.py", "status": "modified", "additions": 8261, "deletions": 8178, "changes": 16439, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fidna%2Futs46data.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fidna%2Futs46data.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fidna%2Futs46data.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "46a14e640dacff5de6ccdf5b66a1003b921ffcdc", "filename": "lib/importlib_metadata/__init__.py", "status": "modified", "additions": 66, "deletions": 28, "changes": 94, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_metadata%2F__init__.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_metadata%2F__init__.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_metadata%2F__init__.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,24 +1,34 @@\n+\"\"\"\n+APIs exposing metadata from third-party Python packages.\n+\n+This codebase is shared between importlib.metadata in the stdlib\n+and importlib_metadata in PyPI. See\n+https://github.com/python/importlib_metadata/wiki/Development-Methodology\n+for more detail.\n+\"\"\"\n+\n from __future__ import annotations\n \n-import os\n-import re\n import abc\n-import sys\n-import json\n-import zipp\n+import collections\n import email\n-import types\n-import inspect\n-import pathlib\n-import operator\n-import textwrap\n import functools\n import itertools\n+import operator\n+import os\n+import pathlib\n import posixpath\n-import collections\n+import re\n+import sys\n+import textwrap\n+import types\n+from contextlib import suppress\n+from importlib import import_module\n+from importlib.abc import MetaPathFinder\n+from itertools import starmap\n+from typing import Any, Iterable, List, Mapping, Match, Optional, Set, cast\n \n from . import _meta\n-from .compat import py39, py311\n from ._collections import FreezableDefaultDict, Pair\n from ._compat import (\n NullFinder,\n@@ -27,12 +37,7 @@\n from ._functools import method_cache, pass_none\n from ._itertools import always_iterable, bucket, unique_everseen\n from ._meta import PackageMetadata, SimplePath\n-\n-from contextlib import suppress\n-from importlib import import_module\n-from importlib.abc import MetaPathFinder\n-from itertools import starmap\n-from typing import Any, Iterable, List, Mapping, Match, Optional, Set, cast\n+from .compat import py39, py311\n \n __all__ = [\n 'Distribution',\n@@ -58,7 +63,7 @@ def __str__(self) -> str:\n return f\"No package metadata was found for {self.name}\"\n \n @property\n- def name(self) -> str: # type: ignore[override]\n+ def name(self) -> str: # type: ignore[override] # make readonly\n (name,) = self.args\n return name\n \n@@ -227,9 +232,26 @@ def matches(self, **params):\n >>> ep.matches(attr='bong')\n True\n \"\"\"\n+ self._disallow_dist(params)\n attrs = (getattr(self, param) for param in params)\n return all(map(operator.eq, params.values(), attrs))\n \n+ @staticmethod\n+ def _disallow_dist(params):\n+ \"\"\"\n+ Querying by dist is not allowed (dist objects are not comparable).\n+ >>> EntryPoint(name='fan', value='fav', group='fag').matches(dist='foo')\n+ Traceback (most recent call last):\n+ ...\n+ ValueError: \"dist\" is not suitable for matching...\n+ \"\"\"\n+ if \"dist\" in params:\n+ raise ValueError(\n+ '\"dist\" is not suitable for matching. '\n+ \"Instead, use Distribution.entry_points.select() on a \"\n+ \"located distribution.\"\n+ )\n+\n def _key(self):\n return self.name, self.value, self.group\n \n@@ -259,7 +281,7 @@ class EntryPoints(tuple):\n \n __slots__ = ()\n \n- def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override]\n+ def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override] # Work with str instead of int\n \"\"\"\n Get the EntryPoint in self matching name.\n \"\"\"\n@@ -315,7 +337,7 @@ class PackagePath(pathlib.PurePosixPath):\n size: int\n dist: Distribution\n \n- def read_text(self, encoding: str = 'utf-8') -> str: # type: ignore[override]\n+ def read_text(self, encoding: str = 'utf-8') -> str:\n return self.locate().read_text(encoding=encoding)\n \n def read_binary(self) -> bytes:\n@@ -373,6 +395,17 @@ def locate_file(self, path: str | os.PathLike[str]) -> SimplePath:\n \"\"\"\n Given a path to a file in this distribution, return a SimplePath\n to it.\n+\n+ This method is used by callers of ``Distribution.files()`` to\n+ locate files within the distribution. If it's possible for a\n+ Distribution to represent files in the distribution as\n+ ``SimplePath`` objects, it should implement this method\n+ to resolve such objects.\n+\n+ Some Distribution providers may elect not to resolve SimplePath\n+ objects within the distribution by raising a\n+ NotImplementedError, but consumers of such a Distribution would\n+ be unable to invoke ``Distribution.files()``.\n \"\"\"\n \n @classmethod\n@@ -639,6 +672,9 @@ def origin(self):\n return self._load_json('direct_url.json')\n \n def _load_json(self, filename):\n+ # Deferred for performance (python/importlib_metadata#503)\n+ import json\n+\n return pass_none(json.loads)(\n self.read_text(filename),\n object_hook=lambda data: types.SimpleNamespace(**data),\n@@ -723,7 +759,7 @@ class FastPath:\n True\n \"\"\"\n \n- @functools.lru_cache() # type: ignore\n+ @functools.lru_cache() # type: ignore[misc]\n def __new__(cls, root):\n return super().__new__(cls)\n \n@@ -741,7 +777,10 @@ def children(self):\n return []\n \n def zip_children(self):\n- zip_path = zipp.Path(self.root)\n+ # deferred for performance (python/importlib_metadata#502)\n+ from zipp.compat.overlay import zipfile\n+\n+ zip_path = zipfile.Path(self.root)\n names = zip_path.root.namelist()\n self.joinpath = zip_path.joinpath\n \n@@ -1078,11 +1117,10 @@ def _get_toplevel_name(name: PackagePath) -> str:\n >>> _get_toplevel_name(PackagePath('foo.dist-info'))\n 'foo.dist-info'\n \"\"\"\n- return _topmost(name) or (\n- # python/typeshed#10328\n- inspect.getmodulename(name) # type: ignore\n- or str(name)\n- )\n+ # Defer import of inspect for performance (python/cpython#118761)\n+ import inspect\n+\n+ return _topmost(name) or inspect.getmodulename(name) or str(name)\n \n \n def _top_level_inferred(dist):"}, {"sha": "3b516a2d066f6545dbcb343505a5c453b2ddafaf", "filename": "lib/importlib_metadata/_adapters.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_metadata%2F_adapters.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_metadata%2F_adapters.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_metadata%2F_adapters.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,6 +1,6 @@\n+import email.message\n import re\n import textwrap\n-import email.message\n \n from ._text import FoldedCase\n "}, {"sha": "01356d69b97c95a6d41818e5c2c50a299146bef4", "filename": "lib/importlib_metadata/_compat.py", "status": "modified", "additions": 1, "deletions": 2, "changes": 3, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_metadata%2F_compat.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_metadata%2F_compat.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_metadata%2F_compat.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,6 +1,5 @@\n-import sys\n import platform\n-\n+import sys\n \n __all__ = ['install', 'NullFinder']\n "}, {"sha": "5dda6a2199ad0be79351899a583b98c48eda4938", "filename": "lib/importlib_metadata/_functools.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_metadata%2F_functools.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_metadata%2F_functools.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_metadata%2F_functools.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,5 +1,5 @@\n-import types\n import functools\n+import types\n \n \n # from jaraco.functools 3.3"}, {"sha": "0942bbd963ae3f622d23dcbcdf8821593bee8101", "filename": "lib/importlib_metadata/_meta.py", "status": "modified", "additions": 11, "deletions": 3, "changes": 14, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_metadata%2F_meta.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_metadata%2F_meta.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_metadata%2F_meta.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,9 +1,17 @@\n from __future__ import annotations\n \n import os\n-from typing import Protocol\n-from typing import Any, Dict, Iterator, List, Optional, TypeVar, Union, overload\n-\n+from typing import (\n+ Any,\n+ Dict,\n+ Iterator,\n+ List,\n+ Optional,\n+ Protocol,\n+ TypeVar,\n+ Union,\n+ overload,\n+)\n \n _T = TypeVar(\"_T\")\n "}, {"sha": "723c9f9eb33ce1e5f7fb5a70c5c9d62628a93521", "filename": "lib/importlib_resources/__init__.py", "status": "modified", "additions": 9, "deletions": 2, "changes": 11, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2F__init__.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2F__init__.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2F__init__.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,4 +1,11 @@\n-\"\"\"Read resources contained within a package.\"\"\"\n+\"\"\"\n+Read resources contained within a package.\n+\n+This codebase is shared between importlib.resources in the stdlib\n+and importlib_resources in PyPI. See\n+https://github.com/python/importlib_metadata/wiki/Development-Methodology\n+for more detail.\n+\"\"\"\n \n from ._common import (\n as_file,\n@@ -7,7 +14,7 @@\n Anchor,\n )\n \n-from .functional import (\n+from ._functional import (\n contents,\n is_resource,\n open_binary,"}, {"sha": "f065d493fb7e16e235c2c5b9bd7cf02cfcb478b7", "filename": "lib/importlib_resources/_common.py", "status": "modified", "additions": 6, "deletions": 5, "changes": 11, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2F_common.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2F_common.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2F_common.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -66,10 +66,10 @@ def get_resource_reader(package: types.ModuleType) -> Optional[ResourceReader]:\n # zipimport.zipimporter does not support weak references, resulting in a\n # TypeError. That seems terrible.\n spec = package.__spec__\n- reader = getattr(spec.loader, 'get_resource_reader', None) # type: ignore\n+ reader = getattr(spec.loader, 'get_resource_reader', None) # type: ignore[union-attr]\n if reader is None:\n return None\n- return reader(spec.name) # type: ignore\n+ return reader(spec.name) # type: ignore[union-attr]\n \n \n @functools.singledispatch\n@@ -93,12 +93,13 @@ def _infer_caller():\n \"\"\"\n \n def is_this_file(frame_info):\n- return frame_info.filename == __file__\n+ return frame_info.filename == stack[0].filename\n \n def is_wrapper(frame_info):\n return frame_info.function == 'wrapper'\n \n- not_this_file = itertools.filterfalse(is_this_file, inspect.stack())\n+ stack = inspect.stack()\n+ not_this_file = itertools.filterfalse(is_this_file, stack)\n # also exclude 'wrapper' due to singledispatch in the call stack\n callers = itertools.filterfalse(is_wrapper, not_this_file)\n return next(callers).frame\n@@ -182,7 +183,7 @@ def _(path):\n @contextlib.contextmanager\n def _temp_path(dir: tempfile.TemporaryDirectory):\n \"\"\"\n- Wrap tempfile.TemporyDirectory to return a pathlib object.\n+ Wrap tempfile.TemporaryDirectory to return a pathlib object.\n \"\"\"\n with dir as result:\n yield pathlib.Path(result)"}, {"sha": "f59416f2dd627d560b3e393775cc5be14c578370", "filename": "lib/importlib_resources/_functional.py", "status": "renamed", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2F_functional.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2F_functional.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2F_functional.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "previous_filename": "lib/importlib_resources/functional.py"}, {"sha": "ed5abd5e58523d65ebaef1e893b5af83dafd6e4b", "filename": "lib/importlib_resources/compat/py39.py", "status": "modified", "additions": 2, "deletions": 2, "changes": 4, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Fcompat%2Fpy39.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Fcompat%2Fpy39.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Fcompat%2Fpy39.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -5,6 +5,6 @@\n \n \n if sys.version_info >= (3, 10):\n- from zipfile import Path as ZipPath # type: ignore\n+ from zipfile import Path as ZipPath\n else:\n- from zipp import Path as ZipPath # type: ignore\n+ from zipp import Path as ZipPath"}, {"sha": "4f761c64b5355dd64a92b2df447f2c7ee5227116", "filename": "lib/importlib_resources/readers.py", "status": "modified", "additions": 17, "deletions": 8, "changes": 25, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Freaders.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Freaders.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Freaders.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,10 +1,13 @@\n+from __future__ import annotations\n+\n import collections\n import contextlib\n import itertools\n import pathlib\n import operator\n import re\n import warnings\n+from collections.abc import Iterator\n \n from . import abc\n \n@@ -34,8 +37,10 @@ def files(self):\n \n class ZipReader(abc.TraversableResources):\n def __init__(self, loader, module):\n- _, _, name = module.rpartition('.')\n- self.prefix = loader.prefix.replace('\\\\', '/') + name + '/'\n+ self.prefix = loader.prefix.replace('\\\\', '/')\n+ if loader.is_package(module):\n+ _, _, name = module.rpartition('.')\n+ self.prefix += name + '/'\n self.archive = loader.archive\n \n def open_resource(self, resource):\n@@ -133,27 +138,31 @@ class NamespaceReader(abc.TraversableResources):\n def __init__(self, namespace_path):\n if 'NamespacePath' not in str(namespace_path):\n raise ValueError('Invalid path')\n- self.path = MultiplexedPath(*map(self._resolve, namespace_path))\n+ self.path = MultiplexedPath(*filter(bool, map(self._resolve, namespace_path)))\n \n @classmethod\n- def _resolve(cls, path_str) -> abc.Traversable:\n+ def _resolve(cls, path_str) -> abc.Traversable | None:\n r\"\"\"\n Given an item from a namespace path, resolve it to a Traversable.\n \n path_str might be a directory on the filesystem or a path to a\n zipfile plus the path within the zipfile, e.g. ``/foo/bar`` or\n ``/foo/baz.zip/inner_dir`` or ``foo\\baz.zip\\inner_dir\\sub``.\n+\n+ path_str might also be a sentinel used by editable packages to\n+ trigger other behaviors (see python/importlib_resources#311).\n+ In that case, return None.\n \"\"\"\n- (dir,) = (cand for cand in cls._candidate_paths(path_str) if cand.is_dir())\n- return dir\n+ dirs = (cand for cand in cls._candidate_paths(path_str) if cand.is_dir())\n+ return next(dirs, None)\n \n @classmethod\n- def _candidate_paths(cls, path_str):\n+ def _candidate_paths(cls, path_str: str) -> Iterator[abc.Traversable]:\n yield pathlib.Path(path_str)\n yield from cls._resolve_zip_path(path_str)\n \n @staticmethod\n- def _resolve_zip_path(path_str):\n+ def _resolve_zip_path(path_str: str):\n for match in reversed(list(re.finditer(r'[\\\\/]', path_str))):\n with contextlib.suppress(\n FileNotFoundError,"}, {"sha": "2e75299b13aabf3dd11aea5a23e6723d67854fc9", "filename": "lib/importlib_resources/simple.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Fsimple.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Fsimple.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Fsimple.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -77,7 +77,7 @@ class ResourceHandle(Traversable):\n \n def __init__(self, parent: ResourceContainer, name: str):\n self.parent = parent\n- self.name = name # type: ignore\n+ self.name = name # type: ignore[misc]\n \n def is_file(self):\n return True"}, {"sha": "b144628cb73c77d7ee750dc32aab4d5a9d7b1f2f", "filename": "lib/importlib_resources/tests/_path.py", "status": "modified", "additions": 44, "deletions": 6, "changes": 50, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Ftests%2F_path.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Ftests%2F_path.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2F_path.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -2,15 +2,44 @@\n import functools\n \n from typing import Dict, Union\n+from typing import runtime_checkable\n+from typing import Protocol\n \n \n ####\n-# from jaraco.path 3.4.1\n+# from jaraco.path 3.7.1\n \n-FilesSpec = Dict[str, Union[str, bytes, 'FilesSpec']] # type: ignore\n \n+class Symlink(str):\n+ \"\"\"\n+ A string indicating the target of a symlink.\n+ \"\"\"\n+\n+\n+FilesSpec = Dict[str, Union[str, bytes, Symlink, 'FilesSpec']]\n+\n+\n+@runtime_checkable\n+class TreeMaker(Protocol):\n+ def __truediv__(self, *args, **kwargs): ... # pragma: no cover\n+\n+ def mkdir(self, **kwargs): ... # pragma: no cover\n+\n+ def write_text(self, content, **kwargs): ... # pragma: no cover\n+\n+ def write_bytes(self, content): ... # pragma: no cover\n \n-def build(spec: FilesSpec, prefix=pathlib.Path()):\n+ def symlink_to(self, target): ... # pragma: no cover\n+\n+\n+def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker:\n+ return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore[return-value]\n+\n+\n+def build(\n+ spec: FilesSpec,\n+ prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore[assignment]\n+):\n \"\"\"\n Build a set of files/directories, as described by the spec.\n \n@@ -25,21 +54,25 @@ def build(spec: FilesSpec, prefix=pathlib.Path()):\n ... \"__init__.py\": \"\",\n ... },\n ... \"baz.py\": \"# Some code\",\n- ... }\n+ ... \"bar.py\": Symlink(\"baz.py\"),\n+ ... },\n+ ... \"bing\": Symlink(\"foo\"),\n ... }\n >>> target = getfixture('tmp_path')\n >>> build(spec, target)\n >>> target.joinpath('foo/baz.py').read_text(encoding='utf-8')\n '# Some code'\n+ >>> target.joinpath('bing/bar.py').read_text(encoding='utf-8')\n+ '# Some code'\n \"\"\"\n for name, contents in spec.items():\n- create(contents, pathlib.Path(prefix) / name)\n+ create(contents, _ensure_tree_maker(prefix) / name)\n \n \n @functools.singledispatch\n def create(content: Union[str, bytes, FilesSpec], path):\n path.mkdir(exist_ok=True)\n- build(content, prefix=path) # type: ignore\n+ build(content, prefix=path) # type: ignore[arg-type]\n \n \n @create.register\n@@ -52,5 +85,10 @@ def _(content: str, path):\n path.write_text(content, encoding='utf-8')\n \n \n+@create.register\n+def _(content: Symlink, path):\n+ path.symlink_to(content)\n+\n+\n # end from jaraco.path\n ####"}, {"sha": "e01d276bd96ef5d823946b84b74d3af2710d26f9", "filename": "lib/importlib_resources/tests/compat/py39.py", "status": "modified", "additions": 3, "deletions": 0, "changes": 3, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Ftests%2Fcompat%2Fpy39.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Ftests%2Fcompat%2Fpy39.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2Fcompat%2Fpy39.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -8,3 +8,6 @@\n 'modules_setup', 'modules_cleanup', 'DirsOnSysPath'\n )\n os_helper = try_import('os_helper') or from_test_support('temp_dir')\n+warnings_helper = try_import('warnings_helper') or from_test_support(\n+ 'ignore_warnings', 'check_warnings'\n+)"}, {"sha": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", "filename": "lib/importlib_resources/tests/data01/__init__.py", "status": "removed", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fdata01%2F__init__.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fdata01%2F__init__.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2Fdata01%2F__init__.py?ref=940c2ae6cd064817512aa7646386a31fa9f465fa"}, {"sha": "eaf36c1daccfdf325514461cd1a2ffbc139b5464", "filename": "lib/importlib_resources/tests/data01/binary.file", "status": "removed", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fdata01%2Fbinary.file", "raw_url": "https://github.com/Tautulli/Tautulli/raw/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fdata01%2Fbinary.file", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2Fdata01%2Fbinary.file?ref=940c2ae6cd064817512aa7646386a31fa9f465fa"}, {"sha": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", "filename": "lib/importlib_resources/tests/data01/subdirectory/__init__.py", "status": "removed", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fdata01%2Fsubdirectory%2F__init__.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fdata01%2Fsubdirectory%2F__init__.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2Fdata01%2Fsubdirectory%2F__init__.py?ref=940c2ae6cd064817512aa7646386a31fa9f465fa"}, {"sha": "5bd8bb897b13225c93a1d26baa88c96b7bd5d817", "filename": "lib/importlib_resources/tests/data01/subdirectory/binary.file", "status": "removed", "additions": 0, "deletions": 1, "changes": 1, "blob_url": "https://github.com/Tautulli/Tautulli/blob/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fdata01%2Fsubdirectory%2Fbinary.file", "raw_url": "https://github.com/Tautulli/Tautulli/raw/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fdata01%2Fsubdirectory%2Fbinary.file", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2Fdata01%2Fsubdirectory%2Fbinary.file?ref=940c2ae6cd064817512aa7646386a31fa9f465fa", "patch": "@@ -1 +0,0 @@\n-\u0004\u0005\u0006\u0007\n\\ No newline at end of file"}, {"sha": "2cb772295ef4b480a8d83725bd5006a0236d8f68", "filename": "lib/importlib_resources/tests/data01/utf-16.file", "status": "removed", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fdata01%2Futf-16.file", "raw_url": "https://github.com/Tautulli/Tautulli/raw/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fdata01%2Futf-16.file", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2Fdata01%2Futf-16.file?ref=940c2ae6cd064817512aa7646386a31fa9f465fa"}, {"sha": "1c0132ad90a1926b64be56b6fe335de6d727aa17", "filename": "lib/importlib_resources/tests/data01/utf-8.file", "status": "removed", "additions": 0, "deletions": 1, "changes": 1, "blob_url": "https://github.com/Tautulli/Tautulli/blob/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fdata01%2Futf-8.file", "raw_url": "https://github.com/Tautulli/Tautulli/raw/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fdata01%2Futf-8.file", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2Fdata01%2Futf-8.file?ref=940c2ae6cd064817512aa7646386a31fa9f465fa", "patch": "@@ -1 +0,0 @@\n-Hello, UTF-8 world!"}, {"sha": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", "filename": "lib/importlib_resources/tests/data02/__init__.py", "status": "removed", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fdata02%2F__init__.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fdata02%2F__init__.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2Fdata02%2F__init__.py?ref=940c2ae6cd064817512aa7646386a31fa9f465fa"}, {"sha": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", "filename": "lib/importlib_resources/tests/data02/one/__init__.py", "status": "removed", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fdata02%2Fone%2F__init__.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fdata02%2Fone%2F__init__.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2Fdata02%2Fone%2F__init__.py?ref=940c2ae6cd064817512aa7646386a31fa9f465fa"}, {"sha": "61a813e40174a6ffda9ef98820a4f733c3276d68", "filename": "lib/importlib_resources/tests/data02/one/resource1.txt", "status": "removed", "additions": 0, "deletions": 1, "changes": 1, "blob_url": "https://github.com/Tautulli/Tautulli/blob/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fdata02%2Fone%2Fresource1.txt", "raw_url": "https://github.com/Tautulli/Tautulli/raw/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fdata02%2Fone%2Fresource1.txt", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2Fdata02%2Fone%2Fresource1.txt?ref=940c2ae6cd064817512aa7646386a31fa9f465fa", "patch": "@@ -1 +0,0 @@\n-one resource"}, {"sha": "48f587a2d0ac53d475f7d91195032d3e24a0b330", "filename": "lib/importlib_resources/tests/data02/subdirectory/subsubdir/resource.txt", "status": "removed", "additions": 0, "deletions": 1, "changes": 1, "blob_url": "https://github.com/Tautulli/Tautulli/blob/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fdata02%2Fsubdirectory%2Fsubsubdir%2Fresource.txt", "raw_url": "https://github.com/Tautulli/Tautulli/raw/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fdata02%2Fsubdirectory%2Fsubsubdir%2Fresource.txt", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2Fdata02%2Fsubdirectory%2Fsubsubdir%2Fresource.txt?ref=940c2ae6cd064817512aa7646386a31fa9f465fa", "patch": "@@ -1 +0,0 @@\n-a resource\n\\ No newline at end of file"}, {"sha": "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", "filename": "lib/importlib_resources/tests/data02/two/__init__.py", "status": "removed", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fdata02%2Ftwo%2F__init__.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fdata02%2Ftwo%2F__init__.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2Fdata02%2Ftwo%2F__init__.py?ref=940c2ae6cd064817512aa7646386a31fa9f465fa"}, {"sha": "a80ce46ea362e2019c489c6cf7a63e833acb7fbd", "filename": "lib/importlib_resources/tests/data02/two/resource2.txt", "status": "removed", "additions": 0, "deletions": 1, "changes": 1, "blob_url": "https://github.com/Tautulli/Tautulli/blob/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fdata02%2Ftwo%2Fresource2.txt", "raw_url": "https://github.com/Tautulli/Tautulli/raw/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fdata02%2Ftwo%2Fresource2.txt", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2Fdata02%2Ftwo%2Fresource2.txt?ref=940c2ae6cd064817512aa7646386a31fa9f465fa", "patch": "@@ -1 +0,0 @@\n-two resource"}, {"sha": "eaf36c1daccfdf325514461cd1a2ffbc139b5464", "filename": "lib/importlib_resources/tests/namespacedata01/binary.file", "status": "removed", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fnamespacedata01%2Fbinary.file", "raw_url": "https://github.com/Tautulli/Tautulli/raw/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fnamespacedata01%2Fbinary.file", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2Fnamespacedata01%2Fbinary.file?ref=940c2ae6cd064817512aa7646386a31fa9f465fa"}, {"sha": "100f50643d8d21785f77f83997f2a74c58029a1e", "filename": "lib/importlib_resources/tests/namespacedata01/subdirectory/binary.file", "status": "removed", "additions": 0, "deletions": 1, "changes": 1, "blob_url": "https://github.com/Tautulli/Tautulli/blob/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fnamespacedata01%2Fsubdirectory%2Fbinary.file", "raw_url": "https://github.com/Tautulli/Tautulli/raw/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fnamespacedata01%2Fsubdirectory%2Fbinary.file", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2Fnamespacedata01%2Fsubdirectory%2Fbinary.file?ref=940c2ae6cd064817512aa7646386a31fa9f465fa", "patch": "@@ -1 +0,0 @@\n-\f\r\u000e\u000f\n\\ No newline at end of file"}, {"sha": "2cb772295ef4b480a8d83725bd5006a0236d8f68", "filename": "lib/importlib_resources/tests/namespacedata01/utf-16.file", "status": "removed", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fnamespacedata01%2Futf-16.file", "raw_url": "https://github.com/Tautulli/Tautulli/raw/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fnamespacedata01%2Futf-16.file", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2Fnamespacedata01%2Futf-16.file?ref=940c2ae6cd064817512aa7646386a31fa9f465fa"}, {"sha": "1c0132ad90a1926b64be56b6fe335de6d727aa17", "filename": "lib/importlib_resources/tests/namespacedata01/utf-8.file", "status": "removed", "additions": 0, "deletions": 1, "changes": 1, "blob_url": "https://github.com/Tautulli/Tautulli/blob/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fnamespacedata01%2Futf-8.file", "raw_url": "https://github.com/Tautulli/Tautulli/raw/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fimportlib_resources%2Ftests%2Fnamespacedata01%2Futf-8.file", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2Fnamespacedata01%2Futf-8.file?ref=940c2ae6cd064817512aa7646386a31fa9f465fa", "patch": "@@ -1 +0,0 @@\n-Hello, UTF-8 world!"}, {"sha": "741a740761a26b589dfdede1334e7dcfa0107d3b", "filename": "lib/importlib_resources/tests/test_contents.py", "status": "modified", "additions": 5, "deletions": 10, "changes": 15, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Ftests%2Ftest_contents.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Ftests%2Ftest_contents.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2Ftest_contents.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,7 +1,6 @@\n import unittest\n import importlib_resources as resources\n \n-from . import data01\n from . import util\n \n \n@@ -19,25 +18,21 @@ def test_contents(self):\n assert self.expected <= contents\n \n \n-class ContentsDiskTests(ContentsTests, unittest.TestCase):\n- def setUp(self):\n- self.data = data01\n+class ContentsDiskTests(ContentsTests, util.DiskSetup, unittest.TestCase):\n+ pass\n \n \n class ContentsZipTests(ContentsTests, util.ZipSetup, unittest.TestCase):\n pass\n \n \n-class ContentsNamespaceTests(ContentsTests, unittest.TestCase):\n+class ContentsNamespaceTests(ContentsTests, util.DiskSetup, unittest.TestCase):\n+ MODULE = 'namespacedata01'\n+\n expected = {\n # no __init__ because of namespace design\n 'binary.file',\n 'subdirectory',\n 'utf-16.file',\n 'utf-8.file',\n }\n-\n- def setUp(self):\n- from . import namespacedata01\n-\n- self.data = namespacedata01"}, {"sha": "f1fe2337a23ca46e2538f0f857a8e77aba910528", "filename": "lib/importlib_resources/tests/test_files.py", "status": "modified", "additions": 117, "deletions": 40, "changes": 157, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Ftests%2Ftest_files.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Ftests%2Ftest_files.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2Ftest_files.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,3 +1,7 @@\n+import os\n+import pathlib\n+import py_compile\n+import shutil\n import textwrap\n import unittest\n import warnings\n@@ -6,11 +10,8 @@\n \n import importlib_resources as resources\n from ..abc import Traversable\n-from . import data01\n from . import util\n-from . import _path\n-from .compat.py39 import os_helper\n-from .compat.py312 import import_helper\n+from .compat.py39 import os_helper, import_helper\n \n \n @contextlib.contextmanager\n@@ -48,70 +49,146 @@ def test_old_parameter(self):\n resources.files(package=self.data)\n \n \n-class OpenDiskTests(FilesTests, unittest.TestCase):\n- def setUp(self):\n- self.data = data01\n+class OpenDiskTests(FilesTests, util.DiskSetup, unittest.TestCase):\n+ pass\n \n \n class OpenZipTests(FilesTests, util.ZipSetup, unittest.TestCase):\n pass\n \n \n-class OpenNamespaceTests(FilesTests, unittest.TestCase):\n- def setUp(self):\n- from . import namespacedata01\n+class OpenNamespaceTests(FilesTests, util.DiskSetup, unittest.TestCase):\n+ MODULE = 'namespacedata01'\n+\n+ def test_non_paths_in_dunder_path(self):\n+ \"\"\"\n+ Non-path items in a namespace package's ``__path__`` are ignored.\n+\n+ As reported in python/importlib_resources#311, some tools\n+ like Setuptools, when creating editable packages, will inject\n+ non-paths into a namespace package's ``__path__``, a\n+ sentinel like\n+ ``__editable__.sample_namespace-1.0.finder.__path_hook__``\n+ to cause the ``PathEntryFinder`` to be called when searching\n+ for packages. In that case, resources should still be loadable.\n+ \"\"\"\n+ import namespacedata01\n+\n+ namespacedata01.__path__.append(\n+ '__editable__.sample_namespace-1.0.finder.__path_hook__'\n+ )\n \n- self.data = namespacedata01\n+ resources.files(namespacedata01)\n \n \n class OpenNamespaceZipTests(FilesTests, util.ZipSetup, unittest.TestCase):\n ZIP_MODULE = 'namespacedata01'\n \n \n-class SiteDir:\n- def setUp(self):\n- self.fixtures = contextlib.ExitStack()\n- self.addCleanup(self.fixtures.close)\n- self.site_dir = self.fixtures.enter_context(os_helper.temp_dir())\n- self.fixtures.enter_context(import_helper.DirsOnSysPath(self.site_dir))\n- self.fixtures.enter_context(import_helper.isolated_modules())\n+class DirectSpec:\n+ \"\"\"\n+ Override behavior of ModuleSetup to write a full spec directly.\n+ \"\"\"\n \n+ MODULE = 'unused'\n+\n+ def load_fixture(self, name):\n+ self.tree_on_path(self.spec)\n+\n+\n+class ModulesFiles:\n+ spec = {\n+ 'mod.py': '',\n+ 'res.txt': 'resources are the best',\n+ }\n \n-class ModulesFilesTests(SiteDir, unittest.TestCase):\n def test_module_resources(self):\n \"\"\"\n A module can have resources found adjacent to the module.\n \"\"\"\n- spec = {\n- 'mod.py': '',\n- 'res.txt': 'resources are the best',\n- }\n- _path.build(spec, self.site_dir)\n- import mod\n+ import mod # type: ignore[import-not-found]\n \n actual = resources.files(mod).joinpath('res.txt').read_text(encoding='utf-8')\n- assert actual == spec['res.txt']\n+ assert actual == self.spec['res.txt']\n \n \n-class ImplicitContextFilesTests(SiteDir, unittest.TestCase):\n- def test_implicit_files(self):\n+class ModuleFilesDiskTests(DirectSpec, util.DiskSetup, ModulesFiles, unittest.TestCase):\n+ pass\n+\n+\n+class ModuleFilesZipTests(DirectSpec, util.ZipSetup, ModulesFiles, unittest.TestCase):\n+ pass\n+\n+\n+class ImplicitContextFiles:\n+ set_val = textwrap.dedent(\n+ f\"\"\"\n+ import {resources.__name__} as res\n+ val = res.files().joinpath('res.txt').read_text(encoding='utf-8')\n+ \"\"\"\n+ )\n+ spec = {\n+ 'somepkg': {\n+ '__init__.py': set_val,\n+ 'submod.py': set_val,\n+ 'res.txt': 'resources are the best',\n+ },\n+ 'frozenpkg': {\n+ '__init__.py': set_val.replace(resources.__name__, 'c_resources'),\n+ 'res.txt': 'resources are the best',\n+ },\n+ }\n+\n+ def test_implicit_files_package(self):\n \"\"\"\n Without any parameter, files() will infer the location as the caller.\n \"\"\"\n- spec = {\n- 'somepkg': {\n- '__init__.py': textwrap.dedent(\n- \"\"\"\n- import importlib_resources as res\n- val = res.files().joinpath('res.txt').read_text(encoding='utf-8')\n- \"\"\"\n- ),\n- 'res.txt': 'resources are the best',\n- },\n- }\n- _path.build(spec, self.site_dir)\n assert importlib.import_module('somepkg').val == 'resources are the best'\n \n+ def test_implicit_files_submodule(self):\n+ \"\"\"\n+ Without any parameter, files() will infer the location as the caller.\n+ \"\"\"\n+ assert importlib.import_module('somepkg.submod').val == 'resources are the best'\n+\n+ def _compile_importlib(self):\n+ \"\"\"\n+ Make a compiled-only copy of the importlib resources package.\n+ \"\"\"\n+ bin_site = self.fixtures.enter_context(os_helper.temp_dir())\n+ c_resources = pathlib.Path(bin_site, 'c_resources')\n+ sources = pathlib.Path(resources.__file__).parent\n+ shutil.copytree(sources, c_resources, ignore=lambda *_: ['__pycache__'])\n+\n+ for dirpath, _, filenames in os.walk(c_resources):\n+ for filename in filenames:\n+ source_path = pathlib.Path(dirpath) / filename\n+ cfile = source_path.with_suffix('.pyc')\n+ py_compile.compile(source_path, cfile)\n+ pathlib.Path.unlink(source_path)\n+ self.fixtures.enter_context(import_helper.DirsOnSysPath(bin_site))\n+\n+ def test_implicit_files_with_compiled_importlib(self):\n+ \"\"\"\n+ Caller detection works for compiled-only resources module.\n+\n+ python/cpython#123085\n+ \"\"\"\n+ self._compile_importlib()\n+ assert importlib.import_module('frozenpkg').val == 'resources are the best'\n+\n+\n+class ImplicitContextFilesDiskTests(\n+ DirectSpec, util.DiskSetup, ImplicitContextFiles, unittest.TestCase\n+):\n+ pass\n+\n+\n+class ImplicitContextFilesZipTests(\n+ DirectSpec, util.ZipSetup, ImplicitContextFiles, unittest.TestCase\n+):\n+ pass\n+\n \n if __name__ == '__main__':\n unittest.main()"}, {"sha": "1851edfba9f7313f2007dacb26515b06724ed287", "filename": "lib/importlib_resources/tests/test_functional.py", "status": "modified", "additions": 43, "deletions": 30, "changes": 73, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Ftests%2Ftest_functional.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Ftests%2Ftest_functional.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2Ftest_functional.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,31 +1,38 @@\n import unittest\n import os\n-import contextlib\n+import importlib\n \n-try:\n- from test.support.warnings_helper import ignore_warnings, check_warnings\n-except ImportError:\n- # older Python versions\n- from test.support import ignore_warnings, check_warnings\n+from .compat.py39 import warnings_helper\n \n import importlib_resources as resources\n \n+from . import util\n+\n # Since the functional API forwards to Traversable, we only test\n # filesystem resources here -- not zip files, namespace packages etc.\n # We do test for two kinds of Anchor, though.\n \n \n class StringAnchorMixin:\n- anchor01 = 'importlib_resources.tests.data01'\n- anchor02 = 'importlib_resources.tests.data02'\n+ anchor01 = 'data01'\n+ anchor02 = 'data02'\n \n \n class ModuleAnchorMixin:\n- from . import data01 as anchor01\n- from . import data02 as anchor02\n+ @property\n+ def anchor01(self):\n+ return importlib.import_module('data01')\n+\n+ @property\n+ def anchor02(self):\n+ return importlib.import_module('data02')\n+\n \n+class FunctionalAPIBase(util.DiskSetup):\n+ def setUp(self):\n+ super().setUp()\n+ self.load_fixture('data02')\n \n-class FunctionalAPIBase:\n def _gen_resourcetxt_path_parts(self):\n \"\"\"Yield various names of a text file in anchor02, each in a subTest\"\"\"\n for path_parts in (\n@@ -36,6 +43,12 @@ def _gen_resourcetxt_path_parts(self):\n with self.subTest(path_parts=path_parts):\n yield path_parts\n \n+ def assertEndsWith(self, string, suffix):\n+ \"\"\"Assert that `string` ends with `suffix`.\n+\n+ Used to ignore an architecture-specific UTF-16 byte-order mark.\"\"\"\n+ self.assertEqual(string[-len(suffix) :], suffix)\n+\n def test_read_text(self):\n self.assertEqual(\n resources.read_text(self.anchor01, 'utf-8.file'),\n@@ -76,13 +89,13 @@ def test_read_text(self):\n ),\n '\\x00\\x01\\x02\\x03',\n )\n- self.assertEqual(\n+ self.assertEndsWith( # ignore the BOM\n resources.read_text(\n self.anchor01,\n 'utf-16.file',\n errors='backslashreplace',\n ),\n- 'Hello, UTF-16 world!\\n'.encode('utf-16').decode(\n+ 'Hello, UTF-16 world!\\n'.encode('utf-16-le').decode(\n errors='backslashreplace',\n ),\n )\n@@ -128,9 +141,9 @@ def test_open_text(self):\n 'utf-16.file',\n errors='backslashreplace',\n ) as f:\n- self.assertEqual(\n+ self.assertEndsWith( # ignore the BOM\n f.read(),\n- 'Hello, UTF-16 world!\\n'.encode('utf-16').decode(\n+ 'Hello, UTF-16 world!\\n'.encode('utf-16-le').decode(\n errors='backslashreplace',\n ),\n )\n@@ -163,32 +176,32 @@ def test_is_resource(self):\n self.assertTrue(is_resource(self.anchor02, *path_parts))\n \n def test_contents(self):\n- with check_warnings((\".*contents.*\", DeprecationWarning)):\n+ with warnings_helper.check_warnings((\".*contents.*\", DeprecationWarning)):\n c = resources.contents(self.anchor01)\n self.assertGreaterEqual(\n set(c),\n {'utf-8.file', 'utf-16.file', 'binary.file', 'subdirectory'},\n )\n- with contextlib.ExitStack() as cm:\n- cm.enter_context(self.assertRaises(OSError))\n- cm.enter_context(check_warnings((\".*contents.*\", DeprecationWarning)))\n-\n+ with self.assertRaises(OSError), warnings_helper.check_warnings((\n+ \".*contents.*\",\n+ DeprecationWarning,\n+ )):\n list(resources.contents(self.anchor01, 'utf-8.file'))\n \n for path_parts in self._gen_resourcetxt_path_parts():\n- with contextlib.ExitStack() as cm:\n- cm.enter_context(self.assertRaises(OSError))\n- cm.enter_context(check_warnings((\".*contents.*\", DeprecationWarning)))\n-\n+ with self.assertRaises(OSError), warnings_helper.check_warnings((\n+ \".*contents.*\",\n+ DeprecationWarning,\n+ )):\n list(resources.contents(self.anchor01, *path_parts))\n- with check_warnings((\".*contents.*\", DeprecationWarning)):\n+ with warnings_helper.check_warnings((\".*contents.*\", DeprecationWarning)):\n c = resources.contents(self.anchor01, 'subdirectory')\n self.assertGreaterEqual(\n set(c),\n {'binary.file'},\n )\n \n- @ignore_warnings(category=DeprecationWarning)\n+ @warnings_helper.ignore_warnings(category=DeprecationWarning)\n def test_common_errors(self):\n for func in (\n resources.read_text,\n@@ -227,16 +240,16 @@ def test_text_errors(self):\n \n \n class FunctionalAPITest_StringAnchor(\n- unittest.TestCase,\n- FunctionalAPIBase,\n StringAnchorMixin,\n+ FunctionalAPIBase,\n+ unittest.TestCase,\n ):\n pass\n \n \n class FunctionalAPITest_ModuleAnchor(\n- unittest.TestCase,\n- FunctionalAPIBase,\n ModuleAnchorMixin,\n+ FunctionalAPIBase,\n+ unittest.TestCase,\n ):\n pass"}, {"sha": "c40bb8c6f43b4d2f3e472907bdaa8da33d86043c", "filename": "lib/importlib_resources/tests/test_open.py", "status": "modified", "additions": 5, "deletions": 10, "changes": 15, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Ftests%2Ftest_open.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Ftests%2Ftest_open.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2Ftest_open.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,7 +1,6 @@\n import unittest\n \n import importlib_resources as resources\n-from . import data01\n from . import util\n \n \n@@ -65,24 +64,20 @@ def test_open_text_FileNotFoundError(self):\n target.open(encoding='utf-8')\n \n \n-class OpenDiskTests(OpenTests, unittest.TestCase):\n- def setUp(self):\n- self.data = data01\n-\n+class OpenDiskTests(OpenTests, util.DiskSetup, unittest.TestCase):\n+ pass\n \n-class OpenDiskNamespaceTests(OpenTests, unittest.TestCase):\n- def setUp(self):\n- from . import namespacedata01\n \n- self.data = namespacedata01\n+class OpenDiskNamespaceTests(OpenTests, util.DiskSetup, unittest.TestCase):\n+ MODULE = 'namespacedata01'\n \n \n class OpenZipTests(OpenTests, util.ZipSetup, unittest.TestCase):\n pass\n \n \n class OpenNamespaceZipTests(OpenTests, util.ZipSetup, unittest.TestCase):\n- ZIP_MODULE = 'namespacedata01'\n+ MODULE = 'namespacedata01'\n \n \n if __name__ == '__main__':"}, {"sha": "1e30f2bc53efc916121135bad14692ac96fb8739", "filename": "lib/importlib_resources/tests/test_path.py", "status": "modified", "additions": 1, "deletions": 4, "changes": 5, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Ftests%2Ftest_path.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Ftests%2Ftest_path.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2Ftest_path.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -3,7 +3,6 @@\n import unittest\n \n import importlib_resources as resources\n-from . import data01\n from . import util\n \n \n@@ -25,9 +24,7 @@ def test_reading(self):\n self.assertEqual('Hello, UTF-8 world!\\n', path.read_text(encoding='utf-8'))\n \n \n-class PathDiskTests(PathTests, unittest.TestCase):\n- data = data01\n-\n+class PathDiskTests(PathTests, util.DiskSetup, unittest.TestCase):\n def test_natural_path(self):\n \"\"\"\n Guarantee the internal implementation detail that"}, {"sha": "6780a2d161bdf3bc5af5d7666ea735e1a9359483", "filename": "lib/importlib_resources/tests/test_read.py", "status": "modified", "additions": 5, "deletions": 9, "changes": 14, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Ftests%2Ftest_read.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Ftests%2Ftest_read.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2Ftest_read.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,7 +1,6 @@\n import unittest\n import importlib_resources as resources\n \n-from . import data01\n from . import util\n from importlib import import_module\n \n@@ -52,8 +51,8 @@ def test_read_text_with_errors(self):\n )\n \n \n-class ReadDiskTests(ReadTests, unittest.TestCase):\n- data = data01\n+class ReadDiskTests(ReadTests, util.DiskSetup, unittest.TestCase):\n+ pass\n \n \n class ReadZipTests(ReadTests, util.ZipSetup, unittest.TestCase):\n@@ -69,15 +68,12 @@ def test_read_submodule_resource_by_name(self):\n self.assertEqual(result, bytes(range(4, 8)))\n \n \n-class ReadNamespaceTests(ReadTests, unittest.TestCase):\n- def setUp(self):\n- from . import namespacedata01\n-\n- self.data = namespacedata01\n+class ReadNamespaceTests(ReadTests, util.DiskSetup, unittest.TestCase):\n+ MODULE = 'namespacedata01'\n \n \n class ReadNamespaceZipTests(ReadTests, util.ZipSetup, unittest.TestCase):\n- ZIP_MODULE = 'namespacedata01'\n+ MODULE = 'namespacedata01'\n \n def test_read_submodule_resource(self):\n submodule = import_module('namespacedata01.subdirectory')"}, {"sha": "0a77eb4045086f74439dceed367be6bf72bd791d", "filename": "lib/importlib_resources/tests/test_reader.py", "status": "modified", "additions": 20, "deletions": 28, "changes": 48, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Ftests%2Ftest_reader.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Ftests%2Ftest_reader.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2Ftest_reader.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,16 +1,21 @@\n import os.path\n-import sys\n import pathlib\n import unittest\n \n from importlib import import_module\n from importlib_resources.readers import MultiplexedPath, NamespaceReader\n \n+from . import util\n \n-class MultiplexedPathTest(unittest.TestCase):\n- @classmethod\n- def setUpClass(cls):\n- cls.folder = pathlib.Path(__file__).parent / 'namespacedata01'\n+\n+class MultiplexedPathTest(util.DiskSetup, unittest.TestCase):\n+ MODULE = 'namespacedata01'\n+\n+ def setUp(self):\n+ super().setUp()\n+ self.folder = pathlib.Path(self.data.__path__[0])\n+ self.data01 = pathlib.Path(self.load_fixture('data01').__file__).parent\n+ self.data02 = pathlib.Path(self.load_fixture('data02').__file__).parent\n \n def test_init_no_paths(self):\n with self.assertRaises(FileNotFoundError):\n@@ -31,9 +36,8 @@ def test_iterdir(self):\n )\n \n def test_iterdir_duplicate(self):\n- data01 = pathlib.Path(__file__).parent.joinpath('data01')\n contents = {\n- path.name for path in MultiplexedPath(self.folder, data01).iterdir()\n+ path.name for path in MultiplexedPath(self.folder, self.data01).iterdir()\n }\n for remove in ('__pycache__', '__init__.pyc'):\n try:\n@@ -61,9 +65,8 @@ def test_open_file(self):\n path.open()\n \n def test_join_path(self):\n- data01 = pathlib.Path(__file__).parent.joinpath('data01')\n- prefix = str(data01.parent)\n- path = MultiplexedPath(self.folder, data01)\n+ prefix = str(self.folder.parent)\n+ path = MultiplexedPath(self.folder, self.data01)\n self.assertEqual(\n str(path.joinpath('binary.file'))[len(prefix) + 1 :],\n os.path.join('namespacedata01', 'binary.file'),\n@@ -83,10 +86,8 @@ def test_join_path_compound(self):\n assert not path.joinpath('imaginary/foo.py').exists()\n \n def test_join_path_common_subdir(self):\n- data01 = pathlib.Path(__file__).parent.joinpath('data01')\n- data02 = pathlib.Path(__file__).parent.joinpath('data02')\n- prefix = str(data01.parent)\n- path = MultiplexedPath(data01, data02)\n+ prefix = str(self.data02.parent)\n+ path = MultiplexedPath(self.data01, self.data02)\n self.assertIsInstance(path.joinpath('subdirectory'), MultiplexedPath)\n self.assertEqual(\n str(path.joinpath('subdirectory', 'subsubdir'))[len(prefix) + 1 :],\n@@ -106,16 +107,8 @@ def test_name(self):\n )\n \n \n-class NamespaceReaderTest(unittest.TestCase):\n- site_dir = str(pathlib.Path(__file__).parent)\n-\n- @classmethod\n- def setUpClass(cls):\n- sys.path.append(cls.site_dir)\n-\n- @classmethod\n- def tearDownClass(cls):\n- sys.path.remove(cls.site_dir)\n+class NamespaceReaderTest(util.DiskSetup, unittest.TestCase):\n+ MODULE = 'namespacedata01'\n \n def test_init_error(self):\n with self.assertRaises(ValueError):\n@@ -125,7 +118,7 @@ def test_resource_path(self):\n namespacedata01 = import_module('namespacedata01')\n reader = NamespaceReader(namespacedata01.__spec__.submodule_search_locations)\n \n- root = os.path.abspath(os.path.join(__file__, '..', 'namespacedata01'))\n+ root = self.data.__path__[0]\n self.assertEqual(\n reader.resource_path('binary.file'), os.path.join(root, 'binary.file')\n )\n@@ -134,9 +127,8 @@ def test_resource_path(self):\n )\n \n def test_files(self):\n- namespacedata01 = import_module('namespacedata01')\n- reader = NamespaceReader(namespacedata01.__spec__.submodule_search_locations)\n- root = os.path.abspath(os.path.join(__file__, '..', 'namespacedata01'))\n+ reader = NamespaceReader(self.data.__spec__.submodule_search_locations)\n+ root = self.data.__path__[0]\n self.assertIsInstance(reader.files(), MultiplexedPath)\n self.assertEqual(repr(reader.files()), f\"MultiplexedPath('{root}')\")\n "}, {"sha": "a0da6a3566b7750dc90800c95d371c6d28e64067", "filename": "lib/importlib_resources/tests/test_resource.py", "status": "modified", "additions": 27, "deletions": 31, "changes": 58, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Ftests%2Ftest_resource.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Ftests%2Ftest_resource.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2Ftest_resource.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,9 +1,6 @@\n-import sys\n import unittest\n import importlib_resources as resources\n-import pathlib\n \n-from . import data01\n from . import util\n from importlib import import_module\n \n@@ -25,9 +22,8 @@ def test_is_dir(self):\n self.assertTrue(target.is_dir())\n \n \n-class ResourceDiskTests(ResourceTests, unittest.TestCase):\n- def setUp(self):\n- self.data = data01\n+class ResourceDiskTests(ResourceTests, util.DiskSetup, unittest.TestCase):\n+ pass\n \n \n class ResourceZipTests(ResourceTests, util.ZipSetup, unittest.TestCase):\n@@ -38,33 +34,39 @@ def names(traversable):\n return {item.name for item in traversable.iterdir()}\n \n \n-class ResourceLoaderTests(unittest.TestCase):\n+class ResourceLoaderTests(util.DiskSetup, unittest.TestCase):\n def test_resource_contents(self):\n package = util.create_package(\n- file=data01, path=data01.__file__, contents=['A', 'B', 'C']\n+ file=self.data, path=self.data.__file__, contents=['A', 'B', 'C']\n )\n self.assertEqual(names(resources.files(package)), {'A', 'B', 'C'})\n \n def test_is_file(self):\n package = util.create_package(\n- file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F']\n+ file=self.data,\n+ path=self.data.__file__,\n+ contents=['A', 'B', 'C', 'D/E', 'D/F'],\n )\n self.assertTrue(resources.files(package).joinpath('B').is_file())\n \n def test_is_dir(self):\n package = util.create_package(\n- file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F']\n+ file=self.data,\n+ path=self.data.__file__,\n+ contents=['A', 'B', 'C', 'D/E', 'D/F'],\n )\n self.assertTrue(resources.files(package).joinpath('D').is_dir())\n \n def test_resource_missing(self):\n package = util.create_package(\n- file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F']\n+ file=self.data,\n+ path=self.data.__file__,\n+ contents=['A', 'B', 'C', 'D/E', 'D/F'],\n )\n self.assertFalse(resources.files(package).joinpath('Z').is_file())\n \n \n-class ResourceCornerCaseTests(unittest.TestCase):\n+class ResourceCornerCaseTests(util.DiskSetup, unittest.TestCase):\n def test_package_has_no_reader_fallback(self):\n \"\"\"\n Test odd ball packages which:\n@@ -73,7 +75,7 @@ def test_package_has_no_reader_fallback(self):\n # 3. Are not in a zip file\n \"\"\"\n module = util.create_package(\n- file=data01, path=data01.__file__, contents=['A', 'B', 'C']\n+ file=self.data, path=self.data.__file__, contents=['A', 'B', 'C']\n )\n # Give the module a dummy loader.\n module.__loader__ = object()\n@@ -84,9 +86,7 @@ def test_package_has_no_reader_fallback(self):\n self.assertFalse(resources.files(module).joinpath('A').is_file())\n \n \n-class ResourceFromZipsTest01(util.ZipSetupBase, unittest.TestCase):\n- ZIP_MODULE = 'data01'\n-\n+class ResourceFromZipsTest01(util.ZipSetup, unittest.TestCase):\n def test_is_submodule_resource(self):\n submodule = import_module('data01.subdirectory')\n self.assertTrue(resources.files(submodule).joinpath('binary.file').is_file())\n@@ -117,8 +117,8 @@ def test_as_file_directory(self):\n assert not data.parent.exists()\n \n \n-class ResourceFromZipsTest02(util.ZipSetupBase, unittest.TestCase):\n- ZIP_MODULE = 'data02'\n+class ResourceFromZipsTest02(util.ZipSetup, unittest.TestCase):\n+ MODULE = 'data02'\n \n def test_unrelated_contents(self):\n \"\"\"\n@@ -135,7 +135,7 @@ def test_unrelated_contents(self):\n )\n \n \n-class DeletingZipsTest(util.ZipSetupBase, unittest.TestCase):\n+class DeletingZipsTest(util.ZipSetup, unittest.TestCase):\n \"\"\"Having accessed resources in a zip file should not keep an open\n reference to the zip.\n \"\"\"\n@@ -217,24 +217,20 @@ def test_submodule_sub_contents_by_name(self):\n self.assertEqual(contents, {'binary.file'})\n \n \n-class ResourceFromNamespaceDiskTests(ResourceFromNamespaceTests, unittest.TestCase):\n- site_dir = str(pathlib.Path(__file__).parent)\n-\n- @classmethod\n- def setUpClass(cls):\n- sys.path.append(cls.site_dir)\n-\n- @classmethod\n- def tearDownClass(cls):\n- sys.path.remove(cls.site_dir)\n+class ResourceFromNamespaceDiskTests(\n+ util.DiskSetup,\n+ ResourceFromNamespaceTests,\n+ unittest.TestCase,\n+):\n+ MODULE = 'namespacedata01'\n \n \n class ResourceFromNamespaceZipTests(\n- util.ZipSetupBase,\n+ util.ZipSetup,\n ResourceFromNamespaceTests,\n unittest.TestCase,\n ):\n- ZIP_MODULE = 'namespacedata01'\n+ MODULE = 'namespacedata01'\n \n \n if __name__ == '__main__':"}, {"sha": "a4eafac3ae4b81ca73ef9de5223ade3a9ae56580", "filename": "lib/importlib_resources/tests/util.py", "status": "modified", "additions": 57, "deletions": 15, "changes": 72, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Ftests%2Futil.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Ftests%2Futil.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2Futil.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -6,10 +6,10 @@\n import pathlib\n import contextlib\n \n-from . import data01\n from ..abc import ResourceReader\n from .compat.py39 import import_helper, os_helper\n from . import zip as zip_\n+from . import _path\n \n \n from importlib.machinery import ModuleSpec\n@@ -68,7 +68,7 @@ def create_package(file=None, path=None, is_package=True, contents=()):\n )\n \n \n-class CommonTests(metaclass=abc.ABCMeta):\n+class CommonTestsBase(metaclass=abc.ABCMeta):\n \"\"\"\n Tests shared by test_open, test_path, and test_read.\n \"\"\"\n@@ -84,34 +84,34 @@ def test_package_name(self):\n \"\"\"\n Passing in the package name should succeed.\n \"\"\"\n- self.execute(data01.__name__, 'utf-8.file')\n+ self.execute(self.data.__name__, 'utf-8.file')\n \n def test_package_object(self):\n \"\"\"\n Passing in the package itself should succeed.\n \"\"\"\n- self.execute(data01, 'utf-8.file')\n+ self.execute(self.data, 'utf-8.file')\n \n def test_string_path(self):\n \"\"\"\n Passing in a string for the path should succeed.\n \"\"\"\n path = 'utf-8.file'\n- self.execute(data01, path)\n+ self.execute(self.data, path)\n \n def test_pathlib_path(self):\n \"\"\"\n Passing in a pathlib.PurePath object for the path should succeed.\n \"\"\"\n path = pathlib.PurePath('utf-8.file')\n- self.execute(data01, path)\n+ self.execute(self.data, path)\n \n def test_importing_module_as_side_effect(self):\n \"\"\"\n The anchor package can already be imported.\n \"\"\"\n- del sys.modules[data01.__name__]\n- self.execute(data01.__name__, 'utf-8.file')\n+ del sys.modules[self.data.__name__]\n+ self.execute(self.data.__name__, 'utf-8.file')\n \n def test_missing_path(self):\n \"\"\"\n@@ -141,24 +141,66 @@ def test_useless_loader(self):\n self.execute(package, 'utf-8.file')\n \n \n-class ZipSetupBase:\n- ZIP_MODULE = 'data01'\n-\n+fixtures = dict(\n+ data01={\n+ '__init__.py': '',\n+ 'binary.file': bytes(range(4)),\n+ 'utf-16.file': 'Hello, UTF-16 world!\\n'.encode('utf-16'),\n+ 'utf-8.file': 'Hello, UTF-8 world!\\n'.encode('utf-8'),\n+ 'subdirectory': {\n+ '__init__.py': '',\n+ 'binary.file': bytes(range(4, 8)),\n+ },\n+ },\n+ data02={\n+ '__init__.py': '',\n+ 'one': {'__init__.py': '', 'resource1.txt': 'one resource'},\n+ 'two': {'__init__.py': '', 'resource2.txt': 'two resource'},\n+ 'subdirectory': {'subsubdir': {'resource.txt': 'a resource'}},\n+ },\n+ namespacedata01={\n+ 'binary.file': bytes(range(4)),\n+ 'utf-16.file': 'Hello, UTF-16 world!\\n'.encode('utf-16'),\n+ 'utf-8.file': 'Hello, UTF-8 world!\\n'.encode('utf-8'),\n+ 'subdirectory': {\n+ 'binary.file': bytes(range(12, 16)),\n+ },\n+ },\n+)\n+\n+\n+class ModuleSetup:\n def setUp(self):\n self.fixtures = contextlib.ExitStack()\n self.addCleanup(self.fixtures.close)\n \n self.fixtures.enter_context(import_helper.isolated_modules())\n+ self.data = self.load_fixture(self.MODULE)\n+\n+ def load_fixture(self, module):\n+ self.tree_on_path({module: fixtures[module]})\n+ return importlib.import_module(module)\n+\n+\n+class ZipSetup(ModuleSetup):\n+ MODULE = 'data01'\n \n+ def tree_on_path(self, spec):\n temp_dir = self.fixtures.enter_context(os_helper.temp_dir())\n modules = pathlib.Path(temp_dir) / 'zipped modules.zip'\n- src_path = pathlib.Path(__file__).parent.joinpath(self.ZIP_MODULE)\n self.fixtures.enter_context(\n- import_helper.DirsOnSysPath(str(zip_.make_zip_file(src_path, modules)))\n+ import_helper.DirsOnSysPath(str(zip_.make_zip_file(spec, modules)))\n )\n \n- self.data = importlib.import_module(self.ZIP_MODULE)\n+\n+class DiskSetup(ModuleSetup):\n+ MODULE = 'data01'\n+\n+ def tree_on_path(self, spec):\n+ temp_dir = self.fixtures.enter_context(os_helper.temp_dir())\n+ _path.build(spec, pathlib.Path(temp_dir))\n+ self.fixtures.enter_context(import_helper.DirsOnSysPath(temp_dir))\n \n \n-class ZipSetup(ZipSetupBase):\n+class CommonTests(DiskSetup, CommonTestsBase):\n pass"}, {"sha": "51ee564870c101bd447969c8433148eba597ab26", "filename": "lib/importlib_resources/tests/zip.py", "status": "modified", "additions": 10, "deletions": 16, "changes": 26, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Ftests%2Fzip.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fimportlib_resources%2Ftests%2Fzip.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fimportlib_resources%2Ftests%2Fzip.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -2,31 +2,25 @@\n Generate zip test data files.\n \"\"\"\n \n-import contextlib\n-import os\n-import pathlib\n import zipfile\n \n import zipp\n \n \n-def make_zip_file(src, dst):\n+def make_zip_file(tree, dst):\n \"\"\"\n- Zip the files in src into a new zipfile at dst.\n+ Zip the files in tree into a new zipfile at dst.\n \"\"\"\n with zipfile.ZipFile(dst, 'w') as zf:\n- for src_path, rel in walk(src):\n- dst_name = src.name / pathlib.PurePosixPath(rel.as_posix())\n- zf.write(src_path, dst_name)\n+ for name, contents in walk(tree):\n+ zf.writestr(name, contents)\n zipp.CompleteDirs.inject(zf)\n return dst\n \n \n-def walk(datapath):\n- for dirpath, dirnames, filenames in os.walk(datapath):\n- with contextlib.suppress(ValueError):\n- dirnames.remove('__pycache__')\n- for filename in filenames:\n- res = pathlib.Path(dirpath) / filename\n- rel = res.relative_to(datapath)\n- yield res, rel\n+def walk(tree, prefix=''):\n+ for name, contents in tree.items():\n+ if isinstance(contents, dict):\n+ yield from walk(contents, prefix=f'{prefix}{name}/')\n+ else:\n+ yield f'{prefix}{name}', contents"}, {"sha": "9d4b67449546e06b200aa08a4f8e4ea876eb3121", "filename": "lib/jwt/__init__.py", "status": "modified", "additions": 3, "deletions": 2, "changes": 5, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fjwt%2F__init__.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fjwt%2F__init__.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fjwt%2F__init__.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -6,7 +6,7 @@\n register_algorithm,\n unregister_algorithm,\n )\n-from .api_jwt import PyJWT, decode, encode\n+from .api_jwt import PyJWT, decode, decode_complete, encode\n from .exceptions import (\n DecodeError,\n ExpiredSignatureError,\n@@ -27,7 +27,7 @@\n )\n from .jwks_client import PyJWKClient\n \n-__version__ = \"2.9.0\"\n+__version__ = \"2.10.0\"\n \n __title__ = \"PyJWT\"\n __description__ = \"JSON Web Token implementation in Python\"\n@@ -49,6 +49,7 @@\n \"PyJWK\",\n \"PyJWKSet\",\n \"decode\",\n+ \"decode_complete\",\n \"encode\",\n \"get_unverified_header\",\n \"register_algorithm\","}, {"sha": "ccb1500fcf827b8a2c1eed0e0acf8c1699c0dbf6", "filename": "lib/jwt/algorithms.py", "status": "modified", "additions": 31, "deletions": 16, "changes": 47, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fjwt%2Falgorithms.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fjwt%2Falgorithms.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fjwt%2Falgorithms.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -297,7 +297,7 @@ def from_jwk(jwk: str | JWKDict) -> bytes:\n else:\n raise ValueError\n except ValueError:\n- raise InvalidKeyError(\"Key is not valid JSON\")\n+ raise InvalidKeyError(\"Key is not valid JSON\") from None\n \n if obj.get(\"kty\") != \"oct\":\n raise InvalidKeyError(\"Not an HMAC key\")\n@@ -346,7 +346,9 @@ def prepare_key(self, key: AllowedRSAKeys | str | bytes) -> AllowedRSAKeys:\n try:\n return cast(RSAPublicKey, load_pem_public_key(key_bytes))\n except (ValueError, UnsupportedAlgorithm):\n- raise InvalidKeyError(\"Could not parse the provided public key.\")\n+ raise InvalidKeyError(\n+ \"Could not parse the provided public key.\"\n+ ) from None\n \n @overload\n @staticmethod\n@@ -409,10 +411,10 @@ def from_jwk(jwk: str | JWKDict) -> AllowedRSAKeys:\n else:\n raise ValueError\n except ValueError:\n- raise InvalidKeyError(\"Key is not valid JSON\")\n+ raise InvalidKeyError(\"Key is not valid JSON\") from None\n \n if obj.get(\"kty\") != \"RSA\":\n- raise InvalidKeyError(\"Not an RSA key\")\n+ raise InvalidKeyError(\"Not an RSA key\") from None\n \n if \"d\" in obj and \"e\" in obj and \"n\" in obj:\n # Private key\n@@ -428,7 +430,7 @@ def from_jwk(jwk: str | JWKDict) -> AllowedRSAKeys:\n if any_props_found and not all(props_found):\n raise InvalidKeyError(\n \"RSA key must include all parameters if any are present besides d\"\n- )\n+ ) from None\n \n public_numbers = RSAPublicNumbers(\n from_base64url_uint(obj[\"e\"]),\n@@ -520,7 +522,7 @@ def prepare_key(self, key: AllowedECKeys | str | bytes) -> AllowedECKeys:\n ):\n raise InvalidKeyError(\n \"Expecting a EllipticCurvePrivateKey/EllipticCurvePublicKey. Wrong key provided for ECDSA algorithms\"\n- )\n+ ) from None\n \n return crypto_key\n \n@@ -581,13 +583,20 @@ def to_jwk(key_obj: AllowedECKeys, as_dict: bool = False) -> JWKDict | str:\n obj: dict[str, Any] = {\n \"kty\": \"EC\",\n \"crv\": crv,\n- \"x\": to_base64url_uint(public_numbers.x).decode(),\n- \"y\": to_base64url_uint(public_numbers.y).decode(),\n+ \"x\": to_base64url_uint(\n+ public_numbers.x,\n+ bit_length=key_obj.curve.key_size,\n+ ).decode(),\n+ \"y\": to_base64url_uint(\n+ public_numbers.y,\n+ bit_length=key_obj.curve.key_size,\n+ ).decode(),\n }\n \n if isinstance(key_obj, EllipticCurvePrivateKey):\n obj[\"d\"] = to_base64url_uint(\n- key_obj.private_numbers().private_value\n+ key_obj.private_numbers().private_value,\n+ bit_length=key_obj.curve.key_size,\n ).decode()\n \n if as_dict:\n@@ -605,13 +614,13 @@ def from_jwk(jwk: str | JWKDict) -> AllowedECKeys:\n else:\n raise ValueError\n except ValueError:\n- raise InvalidKeyError(\"Key is not valid JSON\")\n+ raise InvalidKeyError(\"Key is not valid JSON\") from None\n \n if obj.get(\"kty\") != \"EC\":\n- raise InvalidKeyError(\"Not an Elliptic curve key\")\n+ raise InvalidKeyError(\"Not an Elliptic curve key\") from None\n \n if \"x\" not in obj or \"y\" not in obj:\n- raise InvalidKeyError(\"Not an Elliptic curve key\")\n+ raise InvalidKeyError(\"Not an Elliptic curve key\") from None\n \n x = base64url_decode(obj.get(\"x\"))\n y = base64url_decode(obj.get(\"y\"))\n@@ -623,17 +632,23 @@ def from_jwk(jwk: str | JWKDict) -> AllowedECKeys:\n if len(x) == len(y) == 32:\n curve_obj = SECP256R1()\n else:\n- raise InvalidKeyError(\"Coords should be 32 bytes for curve P-256\")\n+ raise InvalidKeyError(\n+ \"Coords should be 32 bytes for curve P-256\"\n+ ) from None\n elif curve == \"P-384\":\n if len(x) == len(y) == 48:\n curve_obj = SECP384R1()\n else:\n- raise InvalidKeyError(\"Coords should be 48 bytes for curve P-384\")\n+ raise InvalidKeyError(\n+ \"Coords should be 48 bytes for curve P-384\"\n+ ) from None\n elif curve == \"P-521\":\n if len(x) == len(y) == 66:\n curve_obj = SECP521R1()\n else:\n- raise InvalidKeyError(\"Coords should be 66 bytes for curve P-521\")\n+ raise InvalidKeyError(\n+ \"Coords should be 66 bytes for curve P-521\"\n+ ) from None\n elif curve == \"secp256k1\":\n if len(x) == len(y) == 32:\n curve_obj = SECP256K1()\n@@ -834,7 +849,7 @@ def from_jwk(jwk: str | JWKDict) -> AllowedOKPKeys:\n else:\n raise ValueError\n except ValueError:\n- raise InvalidKeyError(\"Key is not valid JSON\")\n+ raise InvalidKeyError(\"Key is not valid JSON\") from None\n \n if obj.get(\"kty\") != \"OKP\":\n raise InvalidKeyError(\"Not an Octet Key Pair\")"}, {"sha": "654ee0b744b35a81bb01fe9c4f1bccd041e2cab8", "filename": "lib/jwt/api_jws.py", "status": "modified", "additions": 19, "deletions": 8, "changes": 27, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fjwt%2Fapi_jws.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fjwt%2Fapi_jws.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fjwt%2Fapi_jws.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -3,6 +3,7 @@\n import binascii\n import json\n import warnings\n+from collections.abc import Sequence\n from typing import TYPE_CHECKING, Any\n \n from .algorithms import (\n@@ -30,7 +31,7 @@ class PyJWS:\n \n def __init__(\n self,\n- algorithms: list[str] | None = None,\n+ algorithms: Sequence[str] | None = None,\n options: dict[str, Any] | None = None,\n ) -> None:\n self._algorithms = get_default_algorithms()\n@@ -104,8 +105,8 @@ def get_algorithm_by_name(self, alg_name: str) -> Algorithm:\n def encode(\n self,\n payload: bytes,\n- key: AllowedPrivateKeys | str | bytes,\n- algorithm: str | None = \"HS256\",\n+ key: AllowedPrivateKeys | PyJWK | str | bytes,\n+ algorithm: str | None = None,\n headers: dict[str, Any] | None = None,\n json_encoder: type[json.JSONEncoder] | None = None,\n is_payload_detached: bool = False,\n@@ -114,7 +115,13 @@ def encode(\n segments = []\n \n # declare a new var to narrow the type for type checkers\n- algorithm_: str = algorithm if algorithm is not None else \"none\"\n+ if algorithm is None:\n+ if isinstance(key, PyJWK):\n+ algorithm_ = key.algorithm_name\n+ else:\n+ algorithm_ = \"HS256\"\n+ else:\n+ algorithm_ = algorithm\n \n # Prefer headers values if present to function parameters.\n if headers:\n@@ -158,6 +165,8 @@ def encode(\n signing_input = b\".\".join(segments)\n \n alg_obj = self.get_algorithm_by_name(algorithm_)\n+ if isinstance(key, PyJWK):\n+ key = key.key\n key = alg_obj.prepare_key(key)\n signature = alg_obj.sign(signing_input, key)\n \n@@ -174,7 +183,7 @@ def decode_complete(\n self,\n jwt: str | bytes,\n key: AllowedPublicKeys | PyJWK | str | bytes = \"\",\n- algorithms: list[str] | None = None,\n+ algorithms: Sequence[str] | None = None,\n options: dict[str, Any] | None = None,\n detached_payload: bytes | None = None,\n **kwargs,\n@@ -185,6 +194,7 @@ def decode_complete(\n \"and will be removed in pyjwt version 3. \"\n f\"Unsupported kwargs: {tuple(kwargs.keys())}\",\n RemovedInPyjwt3Warning,\n+ stacklevel=2,\n )\n if options is None:\n options = {}\n@@ -219,7 +229,7 @@ def decode(\n self,\n jwt: str | bytes,\n key: AllowedPublicKeys | PyJWK | str | bytes = \"\",\n- algorithms: list[str] | None = None,\n+ algorithms: Sequence[str] | None = None,\n options: dict[str, Any] | None = None,\n detached_payload: bytes | None = None,\n **kwargs,\n@@ -230,6 +240,7 @@ def decode(\n \"and will be removed in pyjwt version 3. \"\n f\"Unsupported kwargs: {tuple(kwargs.keys())}\",\n RemovedInPyjwt3Warning,\n+ stacklevel=2,\n )\n decoded = self.decode_complete(\n jwt, key, algorithms, options, detached_payload=detached_payload\n@@ -291,14 +302,14 @@ def _verify_signature(\n header: dict[str, Any],\n signature: bytes,\n key: AllowedPublicKeys | PyJWK | str | bytes = \"\",\n- algorithms: list[str] | None = None,\n+ algorithms: Sequence[str] | None = None,\n ) -> None:\n if algorithms is None and isinstance(key, PyJWK):\n algorithms = [key.algorithm_name]\n try:\n alg = header[\"alg\"]\n except KeyError:\n- raise InvalidAlgorithmError(\"Algorithm not specified\")\n+ raise InvalidAlgorithmError(\"Algorithm not specified\") from None\n \n if not alg or (algorithms is not None and alg not in algorithms):\n raise InvalidAlgorithmError(\"The specified alg value is not allowed\")"}, {"sha": "fa4d5e6f0d3d829b052e31c033583775d2e33735", "filename": "lib/jwt/api_jwt.py", "status": "modified", "additions": 75, "deletions": 19, "changes": 94, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fjwt%2Fapi_jwt.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fjwt%2Fapi_jwt.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fjwt%2Fapi_jwt.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -3,9 +3,9 @@\n import json\n import warnings\n from calendar import timegm\n-from collections.abc import Iterable\n+from collections.abc import Iterable, Sequence\n from datetime import datetime, timedelta, timezone\n-from typing import TYPE_CHECKING, Any, List\n+from typing import TYPE_CHECKING, Any\n \n from . import api_jws\n from .exceptions import (\n@@ -15,6 +15,8 @@\n InvalidAudienceError,\n InvalidIssuedAtError,\n InvalidIssuerError,\n+ InvalidJTIError,\n+ InvalidSubjectError,\n MissingRequiredClaimError,\n )\n from .warnings import RemovedInPyjwt3Warning\n@@ -39,14 +41,16 @@ def _get_default_options() -> dict[str, bool | list[str]]:\n \"verify_iat\": True,\n \"verify_aud\": True,\n \"verify_iss\": True,\n+ \"verify_sub\": True,\n+ \"verify_jti\": True,\n \"require\": [],\n }\n \n def encode(\n self,\n payload: dict[str, Any],\n- key: AllowedPrivateKeys | str | bytes,\n- algorithm: str | None = \"HS256\",\n+ key: AllowedPrivateKeys | PyJWK | str | bytes,\n+ algorithm: str | None = None,\n headers: dict[str, Any] | None = None,\n json_encoder: type[json.JSONEncoder] | None = None,\n sort_headers: bool = True,\n@@ -102,7 +106,7 @@ def decode_complete(\n self,\n jwt: str | bytes,\n key: AllowedPublicKeys | PyJWK | str | bytes = \"\",\n- algorithms: list[str] | None = None,\n+ algorithms: Sequence[str] | None = None,\n options: dict[str, Any] | None = None,\n # deprecated arg, remove in pyjwt3\n verify: bool | None = None,\n@@ -111,7 +115,8 @@ def decode_complete(\n # passthrough arguments to _validate_claims\n # consider putting in options\n audience: str | Iterable[str] | None = None,\n- issuer: str | List[str] | None = None,\n+ issuer: str | Sequence[str] | None = None,\n+ subject: str | None = None,\n leeway: float | timedelta = 0,\n # kwargs\n **kwargs: Any,\n@@ -122,6 +127,7 @@ def decode_complete(\n \"and will be removed in pyjwt version 3. \"\n f\"Unsupported kwargs: {tuple(kwargs.keys())}\",\n RemovedInPyjwt3Warning,\n+ stacklevel=2,\n )\n options = dict(options or {}) # shallow-copy or initialize an empty dict\n options.setdefault(\"verify_signature\", True)\n@@ -135,6 +141,7 @@ def decode_complete(\n \"The equivalent is setting `verify_signature` to False in the `options` dictionary. \"\n \"This invocation has a mismatch between the kwarg and the option entry.\",\n category=DeprecationWarning,\n+ stacklevel=2,\n )\n \n if not options[\"verify_signature\"]:\n@@ -143,11 +150,8 @@ def decode_complete(\n options.setdefault(\"verify_iat\", False)\n options.setdefault(\"verify_aud\", False)\n options.setdefault(\"verify_iss\", False)\n-\n- if options[\"verify_signature\"] and not algorithms:\n- raise DecodeError(\n- 'It is required that you pass in a value for the \"algorithms\" argument when calling decode().'\n- )\n+ options.setdefault(\"verify_sub\", False)\n+ options.setdefault(\"verify_jti\", False)\n \n decoded = api_jws.decode_complete(\n jwt,\n@@ -161,7 +165,12 @@ def decode_complete(\n \n merged_options = {**self.options, **options}\n self._validate_claims(\n- payload, merged_options, audience=audience, issuer=issuer, leeway=leeway\n+ payload,\n+ merged_options,\n+ audience=audience,\n+ issuer=issuer,\n+ leeway=leeway,\n+ subject=subject,\n )\n \n decoded[\"payload\"] = payload\n@@ -178,7 +187,7 @@ def _decode_payload(self, decoded: dict[str, Any]) -> Any:\n try:\n payload = json.loads(decoded[\"payload\"])\n except ValueError as e:\n- raise DecodeError(f\"Invalid payload string: {e}\")\n+ raise DecodeError(f\"Invalid payload string: {e}\") from e\n if not isinstance(payload, dict):\n raise DecodeError(\"Invalid payload string: must be a json object\")\n return payload\n@@ -187,7 +196,7 @@ def decode(\n self,\n jwt: str | bytes,\n key: AllowedPublicKeys | PyJWK | str | bytes = \"\",\n- algorithms: list[str] | None = None,\n+ algorithms: Sequence[str] | None = None,\n options: dict[str, Any] | None = None,\n # deprecated arg, remove in pyjwt3\n verify: bool | None = None,\n@@ -196,7 +205,8 @@ def decode(\n # passthrough arguments to _validate_claims\n # consider putting in options\n audience: str | Iterable[str] | None = None,\n- issuer: str | List[str] | None = None,\n+ subject: str | None = None,\n+ issuer: str | Sequence[str] | None = None,\n leeway: float | timedelta = 0,\n # kwargs\n **kwargs: Any,\n@@ -207,6 +217,7 @@ def decode(\n \"and will be removed in pyjwt version 3. \"\n f\"Unsupported kwargs: {tuple(kwargs.keys())}\",\n RemovedInPyjwt3Warning,\n+ stacklevel=2,\n )\n decoded = self.decode_complete(\n jwt,\n@@ -216,6 +227,7 @@ def decode(\n verify=verify,\n detached_payload=detached_payload,\n audience=audience,\n+ subject=subject,\n issuer=issuer,\n leeway=leeway,\n )\n@@ -227,6 +239,7 @@ def _validate_claims(\n options: dict[str, Any],\n audience=None,\n issuer=None,\n+ subject: str | None = None,\n leeway: float | timedelta = 0,\n ) -> None:\n if isinstance(leeway, timedelta):\n@@ -256,6 +269,12 @@ def _validate_claims(\n payload, audience, strict=options.get(\"strict_aud\", False)\n )\n \n+ if options[\"verify_sub\"]:\n+ self._validate_sub(payload, subject)\n+\n+ if options[\"verify_jti\"]:\n+ self._validate_jti(payload)\n+\n def _validate_required_claims(\n self,\n payload: dict[str, Any],\n@@ -265,6 +284,39 @@ def _validate_required_claims(\n if payload.get(claim) is None:\n raise MissingRequiredClaimError(claim)\n \n+ def _validate_sub(self, payload: dict[str, Any], subject=None) -> None:\n+ \"\"\"\n+ Checks whether \"sub\" if in the payload is valid ot not.\n+ This is an Optional claim\n+\n+ :param payload(dict): The payload which needs to be validated\n+ :param subject(str): The subject of the token\n+ \"\"\"\n+\n+ if \"sub\" not in payload:\n+ return\n+\n+ if not isinstance(payload[\"sub\"], str):\n+ raise InvalidSubjectError(\"Subject must be a string\")\n+\n+ if subject is not None:\n+ if payload.get(\"sub\") != subject:\n+ raise InvalidSubjectError(\"Invalid subject\")\n+\n+ def _validate_jti(self, payload: dict[str, Any]) -> None:\n+ \"\"\"\n+ Checks whether \"jti\" if in the payload is valid ot not\n+ This is an Optional claim\n+\n+ :param payload(dict): The payload which needs to be validated\n+ \"\"\"\n+\n+ if \"jti\" not in payload:\n+ return\n+\n+ if not isinstance(payload.get(\"jti\"), str):\n+ raise InvalidJTIError(\"JWT ID must be a string\")\n+\n def _validate_iat(\n self,\n payload: dict[str, Any],\n@@ -274,7 +326,9 @@ def _validate_iat(\n try:\n iat = int(payload[\"iat\"])\n except ValueError:\n- raise InvalidIssuedAtError(\"Issued At claim (iat) must be an integer.\")\n+ raise InvalidIssuedAtError(\n+ \"Issued At claim (iat) must be an integer.\"\n+ ) from None\n if iat > (now + leeway):\n raise ImmatureSignatureError(\"The token is not yet valid (iat)\")\n \n@@ -287,7 +341,7 @@ def _validate_nbf(\n try:\n nbf = int(payload[\"nbf\"])\n except ValueError:\n- raise DecodeError(\"Not Before claim (nbf) must be an integer.\")\n+ raise DecodeError(\"Not Before claim (nbf) must be an integer.\") from None\n \n if nbf > (now + leeway):\n raise ImmatureSignatureError(\"The token is not yet valid (nbf)\")\n@@ -301,7 +355,9 @@ def _validate_exp(\n try:\n exp = int(payload[\"exp\"])\n except ValueError:\n- raise DecodeError(\"Expiration Time claim (exp) must be an integer.\")\n+ raise DecodeError(\n+ \"Expiration Time claim (exp) must be an integer.\"\n+ ) from None\n \n if exp <= (now - leeway):\n raise ExpiredSignatureError(\"Signature has expired\")\n@@ -363,7 +419,7 @@ def _validate_iss(self, payload: dict[str, Any], issuer: Any) -> None:\n if \"iss\" not in payload:\n raise MissingRequiredClaimError(\"iss\")\n \n- if isinstance(issuer, list):\n+ if isinstance(issuer, Sequence):\n if payload[\"iss\"] not in issuer:\n raise InvalidIssuerError(\"Invalid issuer\")\n else:"}, {"sha": "9b45ae48c4e0f2c7627c152bf0059e57f17b73ca", "filename": "lib/jwt/exceptions.py", "status": "modified", "additions": 8, "deletions": 0, "changes": 8, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fjwt%2Fexceptions.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fjwt%2Fexceptions.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fjwt%2Fexceptions.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -72,3 +72,11 @@ class PyJWKClientError(PyJWTError):\n \n class PyJWKClientConnectionError(PyJWKClientError):\n pass\n+\n+\n+class InvalidSubjectError(InvalidTokenError):\n+ pass\n+\n+\n+class InvalidJTIError(InvalidTokenError):\n+ pass"}, {"sha": "8e1c2286e7ea936292f525c44bcc1b0ee30c6560", "filename": "lib/jwt/help.py", "status": "modified", "additions": 4, "deletions": 1, "changes": 5, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fjwt%2Fhelp.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fjwt%2Fhelp.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fjwt%2Fhelp.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -39,7 +39,10 @@ def info() -> Dict[str, Dict[str, str]]:\n )\n if pypy_version_info.releaselevel != \"final\":\n implementation_version = \"\".join(\n- [implementation_version, pypy_version_info.releaselevel]\n+ [\n+ implementation_version,\n+ pypy_version_info.releaselevel,\n+ ]\n )\n else:\n implementation_version = \"Unknown\""}, {"sha": "9a8992ca6b9d2be153e203b11465a8e6e7cdc570", "filename": "lib/jwt/jwks_client.py", "status": "modified", "additions": 4, "deletions": 2, "changes": 6, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fjwt%2Fjwks_client.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fjwt%2Fjwks_client.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fjwt%2Fjwks_client.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -45,7 +45,9 @@ def __init__(\n if cache_keys:\n # Cache signing keys\n # Ignore mypy (https://github.com/python/mypy/issues/2427)\n- self.get_signing_key = lru_cache(maxsize=max_cached_keys)(self.get_signing_key) # type: ignore\n+ self.get_signing_key = lru_cache(maxsize=max_cached_keys)(\n+ self.get_signing_key\n+ ) # type: ignore\n \n def fetch_data(self) -> Any:\n jwk_set: Any = None\n@@ -58,7 +60,7 @@ def fetch_data(self) -> Any:\n except (URLError, TimeoutError) as e:\n raise PyJWKClientConnectionError(\n f'Fail to fetch data from the url, err: \"{e}\"'\n- )\n+ ) from e\n else:\n return jwk_set\n finally:"}, {"sha": "56e89bb7782f4b5f61c9b2197258dcbf45335d33", "filename": "lib/jwt/utils.py", "status": "modified", "additions": 7, "deletions": 10, "changes": 17, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fjwt%2Futils.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fjwt%2Futils.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fjwt%2Futils.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,7 +1,7 @@\n import base64\n import binascii\n import re\n-from typing import Union\n+from typing import Optional, Union\n \n try:\n from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve\n@@ -37,11 +37,11 @@ def base64url_encode(input: bytes) -> bytes:\n return base64.urlsafe_b64encode(input).replace(b\"=\", b\"\")\n \n \n-def to_base64url_uint(val: int) -> bytes:\n+def to_base64url_uint(val: int, *, bit_length: Optional[int] = None) -> bytes:\n if val < 0:\n raise ValueError(\"Must be a positive integer\")\n \n- int_bytes = bytes_from_int(val)\n+ int_bytes = bytes_from_int(val, bit_length=bit_length)\n \n if len(int_bytes) == 0:\n int_bytes = b\"\\x00\"\n@@ -63,13 +63,10 @@ def bytes_to_number(string: bytes) -> int:\n return int(binascii.b2a_hex(string), 16)\n \n \n-def bytes_from_int(val: int) -> bytes:\n- remaining = val\n- byte_length = 0\n-\n- while remaining != 0:\n- remaining >>= 8\n- byte_length += 1\n+def bytes_from_int(val: int, *, bit_length: Optional[int] = None) -> bytes:\n+ if bit_length is None:\n+ bit_length = val.bit_length()\n+ byte_length = (bit_length + 7) // 8\n \n return val.to_bytes(byte_length, \"big\", signed=False)\n "}, {"sha": "d7223f7c39fb9c01c54da6661671e5e299aa60f6", "filename": "lib/mako/__init__.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fmako%2F__init__.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fmako%2F__init__.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fmako%2F__init__.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -5,4 +5,4 @@\n # the MIT License: http://www.opensource.org/licenses/mit-license.php\n \n \n-__version__ = \"1.3.5\"\n+__version__ = \"1.3.6\""}, {"sha": "b4a76a45f385726a4241a3217a03b2bc4c1d76da", "filename": "lib/mako/lexer.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fmako%2Flexer.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fmako%2Flexer.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fmako%2Flexer.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -375,7 +375,7 @@ def match_text(self):\n |\n (?=\\${) # an expression\n |\n- (?=</?[%&]) # a substitution or block or call start or end\n+ (?=</?%) # a substitution or block or call start or end\n # - don't consume\n |\n (\\\\\\r?\\n) # an escaped newline - throw away"}, {"sha": "fee8dc7acca04f4d3e8ff002a4440d24b59dacac", "filename": "lib/markupsafe/__init__.py", "status": "modified", "additions": 211, "deletions": 148, "changes": 359, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fmarkupsafe%2F__init__.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fmarkupsafe%2F__init__.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fmarkupsafe%2F__init__.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,29 +1,84 @@\n-import functools\n+from __future__ import annotations\n+\n+import collections.abc as cabc\n import string\n-import sys\n import typing as t\n \n+try:\n+ from ._speedups import _escape_inner\n+except ImportError:\n+ from ._native import _escape_inner\n+\n if t.TYPE_CHECKING:\n import typing_extensions as te\n \n- class HasHTML(te.Protocol):\n- def __html__(self) -> str:\n- pass\n \n- _P = te.ParamSpec(\"_P\")\n+class _HasHTML(t.Protocol):\n+ def __html__(self, /) -> str: ...\n+\n+\n+class _TPEscape(t.Protocol):\n+ def __call__(self, s: t.Any, /) -> Markup: ...\n+\n+\n+def escape(s: t.Any, /) -> Markup:\n+ \"\"\"Replace the characters ``&``, ``<``, ``>``, ``'``, and ``\"`` in\n+ the string with HTML-safe sequences. Use this if you need to display\n+ text that might contain such characters in HTML.\n+\n+ If the object has an ``__html__`` method, it is called and the\n+ return value is assumed to already be safe for HTML.\n+\n+ :param s: An object to be converted to a string and escaped.\n+ :return: A :class:`Markup` string with the escaped text.\n+ \"\"\"\n+ # If the object is already a plain string, skip __html__ check and string\n+ # conversion. This is the most common use case.\n+ # Use type(s) instead of s.__class__ because a proxy object may be reporting\n+ # the __class__ of the proxied value.\n+ if type(s) is str:\n+ return Markup(_escape_inner(s))\n+\n+ if hasattr(s, \"__html__\"):\n+ return Markup(s.__html__())\n+\n+ return Markup(_escape_inner(str(s)))\n+\n+\n+def escape_silent(s: t.Any | None, /) -> Markup:\n+ \"\"\"Like :func:`escape` but treats ``None`` as the empty string.\n+ Useful with optional values, as otherwise you get the string\n+ ``'None'`` when the value is ``None``.\n+\n+ >>> escape(None)\n+ Markup('None')\n+ >>> escape_silent(None)\n+ Markup('')\n+ \"\"\"\n+ if s is None:\n+ return Markup()\n \n+ return escape(s)\n \n-__version__ = \"2.1.5\"\n \n+def soft_str(s: t.Any, /) -> str:\n+ \"\"\"Convert an object to a string if it isn't already. This preserves\n+ a :class:`Markup` string rather than converting it back to a basic\n+ string, so it will still be marked as safe and won't be escaped\n+ again.\n \n-def _simple_escaping_wrapper(func: \"t.Callable[_P, str]\") -> \"t.Callable[_P, Markup]\":\n- @functools.wraps(func)\n- def wrapped(self: \"Markup\", *args: \"_P.args\", **kwargs: \"_P.kwargs\") -> \"Markup\":\n- arg_list = _escape_argspec(list(args), enumerate(args), self.escape)\n- _escape_argspec(kwargs, kwargs.items(), self.escape)\n- return self.__class__(func(self, *arg_list, **kwargs)) # type: ignore[arg-type]\n+ >>> value = escape(\"<User 1>\")\n+ >>> value\n+ Markup('<User 1>')\n+ >>> escape(str(value))\n+ Markup('&lt;User 1&gt;')\n+ >>> escape(soft_str(value))\n+ Markup('<User 1>')\n+ \"\"\"\n+ if not isinstance(s, str):\n+ return str(s)\n \n- return wrapped # type: ignore[return-value]\n+ return s\n \n \n class Markup(str):\n@@ -65,82 +120,72 @@ class Markup(str):\n __slots__ = ()\n \n def __new__(\n- cls, base: t.Any = \"\", encoding: t.Optional[str] = None, errors: str = \"strict\"\n- ) -> \"te.Self\":\n- if hasattr(base, \"__html__\"):\n- base = base.__html__()\n+ cls, object: t.Any = \"\", encoding: str | None = None, errors: str = \"strict\"\n+ ) -> te.Self:\n+ if hasattr(object, \"__html__\"):\n+ object = object.__html__()\n \n if encoding is None:\n- return super().__new__(cls, base)\n+ return super().__new__(cls, object)\n \n- return super().__new__(cls, base, encoding, errors)\n+ return super().__new__(cls, object, encoding, errors)\n \n- def __html__(self) -> \"te.Self\":\n+ def __html__(self, /) -> te.Self:\n return self\n \n- def __add__(self, other: t.Union[str, \"HasHTML\"]) -> \"te.Self\":\n- if isinstance(other, str) or hasattr(other, \"__html__\"):\n- return self.__class__(super().__add__(self.escape(other)))\n+ def __add__(self, value: str | _HasHTML, /) -> te.Self:\n+ if isinstance(value, str) or hasattr(value, \"__html__\"):\n+ return self.__class__(super().__add__(self.escape(value)))\n \n return NotImplemented\n \n- def __radd__(self, other: t.Union[str, \"HasHTML\"]) -> \"te.Self\":\n- if isinstance(other, str) or hasattr(other, \"__html__\"):\n- return self.escape(other).__add__(self)\n+ def __radd__(self, value: str | _HasHTML, /) -> te.Self:\n+ if isinstance(value, str) or hasattr(value, \"__html__\"):\n+ return self.escape(value).__add__(self)\n \n return NotImplemented\n \n- def __mul__(self, num: \"te.SupportsIndex\") -> \"te.Self\":\n- if isinstance(num, int):\n- return self.__class__(super().__mul__(num))\n+ def __mul__(self, value: t.SupportsIndex, /) -> te.Self:\n+ return self.__class__(super().__mul__(value))\n \n- return NotImplemented\n-\n- __rmul__ = __mul__\n+ def __rmul__(self, value: t.SupportsIndex, /) -> te.Self:\n+ return self.__class__(super().__mul__(value))\n \n- def __mod__(self, arg: t.Any) -> \"te.Self\":\n- if isinstance(arg, tuple):\n+ def __mod__(self, value: t.Any, /) -> te.Self:\n+ if isinstance(value, tuple):\n # a tuple of arguments, each wrapped\n- arg = tuple(_MarkupEscapeHelper(x, self.escape) for x in arg)\n- elif hasattr(type(arg), \"__getitem__\") and not isinstance(arg, str):\n+ value = tuple(_MarkupEscapeHelper(x, self.escape) for x in value)\n+ elif hasattr(type(value), \"__getitem__\") and not isinstance(value, str):\n # a mapping of arguments, wrapped\n- arg = _MarkupEscapeHelper(arg, self.escape)\n+ value = _MarkupEscapeHelper(value, self.escape)\n else:\n # a single argument, wrapped with the helper and a tuple\n- arg = (_MarkupEscapeHelper(arg, self.escape),)\n+ value = (_MarkupEscapeHelper(value, self.escape),)\n \n- return self.__class__(super().__mod__(arg))\n+ return self.__class__(super().__mod__(value))\n \n- def __repr__(self) -> str:\n+ def __repr__(self, /) -> str:\n return f\"{self.__class__.__name__}({super().__repr__()})\"\n \n- def join(self, seq: t.Iterable[t.Union[str, \"HasHTML\"]]) -> \"te.Self\":\n- return self.__class__(super().join(map(self.escape, seq)))\n-\n- join.__doc__ = str.join.__doc__\n+ def join(self, iterable: cabc.Iterable[str | _HasHTML], /) -> te.Self:\n+ return self.__class__(super().join(map(self.escape, iterable)))\n \n def split( # type: ignore[override]\n- self, sep: t.Optional[str] = None, maxsplit: int = -1\n- ) -> t.List[\"te.Self\"]:\n+ self, /, sep: str | None = None, maxsplit: t.SupportsIndex = -1\n+ ) -> list[te.Self]:\n return [self.__class__(v) for v in super().split(sep, maxsplit)]\n \n- split.__doc__ = str.split.__doc__\n-\n def rsplit( # type: ignore[override]\n- self, sep: t.Optional[str] = None, maxsplit: int = -1\n- ) -> t.List[\"te.Self\"]:\n+ self, /, sep: str | None = None, maxsplit: t.SupportsIndex = -1\n+ ) -> list[te.Self]:\n return [self.__class__(v) for v in super().rsplit(sep, maxsplit)]\n \n- rsplit.__doc__ = str.rsplit.__doc__\n-\n def splitlines( # type: ignore[override]\n- self, keepends: bool = False\n- ) -> t.List[\"te.Self\"]:\n+ self, /, keepends: bool = False\n+ ) -> list[te.Self]:\n return [self.__class__(v) for v in super().splitlines(keepends)]\n \n- splitlines.__doc__ = str.splitlines.__doc__\n-\n- def unescape(self) -> str:\n+ def unescape(self, /) -> str:\n \"\"\"Convert escaped markup back into a text string. This replaces\n HTML entities with the characters they represent.\n \n@@ -151,7 +196,7 @@ def unescape(self) -> str:\n \n return unescape(str(self))\n \n- def striptags(self) -> str:\n+ def striptags(self, /) -> str:\n \"\"\":meth:`unescape` the markup, remove tags, and normalize\n whitespace to single spaces.\n \n@@ -163,31 +208,17 @@ def striptags(self) -> str:\n # Look for comments then tags separately. Otherwise, a comment that\n # contains a tag would end early, leaving some of the comment behind.\n \n- while True:\n- # keep finding comment start marks\n- start = value.find(\"<!--\")\n-\n- if start == -1:\n- break\n-\n+ # keep finding comment start marks\n+ while (start := value.find(\"<!--\")) != -1:\n # find a comment end mark beyond the start, otherwise stop\n- end = value.find(\"-->\", start)\n-\n- if end == -1:\n+ if (end := value.find(\"-->\", start)) == -1:\n break\n \n value = f\"{value[:start]}{value[end + 3:]}\"\n \n # remove tags using the same method\n- while True:\n- start = value.find(\"<\")\n-\n- if start == -1:\n- break\n-\n- end = value.find(\">\", start)\n-\n- if end == -1:\n+ while (start := value.find(\"<\")) != -1:\n+ if (end := value.find(\">\", start)) == -1:\n break\n \n value = f\"{value[:start]}{value[end + 1:]}\"\n@@ -197,7 +228,7 @@ def striptags(self) -> str:\n return self.__class__(value).unescape()\n \n @classmethod\n- def escape(cls, s: t.Any) -> \"te.Self\":\n+ def escape(cls, s: t.Any, /) -> te.Self:\n \"\"\"Escape a string. Calls :func:`escape` and ensures that for\n subclasses the correct type is returned.\n \"\"\"\n@@ -208,49 +239,90 @@ def escape(cls, s: t.Any) -> \"te.Self\":\n \n return rv # type: ignore[return-value]\n \n- __getitem__ = _simple_escaping_wrapper(str.__getitem__)\n- capitalize = _simple_escaping_wrapper(str.capitalize)\n- title = _simple_escaping_wrapper(str.title)\n- lower = _simple_escaping_wrapper(str.lower)\n- upper = _simple_escaping_wrapper(str.upper)\n- replace = _simple_escaping_wrapper(str.replace)\n- ljust = _simple_escaping_wrapper(str.ljust)\n- rjust = _simple_escaping_wrapper(str.rjust)\n- lstrip = _simple_escaping_wrapper(str.lstrip)\n- rstrip = _simple_escaping_wrapper(str.rstrip)\n- center = _simple_escaping_wrapper(str.center)\n- strip = _simple_escaping_wrapper(str.strip)\n- translate = _simple_escaping_wrapper(str.translate)\n- expandtabs = _simple_escaping_wrapper(str.expandtabs)\n- swapcase = _simple_escaping_wrapper(str.swapcase)\n- zfill = _simple_escaping_wrapper(str.zfill)\n- casefold = _simple_escaping_wrapper(str.casefold)\n-\n- if sys.version_info >= (3, 9):\n- removeprefix = _simple_escaping_wrapper(str.removeprefix)\n- removesuffix = _simple_escaping_wrapper(str.removesuffix)\n-\n- def partition(self, sep: str) -> t.Tuple[\"te.Self\", \"te.Self\", \"te.Self\"]:\n- l, s, r = super().partition(self.escape(sep))\n+ def __getitem__(self, key: t.SupportsIndex | slice, /) -> te.Self:\n+ return self.__class__(super().__getitem__(key))\n+\n+ def capitalize(self, /) -> te.Self:\n+ return self.__class__(super().capitalize())\n+\n+ def title(self, /) -> te.Self:\n+ return self.__class__(super().title())\n+\n+ def lower(self, /) -> te.Self:\n+ return self.__class__(super().lower())\n+\n+ def upper(self, /) -> te.Self:\n+ return self.__class__(super().upper())\n+\n+ def replace(self, old: str, new: str, count: t.SupportsIndex = -1, /) -> te.Self:\n+ return self.__class__(super().replace(old, self.escape(new), count))\n+\n+ def ljust(self, width: t.SupportsIndex, fillchar: str = \" \", /) -> te.Self:\n+ return self.__class__(super().ljust(width, self.escape(fillchar)))\n+\n+ def rjust(self, width: t.SupportsIndex, fillchar: str = \" \", /) -> te.Self:\n+ return self.__class__(super().rjust(width, self.escape(fillchar)))\n+\n+ def lstrip(self, chars: str | None = None, /) -> te.Self:\n+ return self.__class__(super().lstrip(chars))\n+\n+ def rstrip(self, chars: str | None = None, /) -> te.Self:\n+ return self.__class__(super().rstrip(chars))\n+\n+ def center(self, width: t.SupportsIndex, fillchar: str = \" \", /) -> te.Self:\n+ return self.__class__(super().center(width, self.escape(fillchar)))\n+\n+ def strip(self, chars: str | None = None, /) -> te.Self:\n+ return self.__class__(super().strip(chars))\n+\n+ def translate(\n+ self,\n+ table: cabc.Mapping[int, str | int | None], # type: ignore[override]\n+ /,\n+ ) -> str:\n+ return self.__class__(super().translate(table))\n+\n+ def expandtabs(self, /, tabsize: t.SupportsIndex = 8) -> te.Self:\n+ return self.__class__(super().expandtabs(tabsize))\n+\n+ def swapcase(self, /) -> te.Self:\n+ return self.__class__(super().swapcase())\n+\n+ def zfill(self, width: t.SupportsIndex, /) -> te.Self:\n+ return self.__class__(super().zfill(width))\n+\n+ def casefold(self, /) -> te.Self:\n+ return self.__class__(super().casefold())\n+\n+ def removeprefix(self, prefix: str, /) -> te.Self:\n+ return self.__class__(super().removeprefix(prefix))\n+\n+ def removesuffix(self, suffix: str) -> te.Self:\n+ return self.__class__(super().removesuffix(suffix))\n+\n+ def partition(self, sep: str, /) -> tuple[te.Self, te.Self, te.Self]:\n+ left, sep, right = super().partition(sep)\n cls = self.__class__\n- return cls(l), cls(s), cls(r)\n+ return cls(left), cls(sep), cls(right)\n \n- def rpartition(self, sep: str) -> t.Tuple[\"te.Self\", \"te.Self\", \"te.Self\"]:\n- l, s, r = super().rpartition(self.escape(sep))\n+ def rpartition(self, sep: str, /) -> tuple[te.Self, te.Self, te.Self]:\n+ left, sep, right = super().rpartition(sep)\n cls = self.__class__\n- return cls(l), cls(s), cls(r)\n+ return cls(left), cls(sep), cls(right)\n \n- def format(self, *args: t.Any, **kwargs: t.Any) -> \"te.Self\":\n+ def format(self, *args: t.Any, **kwargs: t.Any) -> te.Self:\n formatter = EscapeFormatter(self.escape)\n return self.__class__(formatter.vformat(self, args, kwargs))\n \n- def format_map( # type: ignore[override]\n- self, map: t.Mapping[str, t.Any]\n- ) -> \"te.Self\":\n+ def format_map(\n+ self,\n+ mapping: cabc.Mapping[str, t.Any], # type: ignore[override]\n+ /,\n+ ) -> te.Self:\n formatter = EscapeFormatter(self.escape)\n- return self.__class__(formatter.vformat(self, (), map))\n+ return self.__class__(formatter.vformat(self, (), mapping))\n \n- def __html_format__(self, format_spec: str) -> \"te.Self\":\n+ def __html_format__(self, format_spec: str, /) -> te.Self:\n if format_spec:\n raise ValueError(\"Unsupported format specification for Markup.\")\n \n@@ -260,8 +332,8 @@ def __html_format__(self, format_spec: str) -> \"te.Self\":\n class EscapeFormatter(string.Formatter):\n __slots__ = (\"escape\",)\n \n- def __init__(self, escape: t.Callable[[t.Any], Markup]) -> None:\n- self.escape = escape\n+ def __init__(self, escape: _TPEscape) -> None:\n+ self.escape: _TPEscape = escape\n super().__init__()\n \n def format_field(self, value: t.Any, format_spec: str) -> str:\n@@ -278,55 +350,46 @@ def format_field(self, value: t.Any, format_spec: str) -> str:\n else:\n # We need to make sure the format spec is str here as\n # otherwise the wrong callback methods are invoked.\n- rv = string.Formatter.format_field(self, value, str(format_spec))\n+ rv = super().format_field(value, str(format_spec))\n return str(self.escape(rv))\n \n \n-_ListOrDict = t.TypeVar(\"_ListOrDict\", list, dict)\n-\n-\n-def _escape_argspec(\n- obj: _ListOrDict, iterable: t.Iterable[t.Any], escape: t.Callable[[t.Any], Markup]\n-) -> _ListOrDict:\n- \"\"\"Helper for various string-wrapped functions.\"\"\"\n- for key, value in iterable:\n- if isinstance(value, str) or hasattr(value, \"__html__\"):\n- obj[key] = escape(value)\n-\n- return obj\n-\n-\n class _MarkupEscapeHelper:\n \"\"\"Helper for :meth:`Markup.__mod__`.\"\"\"\n \n __slots__ = (\"obj\", \"escape\")\n \n- def __init__(self, obj: t.Any, escape: t.Callable[[t.Any], Markup]) -> None:\n- self.obj = obj\n- self.escape = escape\n+ def __init__(self, obj: t.Any, escape: _TPEscape) -> None:\n+ self.obj: t.Any = obj\n+ self.escape: _TPEscape = escape\n \n- def __getitem__(self, item: t.Any) -> \"te.Self\":\n- return self.__class__(self.obj[item], self.escape)\n+ def __getitem__(self, key: t.Any, /) -> te.Self:\n+ return self.__class__(self.obj[key], self.escape)\n \n- def __str__(self) -> str:\n+ def __str__(self, /) -> str:\n return str(self.escape(self.obj))\n \n- def __repr__(self) -> str:\n+ def __repr__(self, /) -> str:\n return str(self.escape(repr(self.obj)))\n \n- def __int__(self) -> int:\n+ def __int__(self, /) -> int:\n return int(self.obj)\n \n- def __float__(self) -> float:\n+ def __float__(self, /) -> float:\n return float(self.obj)\n \n \n-# circular import\n-try:\n- from ._speedups import escape as escape\n- from ._speedups import escape_silent as escape_silent\n- from ._speedups import soft_str as soft_str\n-except ImportError:\n- from ._native import escape as escape\n- from ._native import escape_silent as escape_silent # noqa: F401\n- from ._native import soft_str as soft_str # noqa: F401\n+def __getattr__(name: str) -> t.Any:\n+ if name == \"__version__\":\n+ import importlib.metadata\n+ import warnings\n+\n+ warnings.warn(\n+ \"The '__version__' attribute is deprecated and will be removed in\"\n+ \" MarkupSafe 3.1. Use feature detection, or\"\n+ ' `importlib.metadata.version(\"markupsafe\")`, instead.',\n+ stacklevel=2,\n+ )\n+ return importlib.metadata.version(\"markupsafe\")\n+\n+ raise AttributeError(name)"}, {"sha": "088b3bca9839ee489eefa546a0773a465b8cd0ca", "filename": "lib/markupsafe/_native.py", "status": "modified", "additions": 3, "deletions": 58, "changes": 61, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fmarkupsafe%2F_native.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fmarkupsafe%2F_native.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fmarkupsafe%2F_native.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,63 +1,8 @@\n-import typing as t\n-\n-from . import Markup\n-\n-\n-def escape(s: t.Any) -> Markup:\n- \"\"\"Replace the characters ``&``, ``<``, ``>``, ``'``, and ``\"`` in\n- the string with HTML-safe sequences. Use this if you need to display\n- text that might contain such characters in HTML.\n-\n- If the object has an ``__html__`` method, it is called and the\n- return value is assumed to already be safe for HTML.\n-\n- :param s: An object to be converted to a string and escaped.\n- :return: A :class:`Markup` string with the escaped text.\n- \"\"\"\n- if hasattr(s, \"__html__\"):\n- return Markup(s.__html__())\n-\n- return Markup(\n- str(s)\n- .replace(\"&\", \"&\")\n+def _escape_inner(s: str, /) -> str:\n+ return (\n+ s.replace(\"&\", \"&\")\n .replace(\">\", \">\")\n .replace(\"<\", \"<\")\n .replace(\"'\", \"'\")\n .replace('\"', \""\")\n )\n-\n-\n-def escape_silent(s: t.Optional[t.Any]) -> Markup:\n- \"\"\"Like :func:`escape` but treats ``None`` as the empty string.\n- Useful with optional values, as otherwise you get the string\n- ``'None'`` when the value is ``None``.\n-\n- >>> escape(None)\n- Markup('None')\n- >>> escape_silent(None)\n- Markup('')\n- \"\"\"\n- if s is None:\n- return Markup()\n-\n- return escape(s)\n-\n-\n-def soft_str(s: t.Any) -> str:\n- \"\"\"Convert an object to a string if it isn't already. This preserves\n- a :class:`Markup` string rather than converting it back to a basic\n- string, so it will still be marked as safe and won't be escaped\n- again.\n-\n- >>> value = escape(\"<User 1>\")\n- >>> value\n- Markup('<User 1>')\n- >>> escape(str(value))\n- Markup('&lt;User 1&gt;')\n- >>> escape(soft_str(value))\n- Markup('<User 1>')\n- \"\"\"\n- if not isinstance(s, str):\n- return str(s)\n-\n- return s"}, {"sha": "09dd57caa8c364f431b4fe6cbf37d4cc3172687e", "filename": "lib/markupsafe/_speedups.c", "status": "modified", "additions": 20, "deletions": 136, "changes": 156, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fmarkupsafe%2F_speedups.c", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fmarkupsafe%2F_speedups.c", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fmarkupsafe%2F_speedups.c?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,22 +1,5 @@\n #include <Python.h>\n \n-static PyObject* markup;\n-\n-static int\n-init_constants(void)\n-{\n-\tPyObject *module;\n-\n-\t/* import markup type so that we can mark the return value */\n-\tmodule = PyImport_ImportModule(\"markupsafe\");\n-\tif (!module)\n-\t\treturn 0;\n-\tmarkup = PyObject_GetAttrString(module, \"Markup\");\n-\tPy_DECREF(module);\n-\n-\treturn 1;\n-}\n-\n #define GET_DELTA(inp, inp_end, delta) \\\n \twhile (inp < inp_end) { \\\n \t\tswitch (*inp++) { \\\n@@ -166,135 +149,29 @@ escape_unicode_kind4(PyUnicodeObject *in)\n }\n \n static PyObject*\n-escape_unicode(PyUnicodeObject *in)\n+escape_unicode(PyObject *self, PyObject *s)\n {\n-\tif (PyUnicode_READY(in))\n+\tif (!PyUnicode_Check(s))\n+\t\treturn NULL;\n+\n+ // This check is no longer needed in Python 3.12.\n+\tif (PyUnicode_READY(s))\n \t\treturn NULL;\n \n-\tswitch (PyUnicode_KIND(in)) {\n+\tswitch (PyUnicode_KIND(s)) {\n \tcase PyUnicode_1BYTE_KIND:\n-\t\treturn escape_unicode_kind1(in);\n+\t\treturn escape_unicode_kind1((PyUnicodeObject*) s);\n \tcase PyUnicode_2BYTE_KIND:\n-\t\treturn escape_unicode_kind2(in);\n+\t\treturn escape_unicode_kind2((PyUnicodeObject*) s);\n \tcase PyUnicode_4BYTE_KIND:\n-\t\treturn escape_unicode_kind4(in);\n+\t\treturn escape_unicode_kind4((PyUnicodeObject*) s);\n \t}\n \tassert(0); /* shouldn't happen */\n \treturn NULL;\n }\n \n-static PyObject*\n-escape(PyObject *self, PyObject *text)\n-{\n-\tstatic PyObject *id_html;\n-\tPyObject *s = NULL, *rv = NULL, *html;\n-\n-\tif (id_html == NULL) {\n-\t\tid_html = PyUnicode_InternFromString(\"__html__\");\n-\t\tif (id_html == NULL) {\n-\t\t\treturn NULL;\n-\t\t}\n-\t}\n-\n-\t/* we don't have to escape integers, bools or floats */\n-\tif (PyLong_CheckExact(text) ||\n-\t\tPyFloat_CheckExact(text) || PyBool_Check(text) ||\n-\t\ttext == Py_None)\n-\t\treturn PyObject_CallFunctionObjArgs(markup, text, NULL);\n-\n-\t/* if the object has an __html__ method that performs the escaping */\n-\thtml = PyObject_GetAttr(text ,id_html);\n-\tif (html) {\n-\t\ts = PyObject_CallObject(html, NULL);\n-\t\tPy_DECREF(html);\n-\t\tif (s == NULL) {\n-\t\t\treturn NULL;\n-\t\t}\n-\t\t/* Convert to Markup object */\n-\t\trv = PyObject_CallFunctionObjArgs(markup, (PyObject*)s, NULL);\n-\t\tPy_DECREF(s);\n-\t\treturn rv;\n-\t}\n-\n-\t/* otherwise make the object unicode if it isn't, then escape */\n-\tPyErr_Clear();\n-\tif (!PyUnicode_Check(text)) {\n-\t\tPyObject *unicode = PyObject_Str(text);\n-\t\tif (!unicode)\n-\t\t\treturn NULL;\n-\t\ts = escape_unicode((PyUnicodeObject*)unicode);\n-\t\tPy_DECREF(unicode);\n-\t}\n-\telse\n-\t\ts = escape_unicode((PyUnicodeObject*)text);\n-\n-\t/* convert the unicode string into a markup object. */\n-\trv = PyObject_CallFunctionObjArgs(markup, (PyObject*)s, NULL);\n-\tPy_DECREF(s);\n-\treturn rv;\n-}\n-\n-\n-static PyObject*\n-escape_silent(PyObject *self, PyObject *text)\n-{\n-\tif (text != Py_None)\n-\t\treturn escape(self, text);\n-\treturn PyObject_CallFunctionObjArgs(markup, NULL);\n-}\n-\n-\n-static PyObject*\n-soft_str(PyObject *self, PyObject *s)\n-{\n-\tif (!PyUnicode_Check(s))\n-\t\treturn PyObject_Str(s);\n-\tPy_INCREF(s);\n-\treturn s;\n-}\n-\n-\n static PyMethodDef module_methods[] = {\n-\t{\n-\t\t\"escape\",\n-\t\t(PyCFunction)escape,\n-\t\tMETH_O,\n-\t\t\"Replace the characters ``&``, ``<``, ``>``, ``'``, and ``\\\"`` in\"\n-\t\t\" the string with HTML-safe sequences. Use this if you need to display\"\n-\t\t\" text that might contain such characters in HTML.\\n\\n\"\n-\t\t\"If the object has an ``__html__`` method, it is called and the\"\n-\t\t\" return value is assumed to already be safe for HTML.\\n\\n\"\n-\t\t\":param s: An object to be converted to a string and escaped.\\n\"\n-\t\t\":return: A :class:`Markup` string with the escaped text.\\n\"\n-\t},\n-\t{\n-\t\t\"escape_silent\",\n-\t\t(PyCFunction)escape_silent,\n-\t\tMETH_O,\n-\t\t\"Like :func:`escape` but treats ``None`` as the empty string.\"\n-\t\t\" Useful with optional values, as otherwise you get the string\"\n-\t\t\" ``'None'`` when the value is ``None``.\\n\\n\"\n-\t\t\">>> escape(None)\\n\"\n-\t\t\"Markup('None')\\n\"\n-\t\t\">>> escape_silent(None)\\n\"\n-\t\t\"Markup('')\\n\"\n-\t},\n-\t{\n-\t\t\"soft_str\",\n-\t\t(PyCFunction)soft_str,\n-\t\tMETH_O,\n-\t\t\"Convert an object to a string if it isn't already. This preserves\"\n-\t\t\" a :class:`Markup` string rather than converting it back to a basic\"\n-\t\t\" string, so it will still be marked as safe and won't be escaped\"\n-\t\t\" again.\\n\\n\"\n-\t\t\">>> value = escape(\\\"<User 1>\\\")\\n\"\n-\t\t\">>> value\\n\"\n-\t\t\"Markup('<User 1>')\\n\"\n-\t\t\">>> escape(str(value))\\n\"\n-\t\t\"Markup('&lt;User 1&gt;')\\n\"\n-\t\t\">>> escape(soft_str(value))\\n\"\n-\t\t\"Markup('<User 1>')\\n\"\n-\t},\n+\t{\"_escape_inner\", (PyCFunction)escape_unicode, METH_O, NULL},\n \t{NULL, NULL, 0, NULL} /* Sentinel */\n };\n \n@@ -313,8 +190,15 @@ static struct PyModuleDef module_definition = {\n PyMODINIT_FUNC\n PyInit__speedups(void)\n {\n-\tif (!init_constants())\n+\tPyObject *m = PyModule_Create(&module_definition);\n+\n+\tif (m == NULL) {\n \t\treturn NULL;\n+\t}\n+\n+\t#ifdef Py_GIL_DISABLED\n+\tPyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);\n+\t#endif\n \n-\treturn PyModule_Create(&module_definition);\n+\treturn m;\n }"}, {"sha": "8c8885852a26eba90d3ca1783beca535d4d43bb0", "filename": "lib/markupsafe/_speedups.pyi", "status": "modified", "additions": 1, "deletions": 9, "changes": 10, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fmarkupsafe%2F_speedups.pyi", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fmarkupsafe%2F_speedups.pyi", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fmarkupsafe%2F_speedups.pyi?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,9 +1 @@\n-from typing import Any\n-from typing import Optional\n-\n-from . import Markup\n-\n-def escape(s: Any) -> Markup: ...\n-def escape_silent(s: Optional[Any]) -> Markup: ...\n-def soft_str(s: Any) -> str: ...\n-def soft_unicode(s: Any) -> str: ...\n+def _escape_inner(s: str, /) -> str: ..."}, {"sha": "d79f73c574ffc759ef5d2145b1ec742d85c2500b", "filename": "lib/packaging/__init__.py", "status": "modified", "additions": 2, "deletions": 2, "changes": 4, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpackaging%2F__init__.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpackaging%2F__init__.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpackaging%2F__init__.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -6,10 +6,10 @@\n __summary__ = \"Core utilities for Python packages\"\n __uri__ = \"https://github.com/pypa/packaging\"\n \n-__version__ = \"24.1\"\n+__version__ = \"24.2\"\n \n __author__ = \"Donald Stufft and individual contributors\"\n __email__ = \"donald@stufft.io\"\n \n __license__ = \"BSD-2-Clause or Apache-2.0\"\n-__copyright__ = \"2014 %s\" % __author__\n+__copyright__ = f\"2014 {__author__}\""}, {"sha": "25f4282cc29cb03d7be881f03dee841d7dbc215a", "filename": "lib/packaging/_elffile.py", "status": "modified", "additions": 4, "deletions": 4, "changes": 8, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpackaging%2F_elffile.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpackaging%2F_elffile.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpackaging%2F_elffile.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -48,8 +48,8 @@ def __init__(self, f: IO[bytes]) -> None:\n \n try:\n ident = self._read(\"16B\")\n- except struct.error:\n- raise ELFInvalid(\"unable to parse identification\")\n+ except struct.error as e:\n+ raise ELFInvalid(\"unable to parse identification\") from e\n magic = bytes(ident[:4])\n if magic != b\"\\x7fELF\":\n raise ELFInvalid(f\"invalid magic: {magic!r}\")\n@@ -67,11 +67,11 @@ def __init__(self, f: IO[bytes]) -> None:\n (2, 1): (\"<HHIQQQIHHH\", \"<IIQQQQQQ\", (0, 2, 5)), # 64-bit LSB.\n (2, 2): (\">HHIQQQIHHH\", \">IIQQQQQQ\", (0, 2, 5)), # 64-bit MSB.\n }[(self.capacity, self.encoding)]\n- except KeyError:\n+ except KeyError as e:\n raise ELFInvalid(\n f\"unrecognized capacity ({self.capacity}) or \"\n f\"encoding ({self.encoding})\"\n- )\n+ ) from e\n \n try:\n ("}, {"sha": "61339a6fcc1b82803136f3bf980e0c8f574b2220", "filename": "lib/packaging/_manylinux.py", "status": "modified", "additions": 1, "deletions": 0, "changes": 1, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpackaging%2F_manylinux.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpackaging%2F_manylinux.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpackaging%2F_manylinux.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -164,6 +164,7 @@ def _parse_glibc_version(version_str: str) -> tuple[int, int]:\n f\"Expected glibc version with 2 components major.minor,\"\n f\" got: {version_str}\",\n RuntimeWarning,\n+ stacklevel=2,\n )\n return -1, -1\n return int(m.group(\"major\")), int(m.group(\"minor\"))"}, {"sha": "569156d6ca47719f49b753a4781a86a924de173b", "filename": "lib/packaging/licenses/__init__.py", "status": "added", "additions": 145, "deletions": 0, "changes": 145, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpackaging%2Flicenses%2F__init__.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpackaging%2Flicenses%2F__init__.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpackaging%2Flicenses%2F__init__.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -0,0 +1,145 @@\n+#######################################################################################\n+#\n+# Adapted from:\n+# https://github.com/pypa/hatch/blob/5352e44/backend/src/hatchling/licenses/parse.py\n+#\n+# MIT License\n+#\n+# Copyright (c) 2017-present Ofek Lev <oss@ofek.dev>\n+#\n+# Permission is hereby granted, free of charge, to any person obtaining a copy of this\n+# software and associated documentation files (the \"Software\"), to deal in the Software\n+# without restriction, including without limitation the rights to use, copy, modify,\n+# merge, publish, distribute, sublicense, and/or sell copies of the Software, and to\n+# permit persons to whom the Software is furnished to do so, subject to the following\n+# conditions:\n+#\n+# The above copyright notice and this permission notice shall be included in all copies\n+# or substantial portions of the Software.\n+#\n+# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,\n+# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A\n+# PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF\n+# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE\n+# OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n+#\n+#\n+# With additional allowance of arbitrary `LicenseRef-` identifiers, not just\n+# `LicenseRef-Public-Domain` and `LicenseRef-Proprietary`.\n+#\n+#######################################################################################\n+from __future__ import annotations\n+\n+import re\n+from typing import NewType, cast\n+\n+from packaging.licenses._spdx import EXCEPTIONS, LICENSES\n+\n+__all__ = [\n+ \"NormalizedLicenseExpression\",\n+ \"InvalidLicenseExpression\",\n+ \"canonicalize_license_expression\",\n+]\n+\n+license_ref_allowed = re.compile(\"^[A-Za-z0-9.-]*$\")\n+\n+NormalizedLicenseExpression = NewType(\"NormalizedLicenseExpression\", str)\n+\n+\n+class InvalidLicenseExpression(ValueError):\n+ \"\"\"Raised when a license-expression string is invalid\n+\n+ >>> canonicalize_license_expression(\"invalid\")\n+ Traceback (most recent call last):\n+ ...\n+ packaging.licenses.InvalidLicenseExpression: Invalid license expression: 'invalid'\n+ \"\"\"\n+\n+\n+def canonicalize_license_expression(\n+ raw_license_expression: str,\n+) -> NormalizedLicenseExpression:\n+ if not raw_license_expression:\n+ message = f\"Invalid license expression: {raw_license_expression!r}\"\n+ raise InvalidLicenseExpression(message)\n+\n+ # Pad any parentheses so tokenization can be achieved by merely splitting on\n+ # whitespace.\n+ license_expression = raw_license_expression.replace(\"(\", \" ( \").replace(\")\", \" ) \")\n+ licenseref_prefix = \"LicenseRef-\"\n+ license_refs = {\n+ ref.lower(): \"LicenseRef-\" + ref[len(licenseref_prefix) :]\n+ for ref in license_expression.split()\n+ if ref.lower().startswith(licenseref_prefix.lower())\n+ }\n+\n+ # Normalize to lower case so we can look up licenses/exceptions\n+ # and so boolean operators are Python-compatible.\n+ license_expression = license_expression.lower()\n+\n+ tokens = license_expression.split()\n+\n+ # Rather than implementing boolean logic, we create an expression that Python can\n+ # parse. Everything that is not involved with the grammar itself is treated as\n+ # `False` and the expression should evaluate as such.\n+ python_tokens = []\n+ for token in tokens:\n+ if token not in {\"or\", \"and\", \"with\", \"(\", \")\"}:\n+ python_tokens.append(\"False\")\n+ elif token == \"with\":\n+ python_tokens.append(\"or\")\n+ elif token == \"(\" and python_tokens and python_tokens[-1] not in {\"or\", \"and\"}:\n+ message = f\"Invalid license expression: {raw_license_expression!r}\"\n+ raise InvalidLicenseExpression(message)\n+ else:\n+ python_tokens.append(token)\n+\n+ python_expression = \" \".join(python_tokens)\n+ try:\n+ invalid = eval(python_expression, globals(), locals())\n+ except Exception:\n+ invalid = True\n+\n+ if invalid is not False:\n+ message = f\"Invalid license expression: {raw_license_expression!r}\"\n+ raise InvalidLicenseExpression(message) from None\n+\n+ # Take a final pass to check for unknown licenses/exceptions.\n+ normalized_tokens = []\n+ for token in tokens:\n+ if token in {\"or\", \"and\", \"with\", \"(\", \")\"}:\n+ normalized_tokens.append(token.upper())\n+ continue\n+\n+ if normalized_tokens and normalized_tokens[-1] == \"WITH\":\n+ if token not in EXCEPTIONS:\n+ message = f\"Unknown license exception: {token!r}\"\n+ raise InvalidLicenseExpression(message)\n+\n+ normalized_tokens.append(EXCEPTIONS[token][\"id\"])\n+ else:\n+ if token.endswith(\"+\"):\n+ final_token = token[:-1]\n+ suffix = \"+\"\n+ else:\n+ final_token = token\n+ suffix = \"\"\n+\n+ if final_token.startswith(\"licenseref-\"):\n+ if not license_ref_allowed.match(final_token):\n+ message = f\"Invalid licenseref: {final_token!r}\"\n+ raise InvalidLicenseExpression(message)\n+ normalized_tokens.append(license_refs[final_token] + suffix)\n+ else:\n+ if final_token not in LICENSES:\n+ message = f\"Unknown license: {final_token!r}\"\n+ raise InvalidLicenseExpression(message)\n+ normalized_tokens.append(LICENSES[final_token][\"id\"] + suffix)\n+\n+ normalized_expression = \" \".join(normalized_tokens)\n+\n+ return cast(\n+ NormalizedLicenseExpression,\n+ normalized_expression.replace(\"( \", \"(\").replace(\" )\", \")\"),\n+ )"}, {"sha": "eac22276a34ccd73fc9d70c67ca318a49eb11e77", "filename": "lib/packaging/licenses/_spdx.py", "status": "added", "additions": 759, "deletions": 0, "changes": 759, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpackaging%2Flicenses%2F_spdx.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpackaging%2Flicenses%2F_spdx.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpackaging%2Flicenses%2F_spdx.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -0,0 +1,759 @@\n+\n+from __future__ import annotations\n+\n+from typing import TypedDict\n+\n+class SPDXLicense(TypedDict):\n+ id: str\n+ deprecated: bool\n+\n+class SPDXException(TypedDict):\n+ id: str\n+ deprecated: bool\n+\n+\n+VERSION = '3.25.0'\n+\n+LICENSES: dict[str, SPDXLicense] = {\n+ '0bsd': {'id': '0BSD', 'deprecated': False},\n+ '3d-slicer-1.0': {'id': '3D-Slicer-1.0', 'deprecated': False},\n+ 'aal': {'id': 'AAL', 'deprecated': False},\n+ 'abstyles': {'id': 'Abstyles', 'deprecated': False},\n+ 'adacore-doc': {'id': 'AdaCore-doc', 'deprecated': False},\n+ 'adobe-2006': {'id': 'Adobe-2006', 'deprecated': False},\n+ 'adobe-display-postscript': {'id': 'Adobe-Display-PostScript', 'deprecated': False},\n+ 'adobe-glyph': {'id': 'Adobe-Glyph', 'deprecated': False},\n+ 'adobe-utopia': {'id': 'Adobe-Utopia', 'deprecated': False},\n+ 'adsl': {'id': 'ADSL', 'deprecated': False},\n+ 'afl-1.1': {'id': 'AFL-1.1', 'deprecated': False},\n+ 'afl-1.2': {'id': 'AFL-1.2', 'deprecated': False},\n+ 'afl-2.0': {'id': 'AFL-2.0', 'deprecated': False},\n+ 'afl-2.1': {'id': 'AFL-2.1', 'deprecated': False},\n+ 'afl-3.0': {'id': 'AFL-3.0', 'deprecated': False},\n+ 'afmparse': {'id': 'Afmparse', 'deprecated': False},\n+ 'agpl-1.0': {'id': 'AGPL-1.0', 'deprecated': True},\n+ 'agpl-1.0-only': {'id': 'AGPL-1.0-only', 'deprecated': False},\n+ 'agpl-1.0-or-later': {'id': 'AGPL-1.0-or-later', 'deprecated': False},\n+ 'agpl-3.0': {'id': 'AGPL-3.0', 'deprecated': True},\n+ 'agpl-3.0-only': {'id': 'AGPL-3.0-only', 'deprecated': False},\n+ 'agpl-3.0-or-later': {'id': 'AGPL-3.0-or-later', 'deprecated': False},\n+ 'aladdin': {'id': 'Aladdin', 'deprecated': False},\n+ 'amd-newlib': {'id': 'AMD-newlib', 'deprecated': False},\n+ 'amdplpa': {'id': 'AMDPLPA', 'deprecated': False},\n+ 'aml': {'id': 'AML', 'deprecated': False},\n+ 'aml-glslang': {'id': 'AML-glslang', 'deprecated': False},\n+ 'ampas': {'id': 'AMPAS', 'deprecated': False},\n+ 'antlr-pd': {'id': 'ANTLR-PD', 'deprecated': False},\n+ 'antlr-pd-fallback': {'id': 'ANTLR-PD-fallback', 'deprecated': False},\n+ 'any-osi': {'id': 'any-OSI', 'deprecated': False},\n+ 'apache-1.0': {'id': 'Apache-1.0', 'deprecated': False},\n+ 'apache-1.1': {'id': 'Apache-1.1', 'deprecated': False},\n+ 'apache-2.0': {'id': 'Apache-2.0', 'deprecated': False},\n+ 'apafml': {'id': 'APAFML', 'deprecated': False},\n+ 'apl-1.0': {'id': 'APL-1.0', 'deprecated': False},\n+ 'app-s2p': {'id': 'App-s2p', 'deprecated': False},\n+ 'apsl-1.0': {'id': 'APSL-1.0', 'deprecated': False},\n+ 'apsl-1.1': {'id': 'APSL-1.1', 'deprecated': False},\n+ 'apsl-1.2': {'id': 'APSL-1.2', 'deprecated': False},\n+ 'apsl-2.0': {'id': 'APSL-2.0', 'deprecated': False},\n+ 'arphic-1999': {'id': 'Arphic-1999', 'deprecated': False},\n+ 'artistic-1.0': {'id': 'Artistic-1.0', 'deprecated': False},\n+ 'artistic-1.0-cl8': {'id': 'Artistic-1.0-cl8', 'deprecated': False},\n+ 'artistic-1.0-perl': {'id': 'Artistic-1.0-Perl', 'deprecated': False},\n+ 'artistic-2.0': {'id': 'Artistic-2.0', 'deprecated': False},\n+ 'aswf-digital-assets-1.0': {'id': 'ASWF-Digital-Assets-1.0', 'deprecated': False},\n+ 'aswf-digital-assets-1.1': {'id': 'ASWF-Digital-Assets-1.1', 'deprecated': False},\n+ 'baekmuk': {'id': 'Baekmuk', 'deprecated': False},\n+ 'bahyph': {'id': 'Bahyph', 'deprecated': False},\n+ 'barr': {'id': 'Barr', 'deprecated': False},\n+ 'bcrypt-solar-designer': {'id': 'bcrypt-Solar-Designer', 'deprecated': False},\n+ 'beerware': {'id': 'Beerware', 'deprecated': False},\n+ 'bitstream-charter': {'id': 'Bitstream-Charter', 'deprecated': False},\n+ 'bitstream-vera': {'id': 'Bitstream-Vera', 'deprecated': False},\n+ 'bittorrent-1.0': {'id': 'BitTorrent-1.0', 'deprecated': False},\n+ 'bittorrent-1.1': {'id': 'BitTorrent-1.1', 'deprecated': False},\n+ 'blessing': {'id': 'blessing', 'deprecated': False},\n+ 'blueoak-1.0.0': {'id': 'BlueOak-1.0.0', 'deprecated': False},\n+ 'boehm-gc': {'id': 'Boehm-GC', 'deprecated': False},\n+ 'borceux': {'id': 'Borceux', 'deprecated': False},\n+ 'brian-gladman-2-clause': {'id': 'Brian-Gladman-2-Clause', 'deprecated': False},\n+ 'brian-gladman-3-clause': {'id': 'Brian-Gladman-3-Clause', 'deprecated': False},\n+ 'bsd-1-clause': {'id': 'BSD-1-Clause', 'deprecated': False},\n+ 'bsd-2-clause': {'id': 'BSD-2-Clause', 'deprecated': False},\n+ 'bsd-2-clause-darwin': {'id': 'BSD-2-Clause-Darwin', 'deprecated': False},\n+ 'bsd-2-clause-first-lines': {'id': 'BSD-2-Clause-first-lines', 'deprecated': False},\n+ 'bsd-2-clause-freebsd': {'id': 'BSD-2-Clause-FreeBSD', 'deprecated': True},\n+ 'bsd-2-clause-netbsd': {'id': 'BSD-2-Clause-NetBSD', 'deprecated': True},\n+ 'bsd-2-clause-patent': {'id': 'BSD-2-Clause-Patent', 'deprecated': False},\n+ 'bsd-2-clause-views': {'id': 'BSD-2-Clause-Views', 'deprecated': False},\n+ 'bsd-3-clause': {'id': 'BSD-3-Clause', 'deprecated': False},\n+ 'bsd-3-clause-acpica': {'id': 'BSD-3-Clause-acpica', 'deprecated': False},\n+ 'bsd-3-clause-attribution': {'id': 'BSD-3-Clause-Attribution', 'deprecated': False},\n+ 'bsd-3-clause-clear': {'id': 'BSD-3-Clause-Clear', 'deprecated': False},\n+ 'bsd-3-clause-flex': {'id': 'BSD-3-Clause-flex', 'deprecated': False},\n+ 'bsd-3-clause-hp': {'id': 'BSD-3-Clause-HP', 'deprecated': False},\n+ 'bsd-3-clause-lbnl': {'id': 'BSD-3-Clause-LBNL', 'deprecated': False},\n+ 'bsd-3-clause-modification': {'id': 'BSD-3-Clause-Modification', 'deprecated': False},\n+ 'bsd-3-clause-no-military-license': {'id': 'BSD-3-Clause-No-Military-License', 'deprecated': False},\n+ 'bsd-3-clause-no-nuclear-license': {'id': 'BSD-3-Clause-No-Nuclear-License', 'deprecated': False},\n+ 'bsd-3-clause-no-nuclear-license-2014': {'id': 'BSD-3-Clause-No-Nuclear-License-2014', 'deprecated': False},\n+ 'bsd-3-clause-no-nuclear-warranty': {'id': 'BSD-3-Clause-No-Nuclear-Warranty', 'deprecated': False},\n+ 'bsd-3-clause-open-mpi': {'id': 'BSD-3-Clause-Open-MPI', 'deprecated': False},\n+ 'bsd-3-clause-sun': {'id': 'BSD-3-Clause-Sun', 'deprecated': False},\n+ 'bsd-4-clause': {'id': 'BSD-4-Clause', 'deprecated': False},\n+ 'bsd-4-clause-shortened': {'id': 'BSD-4-Clause-Shortened', 'deprecated': False},\n+ 'bsd-4-clause-uc': {'id': 'BSD-4-Clause-UC', 'deprecated': False},\n+ 'bsd-4.3reno': {'id': 'BSD-4.3RENO', 'deprecated': False},\n+ 'bsd-4.3tahoe': {'id': 'BSD-4.3TAHOE', 'deprecated': False},\n+ 'bsd-advertising-acknowledgement': {'id': 'BSD-Advertising-Acknowledgement', 'deprecated': False},\n+ 'bsd-attribution-hpnd-disclaimer': {'id': 'BSD-Attribution-HPND-disclaimer', 'deprecated': False},\n+ 'bsd-inferno-nettverk': {'id': 'BSD-Inferno-Nettverk', 'deprecated': False},\n+ 'bsd-protection': {'id': 'BSD-Protection', 'deprecated': False},\n+ 'bsd-source-beginning-file': {'id': 'BSD-Source-beginning-file', 'deprecated': False},\n+ 'bsd-source-code': {'id': 'BSD-Source-Code', 'deprecated': False},\n+ 'bsd-systemics': {'id': 'BSD-Systemics', 'deprecated': False},\n+ 'bsd-systemics-w3works': {'id': 'BSD-Systemics-W3Works', 'deprecated': False},\n+ 'bsl-1.0': {'id': 'BSL-1.0', 'deprecated': False},\n+ 'busl-1.1': {'id': 'BUSL-1.1', 'deprecated': False},\n+ 'bzip2-1.0.5': {'id': 'bzip2-1.0.5', 'deprecated': True},\n+ 'bzip2-1.0.6': {'id': 'bzip2-1.0.6', 'deprecated': False},\n+ 'c-uda-1.0': {'id': 'C-UDA-1.0', 'deprecated': False},\n+ 'cal-1.0': {'id': 'CAL-1.0', 'deprecated': False},\n+ 'cal-1.0-combined-work-exception': {'id': 'CAL-1.0-Combined-Work-Exception', 'deprecated': False},\n+ 'caldera': {'id': 'Caldera', 'deprecated': False},\n+ 'caldera-no-preamble': {'id': 'Caldera-no-preamble', 'deprecated': False},\n+ 'catharon': {'id': 'Catharon', 'deprecated': False},\n+ 'catosl-1.1': {'id': 'CATOSL-1.1', 'deprecated': False},\n+ 'cc-by-1.0': {'id': 'CC-BY-1.0', 'deprecated': False},\n+ 'cc-by-2.0': {'id': 'CC-BY-2.0', 'deprecated': False},\n+ 'cc-by-2.5': {'id': 'CC-BY-2.5', 'deprecated': False},\n+ 'cc-by-2.5-au': {'id': 'CC-BY-2.5-AU', 'deprecated': False},\n+ 'cc-by-3.0': {'id': 'CC-BY-3.0', 'deprecated': False},\n+ 'cc-by-3.0-at': {'id': 'CC-BY-3.0-AT', 'deprecated': False},\n+ 'cc-by-3.0-au': {'id': 'CC-BY-3.0-AU', 'deprecated': False},\n+ 'cc-by-3.0-de': {'id': 'CC-BY-3.0-DE', 'deprecated': False},\n+ 'cc-by-3.0-igo': {'id': 'CC-BY-3.0-IGO', 'deprecated': False},\n+ 'cc-by-3.0-nl': {'id': 'CC-BY-3.0-NL', 'deprecated': False},\n+ 'cc-by-3.0-us': {'id': 'CC-BY-3.0-US', 'deprecated': False},\n+ 'cc-by-4.0': {'id': 'CC-BY-4.0', 'deprecated': False},\n+ 'cc-by-nc-1.0': {'id': 'CC-BY-NC-1.0', 'deprecated': False},\n+ 'cc-by-nc-2.0': {'id': 'CC-BY-NC-2.0', 'deprecated': False},\n+ 'cc-by-nc-2.5': {'id': 'CC-BY-NC-2.5', 'deprecated': False},\n+ 'cc-by-nc-3.0': {'id': 'CC-BY-NC-3.0', 'deprecated': False},\n+ 'cc-by-nc-3.0-de': {'id': 'CC-BY-NC-3.0-DE', 'deprecated': False},\n+ 'cc-by-nc-4.0': {'id': 'CC-BY-NC-4.0', 'deprecated': False},\n+ 'cc-by-nc-nd-1.0': {'id': 'CC-BY-NC-ND-1.0', 'deprecated': False},\n+ 'cc-by-nc-nd-2.0': {'id': 'CC-BY-NC-ND-2.0', 'deprecated': False},\n+ 'cc-by-nc-nd-2.5': {'id': 'CC-BY-NC-ND-2.5', 'deprecated': False},\n+ 'cc-by-nc-nd-3.0': {'id': 'CC-BY-NC-ND-3.0', 'deprecated': False},\n+ 'cc-by-nc-nd-3.0-de': {'id': 'CC-BY-NC-ND-3.0-DE', 'deprecated': False},\n+ 'cc-by-nc-nd-3.0-igo': {'id': 'CC-BY-NC-ND-3.0-IGO', 'deprecated': False},\n+ 'cc-by-nc-nd-4.0': {'id': 'CC-BY-NC-ND-4.0', 'deprecated': False},\n+ 'cc-by-nc-sa-1.0': {'id': 'CC-BY-NC-SA-1.0', 'deprecated': False},\n+ 'cc-by-nc-sa-2.0': {'id': 'CC-BY-NC-SA-2.0', 'deprecated': False},\n+ 'cc-by-nc-sa-2.0-de': {'id': 'CC-BY-NC-SA-2.0-DE', 'deprecated': False},\n+ 'cc-by-nc-sa-2.0-fr': {'id': 'CC-BY-NC-SA-2.0-FR', 'deprecated': False},\n+ 'cc-by-nc-sa-2.0-uk': {'id': 'CC-BY-NC-SA-2.0-UK', 'deprecated': False},\n+ 'cc-by-nc-sa-2.5': {'id': 'CC-BY-NC-SA-2.5', 'deprecated': False},\n+ 'cc-by-nc-sa-3.0': {'id': 'CC-BY-NC-SA-3.0', 'deprecated': False},\n+ 'cc-by-nc-sa-3.0-de': {'id': 'CC-BY-NC-SA-3.0-DE', 'deprecated': False},\n+ 'cc-by-nc-sa-3.0-igo': {'id': 'CC-BY-NC-SA-3.0-IGO', 'deprecated': False},\n+ 'cc-by-nc-sa-4.0': {'id': 'CC-BY-NC-SA-4.0', 'deprecated': False},\n+ 'cc-by-nd-1.0': {'id': 'CC-BY-ND-1.0', 'deprecated': False},\n+ 'cc-by-nd-2.0': {'id': 'CC-BY-ND-2.0', 'deprecated': False},\n+ 'cc-by-nd-2.5': {'id': 'CC-BY-ND-2.5', 'deprecated': False},\n+ 'cc-by-nd-3.0': {'id': 'CC-BY-ND-3.0', 'deprecated': False},\n+ 'cc-by-nd-3.0-de': {'id': 'CC-BY-ND-3.0-DE', 'deprecated': False},\n+ 'cc-by-nd-4.0': {'id': 'CC-BY-ND-4.0', 'deprecated': False},\n+ 'cc-by-sa-1.0': {'id': 'CC-BY-SA-1.0', 'deprecated': False},\n+ 'cc-by-sa-2.0': {'id': 'CC-BY-SA-2.0', 'deprecated': False},\n+ 'cc-by-sa-2.0-uk': {'id': 'CC-BY-SA-2.0-UK', 'deprecated': False},\n+ 'cc-by-sa-2.1-jp': {'id': 'CC-BY-SA-2.1-JP', 'deprecated': False},\n+ 'cc-by-sa-2.5': {'id': 'CC-BY-SA-2.5', 'deprecated': False},\n+ 'cc-by-sa-3.0': {'id': 'CC-BY-SA-3.0', 'deprecated': False},\n+ 'cc-by-sa-3.0-at': {'id': 'CC-BY-SA-3.0-AT', 'deprecated': False},\n+ 'cc-by-sa-3.0-de': {'id': 'CC-BY-SA-3.0-DE', 'deprecated': False},\n+ 'cc-by-sa-3.0-igo': {'id': 'CC-BY-SA-3.0-IGO', 'deprecated': False},\n+ 'cc-by-sa-4.0': {'id': 'CC-BY-SA-4.0', 'deprecated': False},\n+ 'cc-pddc': {'id': 'CC-PDDC', 'deprecated': False},\n+ 'cc0-1.0': {'id': 'CC0-1.0', 'deprecated': False},\n+ 'cddl-1.0': {'id': 'CDDL-1.0', 'deprecated': False},\n+ 'cddl-1.1': {'id': 'CDDL-1.1', 'deprecated': False},\n+ 'cdl-1.0': {'id': 'CDL-1.0', 'deprecated': False},\n+ 'cdla-permissive-1.0': {'id': 'CDLA-Permissive-1.0', 'deprecated': False},\n+ 'cdla-permissive-2.0': {'id': 'CDLA-Permissive-2.0', 'deprecated': False},\n+ 'cdla-sharing-1.0': {'id': 'CDLA-Sharing-1.0', 'deprecated': False},\n+ 'cecill-1.0': {'id': 'CECILL-1.0', 'deprecated': False},\n+ 'cecill-1.1': {'id': 'CECILL-1.1', 'deprecated': False},\n+ 'cecill-2.0': {'id': 'CECILL-2.0', 'deprecated': False},\n+ 'cecill-2.1': {'id': 'CECILL-2.1', 'deprecated': False},\n+ 'cecill-b': {'id': 'CECILL-B', 'deprecated': False},\n+ 'cecill-c': {'id': 'CECILL-C', 'deprecated': False},\n+ 'cern-ohl-1.1': {'id': 'CERN-OHL-1.1', 'deprecated': False},\n+ 'cern-ohl-1.2': {'id': 'CERN-OHL-1.2', 'deprecated': False},\n+ 'cern-ohl-p-2.0': {'id': 'CERN-OHL-P-2.0', 'deprecated': False},\n+ 'cern-ohl-s-2.0': {'id': 'CERN-OHL-S-2.0', 'deprecated': False},\n+ 'cern-ohl-w-2.0': {'id': 'CERN-OHL-W-2.0', 'deprecated': False},\n+ 'cfitsio': {'id': 'CFITSIO', 'deprecated': False},\n+ 'check-cvs': {'id': 'check-cvs', 'deprecated': False},\n+ 'checkmk': {'id': 'checkmk', 'deprecated': False},\n+ 'clartistic': {'id': 'ClArtistic', 'deprecated': False},\n+ 'clips': {'id': 'Clips', 'deprecated': False},\n+ 'cmu-mach': {'id': 'CMU-Mach', 'deprecated': False},\n+ 'cmu-mach-nodoc': {'id': 'CMU-Mach-nodoc', 'deprecated': False},\n+ 'cnri-jython': {'id': 'CNRI-Jython', 'deprecated': False},\n+ 'cnri-python': {'id': 'CNRI-Python', 'deprecated': False},\n+ 'cnri-python-gpl-compatible': {'id': 'CNRI-Python-GPL-Compatible', 'deprecated': False},\n+ 'coil-1.0': {'id': 'COIL-1.0', 'deprecated': False},\n+ 'community-spec-1.0': {'id': 'Community-Spec-1.0', 'deprecated': False},\n+ 'condor-1.1': {'id': 'Condor-1.1', 'deprecated': False},\n+ 'copyleft-next-0.3.0': {'id': 'copyleft-next-0.3.0', 'deprecated': False},\n+ 'copyleft-next-0.3.1': {'id': 'copyleft-next-0.3.1', 'deprecated': False},\n+ 'cornell-lossless-jpeg': {'id': 'Cornell-Lossless-JPEG', 'deprecated': False},\n+ 'cpal-1.0': {'id': 'CPAL-1.0', 'deprecated': False},\n+ 'cpl-1.0': {'id': 'CPL-1.0', 'deprecated': False},\n+ 'cpol-1.02': {'id': 'CPOL-1.02', 'deprecated': False},\n+ 'cronyx': {'id': 'Cronyx', 'deprecated': False},\n+ 'crossword': {'id': 'Crossword', 'deprecated': False},\n+ 'crystalstacker': {'id': 'CrystalStacker', 'deprecated': False},\n+ 'cua-opl-1.0': {'id': 'CUA-OPL-1.0', 'deprecated': False},\n+ 'cube': {'id': 'Cube', 'deprecated': False},\n+ 'curl': {'id': 'curl', 'deprecated': False},\n+ 'cve-tou': {'id': 'cve-tou', 'deprecated': False},\n+ 'd-fsl-1.0': {'id': 'D-FSL-1.0', 'deprecated': False},\n+ 'dec-3-clause': {'id': 'DEC-3-Clause', 'deprecated': False},\n+ 'diffmark': {'id': 'diffmark', 'deprecated': False},\n+ 'dl-de-by-2.0': {'id': 'DL-DE-BY-2.0', 'deprecated': False},\n+ 'dl-de-zero-2.0': {'id': 'DL-DE-ZERO-2.0', 'deprecated': False},\n+ 'doc': {'id': 'DOC', 'deprecated': False},\n+ 'docbook-schema': {'id': 'DocBook-Schema', 'deprecated': False},\n+ 'docbook-xml': {'id': 'DocBook-XML', 'deprecated': False},\n+ 'dotseqn': {'id': 'Dotseqn', 'deprecated': False},\n+ 'drl-1.0': {'id': 'DRL-1.0', 'deprecated': False},\n+ 'drl-1.1': {'id': 'DRL-1.1', 'deprecated': False},\n+ 'dsdp': {'id': 'DSDP', 'deprecated': False},\n+ 'dtoa': {'id': 'dtoa', 'deprecated': False},\n+ 'dvipdfm': {'id': 'dvipdfm', 'deprecated': False},\n+ 'ecl-1.0': {'id': 'ECL-1.0', 'deprecated': False},\n+ 'ecl-2.0': {'id': 'ECL-2.0', 'deprecated': False},\n+ 'ecos-2.0': {'id': 'eCos-2.0', 'deprecated': True},\n+ 'efl-1.0': {'id': 'EFL-1.0', 'deprecated': False},\n+ 'efl-2.0': {'id': 'EFL-2.0', 'deprecated': False},\n+ 'egenix': {'id': 'eGenix', 'deprecated': False},\n+ 'elastic-2.0': {'id': 'Elastic-2.0', 'deprecated': False},\n+ 'entessa': {'id': 'Entessa', 'deprecated': False},\n+ 'epics': {'id': 'EPICS', 'deprecated': False},\n+ 'epl-1.0': {'id': 'EPL-1.0', 'deprecated': False},\n+ 'epl-2.0': {'id': 'EPL-2.0', 'deprecated': False},\n+ 'erlpl-1.1': {'id': 'ErlPL-1.1', 'deprecated': False},\n+ 'etalab-2.0': {'id': 'etalab-2.0', 'deprecated': False},\n+ 'eudatagrid': {'id': 'EUDatagrid', 'deprecated': False},\n+ 'eupl-1.0': {'id': 'EUPL-1.0', 'deprecated': False},\n+ 'eupl-1.1': {'id': 'EUPL-1.1', 'deprecated': False},\n+ 'eupl-1.2': {'id': 'EUPL-1.2', 'deprecated': False},\n+ 'eurosym': {'id': 'Eurosym', 'deprecated': False},\n+ 'fair': {'id': 'Fair', 'deprecated': False},\n+ 'fbm': {'id': 'FBM', 'deprecated': False},\n+ 'fdk-aac': {'id': 'FDK-AAC', 'deprecated': False},\n+ 'ferguson-twofish': {'id': 'Ferguson-Twofish', 'deprecated': False},\n+ 'frameworx-1.0': {'id': 'Frameworx-1.0', 'deprecated': False},\n+ 'freebsd-doc': {'id': 'FreeBSD-DOC', 'deprecated': False},\n+ 'freeimage': {'id': 'FreeImage', 'deprecated': False},\n+ 'fsfap': {'id': 'FSFAP', 'deprecated': False},\n+ 'fsfap-no-warranty-disclaimer': {'id': 'FSFAP-no-warranty-disclaimer', 'deprecated': False},\n+ 'fsful': {'id': 'FSFUL', 'deprecated': False},\n+ 'fsfullr': {'id': 'FSFULLR', 'deprecated': False},\n+ 'fsfullrwd': {'id': 'FSFULLRWD', 'deprecated': False},\n+ 'ftl': {'id': 'FTL', 'deprecated': False},\n+ 'furuseth': {'id': 'Furuseth', 'deprecated': False},\n+ 'fwlw': {'id': 'fwlw', 'deprecated': False},\n+ 'gcr-docs': {'id': 'GCR-docs', 'deprecated': False},\n+ 'gd': {'id': 'GD', 'deprecated': False},\n+ 'gfdl-1.1': {'id': 'GFDL-1.1', 'deprecated': True},\n+ 'gfdl-1.1-invariants-only': {'id': 'GFDL-1.1-invariants-only', 'deprecated': False},\n+ 'gfdl-1.1-invariants-or-later': {'id': 'GFDL-1.1-invariants-or-later', 'deprecated': False},\n+ 'gfdl-1.1-no-invariants-only': {'id': 'GFDL-1.1-no-invariants-only', 'deprecated': False},\n+ 'gfdl-1.1-no-invariants-or-later': {'id': 'GFDL-1.1-no-invariants-or-later', 'deprecated': False},\n+ 'gfdl-1.1-only': {'id': 'GFDL-1.1-only', 'deprecated': False},\n+ 'gfdl-1.1-or-later': {'id': 'GFDL-1.1-or-later', 'deprecated': False},\n+ 'gfdl-1.2': {'id': 'GFDL-1.2', 'deprecated': True},\n+ 'gfdl-1.2-invariants-only': {'id': 'GFDL-1.2-invariants-only', 'deprecated': False},\n+ 'gfdl-1.2-invariants-or-later': {'id': 'GFDL-1.2-invariants-or-later', 'deprecated': False},\n+ 'gfdl-1.2-no-invariants-only': {'id': 'GFDL-1.2-no-invariants-only', 'deprecated': False},\n+ 'gfdl-1.2-no-invariants-or-later': {'id': 'GFDL-1.2-no-invariants-or-later', 'deprecated': False},\n+ 'gfdl-1.2-only': {'id': 'GFDL-1.2-only', 'deprecated': False},\n+ 'gfdl-1.2-or-later': {'id': 'GFDL-1.2-or-later', 'deprecated': False},\n+ 'gfdl-1.3': {'id': 'GFDL-1.3', 'deprecated': True},\n+ 'gfdl-1.3-invariants-only': {'id': 'GFDL-1.3-invariants-only', 'deprecated': False},\n+ 'gfdl-1.3-invariants-or-later': {'id': 'GFDL-1.3-invariants-or-later', 'deprecated': False},\n+ 'gfdl-1.3-no-invariants-only': {'id': 'GFDL-1.3-no-invariants-only', 'deprecated': False},\n+ 'gfdl-1.3-no-invariants-or-later': {'id': 'GFDL-1.3-no-invariants-or-later', 'deprecated': False},\n+ 'gfdl-1.3-only': {'id': 'GFDL-1.3-only', 'deprecated': False},\n+ 'gfdl-1.3-or-later': {'id': 'GFDL-1.3-or-later', 'deprecated': False},\n+ 'giftware': {'id': 'Giftware', 'deprecated': False},\n+ 'gl2ps': {'id': 'GL2PS', 'deprecated': False},\n+ 'glide': {'id': 'Glide', 'deprecated': False},\n+ 'glulxe': {'id': 'Glulxe', 'deprecated': False},\n+ 'glwtpl': {'id': 'GLWTPL', 'deprecated': False},\n+ 'gnuplot': {'id': 'gnuplot', 'deprecated': False},\n+ 'gpl-1.0': {'id': 'GPL-1.0', 'deprecated': True},\n+ 'gpl-1.0+': {'id': 'GPL-1.0+', 'deprecated': True},\n+ 'gpl-1.0-only': {'id': 'GPL-1.0-only', 'deprecated': False},\n+ 'gpl-1.0-or-later': {'id': 'GPL-1.0-or-later', 'deprecated': False},\n+ 'gpl-2.0': {'id': 'GPL-2.0', 'deprecated': True},\n+ 'gpl-2.0+': {'id': 'GPL-2.0+', 'deprecated': True},\n+ 'gpl-2.0-only': {'id': 'GPL-2.0-only', 'deprecated': False},\n+ 'gpl-2.0-or-later': {'id': 'GPL-2.0-or-later', 'deprecated': False},\n+ 'gpl-2.0-with-autoconf-exception': {'id': 'GPL-2.0-with-autoconf-exception', 'deprecated': True},\n+ 'gpl-2.0-with-bison-exception': {'id': 'GPL-2.0-with-bison-exception', 'deprecated': True},\n+ 'gpl-2.0-with-classpath-exception': {'id': 'GPL-2.0-with-classpath-exception', 'deprecated': True},\n+ 'gpl-2.0-with-font-exception': {'id': 'GPL-2.0-with-font-exception', 'deprecated': True},\n+ 'gpl-2.0-with-gcc-exception': {'id': 'GPL-2.0-with-GCC-exception', 'deprecated': True},\n+ 'gpl-3.0': {'id': 'GPL-3.0', 'deprecated': True},\n+ 'gpl-3.0+': {'id': 'GPL-3.0+', 'deprecated': True},\n+ 'gpl-3.0-only': {'id': 'GPL-3.0-only', 'deprecated': False},\n+ 'gpl-3.0-or-later': {'id': 'GPL-3.0-or-later', 'deprecated': False},\n+ 'gpl-3.0-with-autoconf-exception': {'id': 'GPL-3.0-with-autoconf-exception', 'deprecated': True},\n+ 'gpl-3.0-with-gcc-exception': {'id': 'GPL-3.0-with-GCC-exception', 'deprecated': True},\n+ 'graphics-gems': {'id': 'Graphics-Gems', 'deprecated': False},\n+ 'gsoap-1.3b': {'id': 'gSOAP-1.3b', 'deprecated': False},\n+ 'gtkbook': {'id': 'gtkbook', 'deprecated': False},\n+ 'gutmann': {'id': 'Gutmann', 'deprecated': False},\n+ 'haskellreport': {'id': 'HaskellReport', 'deprecated': False},\n+ 'hdparm': {'id': 'hdparm', 'deprecated': False},\n+ 'hidapi': {'id': 'HIDAPI', 'deprecated': False},\n+ 'hippocratic-2.1': {'id': 'Hippocratic-2.1', 'deprecated': False},\n+ 'hp-1986': {'id': 'HP-1986', 'deprecated': False},\n+ 'hp-1989': {'id': 'HP-1989', 'deprecated': False},\n+ 'hpnd': {'id': 'HPND', 'deprecated': False},\n+ 'hpnd-dec': {'id': 'HPND-DEC', 'deprecated': False},\n+ 'hpnd-doc': {'id': 'HPND-doc', 'deprecated': False},\n+ 'hpnd-doc-sell': {'id': 'HPND-doc-sell', 'deprecated': False},\n+ 'hpnd-export-us': {'id': 'HPND-export-US', 'deprecated': False},\n+ 'hpnd-export-us-acknowledgement': {'id': 'HPND-export-US-acknowledgement', 'deprecated': False},\n+ 'hpnd-export-us-modify': {'id': 'HPND-export-US-modify', 'deprecated': False},\n+ 'hpnd-export2-us': {'id': 'HPND-export2-US', 'deprecated': False},\n+ 'hpnd-fenneberg-livingston': {'id': 'HPND-Fenneberg-Livingston', 'deprecated': False},\n+ 'hpnd-inria-imag': {'id': 'HPND-INRIA-IMAG', 'deprecated': False},\n+ 'hpnd-intel': {'id': 'HPND-Intel', 'deprecated': False},\n+ 'hpnd-kevlin-henney': {'id': 'HPND-Kevlin-Henney', 'deprecated': False},\n+ 'hpnd-markus-kuhn': {'id': 'HPND-Markus-Kuhn', 'deprecated': False},\n+ 'hpnd-merchantability-variant': {'id': 'HPND-merchantability-variant', 'deprecated': False},\n+ 'hpnd-mit-disclaimer': {'id': 'HPND-MIT-disclaimer', 'deprecated': False},\n+ 'hpnd-netrek': {'id': 'HPND-Netrek', 'deprecated': False},\n+ 'hpnd-pbmplus': {'id': 'HPND-Pbmplus', 'deprecated': False},\n+ 'hpnd-sell-mit-disclaimer-xserver': {'id': 'HPND-sell-MIT-disclaimer-xserver', 'deprecated': False},\n+ 'hpnd-sell-regexpr': {'id': 'HPND-sell-regexpr', 'deprecated': False},\n+ 'hpnd-sell-variant': {'id': 'HPND-sell-variant', 'deprecated': False},\n+ 'hpnd-sell-variant-mit-disclaimer': {'id': 'HPND-sell-variant-MIT-disclaimer', 'deprecated': False},\n+ 'hpnd-sell-variant-mit-disclaimer-rev': {'id': 'HPND-sell-variant-MIT-disclaimer-rev', 'deprecated': False},\n+ 'hpnd-uc': {'id': 'HPND-UC', 'deprecated': False},\n+ 'hpnd-uc-export-us': {'id': 'HPND-UC-export-US', 'deprecated': False},\n+ 'htmltidy': {'id': 'HTMLTIDY', 'deprecated': False},\n+ 'ibm-pibs': {'id': 'IBM-pibs', 'deprecated': False},\n+ 'icu': {'id': 'ICU', 'deprecated': False},\n+ 'iec-code-components-eula': {'id': 'IEC-Code-Components-EULA', 'deprecated': False},\n+ 'ijg': {'id': 'IJG', 'deprecated': False},\n+ 'ijg-short': {'id': 'IJG-short', 'deprecated': False},\n+ 'imagemagick': {'id': 'ImageMagick', 'deprecated': False},\n+ 'imatix': {'id': 'iMatix', 'deprecated': False},\n+ 'imlib2': {'id': 'Imlib2', 'deprecated': False},\n+ 'info-zip': {'id': 'Info-ZIP', 'deprecated': False},\n+ 'inner-net-2.0': {'id': 'Inner-Net-2.0', 'deprecated': False},\n+ 'intel': {'id': 'Intel', 'deprecated': False},\n+ 'intel-acpi': {'id': 'Intel-ACPI', 'deprecated': False},\n+ 'interbase-1.0': {'id': 'Interbase-1.0', 'deprecated': False},\n+ 'ipa': {'id': 'IPA', 'deprecated': False},\n+ 'ipl-1.0': {'id': 'IPL-1.0', 'deprecated': False},\n+ 'isc': {'id': 'ISC', 'deprecated': False},\n+ 'isc-veillard': {'id': 'ISC-Veillard', 'deprecated': False},\n+ 'jam': {'id': 'Jam', 'deprecated': False},\n+ 'jasper-2.0': {'id': 'JasPer-2.0', 'deprecated': False},\n+ 'jpl-image': {'id': 'JPL-image', 'deprecated': False},\n+ 'jpnic': {'id': 'JPNIC', 'deprecated': False},\n+ 'json': {'id': 'JSON', 'deprecated': False},\n+ 'kastrup': {'id': 'Kastrup', 'deprecated': False},\n+ 'kazlib': {'id': 'Kazlib', 'deprecated': False},\n+ 'knuth-ctan': {'id': 'Knuth-CTAN', 'deprecated': False},\n+ 'lal-1.2': {'id': 'LAL-1.2', 'deprecated': False},\n+ 'lal-1.3': {'id': 'LAL-1.3', 'deprecated': False},\n+ 'latex2e': {'id': 'Latex2e', 'deprecated': False},\n+ 'latex2e-translated-notice': {'id': 'Latex2e-translated-notice', 'deprecated': False},\n+ 'leptonica': {'id': 'Leptonica', 'deprecated': False},\n+ 'lgpl-2.0': {'id': 'LGPL-2.0', 'deprecated': True},\n+ 'lgpl-2.0+': {'id': 'LGPL-2.0+', 'deprecated': True},\n+ 'lgpl-2.0-only': {'id': 'LGPL-2.0-only', 'deprecated': False},\n+ 'lgpl-2.0-or-later': {'id': 'LGPL-2.0-or-later', 'deprecated': False},\n+ 'lgpl-2.1': {'id': 'LGPL-2.1', 'deprecated': True},\n+ 'lgpl-2.1+': {'id': 'LGPL-2.1+', 'deprecated': True},\n+ 'lgpl-2.1-only': {'id': 'LGPL-2.1-only', 'deprecated': False},\n+ 'lgpl-2.1-or-later': {'id': 'LGPL-2.1-or-later', 'deprecated': False},\n+ 'lgpl-3.0': {'id': 'LGPL-3.0', 'deprecated': True},\n+ 'lgpl-3.0+': {'id': 'LGPL-3.0+', 'deprecated': True},\n+ 'lgpl-3.0-only': {'id': 'LGPL-3.0-only', 'deprecated': False},\n+ 'lgpl-3.0-or-later': {'id': 'LGPL-3.0-or-later', 'deprecated': False},\n+ 'lgpllr': {'id': 'LGPLLR', 'deprecated': False},\n+ 'libpng': {'id': 'Libpng', 'deprecated': False},\n+ 'libpng-2.0': {'id': 'libpng-2.0', 'deprecated': False},\n+ 'libselinux-1.0': {'id': 'libselinux-1.0', 'deprecated': False},\n+ 'libtiff': {'id': 'libtiff', 'deprecated': False},\n+ 'libutil-david-nugent': {'id': 'libutil-David-Nugent', 'deprecated': False},\n+ 'liliq-p-1.1': {'id': 'LiLiQ-P-1.1', 'deprecated': False},\n+ 'liliq-r-1.1': {'id': 'LiLiQ-R-1.1', 'deprecated': False},\n+ 'liliq-rplus-1.1': {'id': 'LiLiQ-Rplus-1.1', 'deprecated': False},\n+ 'linux-man-pages-1-para': {'id': 'Linux-man-pages-1-para', 'deprecated': False},\n+ 'linux-man-pages-copyleft': {'id': 'Linux-man-pages-copyleft', 'deprecated': False},\n+ 'linux-man-pages-copyleft-2-para': {'id': 'Linux-man-pages-copyleft-2-para', 'deprecated': False},\n+ 'linux-man-pages-copyleft-var': {'id': 'Linux-man-pages-copyleft-var', 'deprecated': False},\n+ 'linux-openib': {'id': 'Linux-OpenIB', 'deprecated': False},\n+ 'loop': {'id': 'LOOP', 'deprecated': False},\n+ 'lpd-document': {'id': 'LPD-document', 'deprecated': False},\n+ 'lpl-1.0': {'id': 'LPL-1.0', 'deprecated': False},\n+ 'lpl-1.02': {'id': 'LPL-1.02', 'deprecated': False},\n+ 'lppl-1.0': {'id': 'LPPL-1.0', 'deprecated': False},\n+ 'lppl-1.1': {'id': 'LPPL-1.1', 'deprecated': False},\n+ 'lppl-1.2': {'id': 'LPPL-1.2', 'deprecated': False},\n+ 'lppl-1.3a': {'id': 'LPPL-1.3a', 'deprecated': False},\n+ 'lppl-1.3c': {'id': 'LPPL-1.3c', 'deprecated': False},\n+ 'lsof': {'id': 'lsof', 'deprecated': False},\n+ 'lucida-bitmap-fonts': {'id': 'Lucida-Bitmap-Fonts', 'deprecated': False},\n+ 'lzma-sdk-9.11-to-9.20': {'id': 'LZMA-SDK-9.11-to-9.20', 'deprecated': False},\n+ 'lzma-sdk-9.22': {'id': 'LZMA-SDK-9.22', 'deprecated': False},\n+ 'mackerras-3-clause': {'id': 'Mackerras-3-Clause', 'deprecated': False},\n+ 'mackerras-3-clause-acknowledgment': {'id': 'Mackerras-3-Clause-acknowledgment', 'deprecated': False},\n+ 'magaz': {'id': 'magaz', 'deprecated': False},\n+ 'mailprio': {'id': 'mailprio', 'deprecated': False},\n+ 'makeindex': {'id': 'MakeIndex', 'deprecated': False},\n+ 'martin-birgmeier': {'id': 'Martin-Birgmeier', 'deprecated': False},\n+ 'mcphee-slideshow': {'id': 'McPhee-slideshow', 'deprecated': False},\n+ 'metamail': {'id': 'metamail', 'deprecated': False},\n+ 'minpack': {'id': 'Minpack', 'deprecated': False},\n+ 'miros': {'id': 'MirOS', 'deprecated': False},\n+ 'mit': {'id': 'MIT', 'deprecated': False},\n+ 'mit-0': {'id': 'MIT-0', 'deprecated': False},\n+ 'mit-advertising': {'id': 'MIT-advertising', 'deprecated': False},\n+ 'mit-cmu': {'id': 'MIT-CMU', 'deprecated': False},\n+ 'mit-enna': {'id': 'MIT-enna', 'deprecated': False},\n+ 'mit-feh': {'id': 'MIT-feh', 'deprecated': False},\n+ 'mit-festival': {'id': 'MIT-Festival', 'deprecated': False},\n+ 'mit-khronos-old': {'id': 'MIT-Khronos-old', 'deprecated': False},\n+ 'mit-modern-variant': {'id': 'MIT-Modern-Variant', 'deprecated': False},\n+ 'mit-open-group': {'id': 'MIT-open-group', 'deprecated': False},\n+ 'mit-testregex': {'id': 'MIT-testregex', 'deprecated': False},\n+ 'mit-wu': {'id': 'MIT-Wu', 'deprecated': False},\n+ 'mitnfa': {'id': 'MITNFA', 'deprecated': False},\n+ 'mmixware': {'id': 'MMIXware', 'deprecated': False},\n+ 'motosoto': {'id': 'Motosoto', 'deprecated': False},\n+ 'mpeg-ssg': {'id': 'MPEG-SSG', 'deprecated': False},\n+ 'mpi-permissive': {'id': 'mpi-permissive', 'deprecated': False},\n+ 'mpich2': {'id': 'mpich2', 'deprecated': False},\n+ 'mpl-1.0': {'id': 'MPL-1.0', 'deprecated': False},\n+ 'mpl-1.1': {'id': 'MPL-1.1', 'deprecated': False},\n+ 'mpl-2.0': {'id': 'MPL-2.0', 'deprecated': False},\n+ 'mpl-2.0-no-copyleft-exception': {'id': 'MPL-2.0-no-copyleft-exception', 'deprecated': False},\n+ 'mplus': {'id': 'mplus', 'deprecated': False},\n+ 'ms-lpl': {'id': 'MS-LPL', 'deprecated': False},\n+ 'ms-pl': {'id': 'MS-PL', 'deprecated': False},\n+ 'ms-rl': {'id': 'MS-RL', 'deprecated': False},\n+ 'mtll': {'id': 'MTLL', 'deprecated': False},\n+ 'mulanpsl-1.0': {'id': 'MulanPSL-1.0', 'deprecated': False},\n+ 'mulanpsl-2.0': {'id': 'MulanPSL-2.0', 'deprecated': False},\n+ 'multics': {'id': 'Multics', 'deprecated': False},\n+ 'mup': {'id': 'Mup', 'deprecated': False},\n+ 'naist-2003': {'id': 'NAIST-2003', 'deprecated': False},\n+ 'nasa-1.3': {'id': 'NASA-1.3', 'deprecated': False},\n+ 'naumen': {'id': 'Naumen', 'deprecated': False},\n+ 'nbpl-1.0': {'id': 'NBPL-1.0', 'deprecated': False},\n+ 'ncbi-pd': {'id': 'NCBI-PD', 'deprecated': False},\n+ 'ncgl-uk-2.0': {'id': 'NCGL-UK-2.0', 'deprecated': False},\n+ 'ncl': {'id': 'NCL', 'deprecated': False},\n+ 'ncsa': {'id': 'NCSA', 'deprecated': False},\n+ 'net-snmp': {'id': 'Net-SNMP', 'deprecated': True},\n+ 'netcdf': {'id': 'NetCDF', 'deprecated': False},\n+ 'newsletr': {'id': 'Newsletr', 'deprecated': False},\n+ 'ngpl': {'id': 'NGPL', 'deprecated': False},\n+ 'nicta-1.0': {'id': 'NICTA-1.0', 'deprecated': False},\n+ 'nist-pd': {'id': 'NIST-PD', 'deprecated': False},\n+ 'nist-pd-fallback': {'id': 'NIST-PD-fallback', 'deprecated': False},\n+ 'nist-software': {'id': 'NIST-Software', 'deprecated': False},\n+ 'nlod-1.0': {'id': 'NLOD-1.0', 'deprecated': False},\n+ 'nlod-2.0': {'id': 'NLOD-2.0', 'deprecated': False},\n+ 'nlpl': {'id': 'NLPL', 'deprecated': False},\n+ 'nokia': {'id': 'Nokia', 'deprecated': False},\n+ 'nosl': {'id': 'NOSL', 'deprecated': False},\n+ 'noweb': {'id': 'Noweb', 'deprecated': False},\n+ 'npl-1.0': {'id': 'NPL-1.0', 'deprecated': False},\n+ 'npl-1.1': {'id': 'NPL-1.1', 'deprecated': False},\n+ 'nposl-3.0': {'id': 'NPOSL-3.0', 'deprecated': False},\n+ 'nrl': {'id': 'NRL', 'deprecated': False},\n+ 'ntp': {'id': 'NTP', 'deprecated': False},\n+ 'ntp-0': {'id': 'NTP-0', 'deprecated': False},\n+ 'nunit': {'id': 'Nunit', 'deprecated': True},\n+ 'o-uda-1.0': {'id': 'O-UDA-1.0', 'deprecated': False},\n+ 'oar': {'id': 'OAR', 'deprecated': False},\n+ 'occt-pl': {'id': 'OCCT-PL', 'deprecated': False},\n+ 'oclc-2.0': {'id': 'OCLC-2.0', 'deprecated': False},\n+ 'odbl-1.0': {'id': 'ODbL-1.0', 'deprecated': False},\n+ 'odc-by-1.0': {'id': 'ODC-By-1.0', 'deprecated': False},\n+ 'offis': {'id': 'OFFIS', 'deprecated': False},\n+ 'ofl-1.0': {'id': 'OFL-1.0', 'deprecated': False},\n+ 'ofl-1.0-no-rfn': {'id': 'OFL-1.0-no-RFN', 'deprecated': False},\n+ 'ofl-1.0-rfn': {'id': 'OFL-1.0-RFN', 'deprecated': False},\n+ 'ofl-1.1': {'id': 'OFL-1.1', 'deprecated': False},\n+ 'ofl-1.1-no-rfn': {'id': 'OFL-1.1-no-RFN', 'deprecated': False},\n+ 'ofl-1.1-rfn': {'id': 'OFL-1.1-RFN', 'deprecated': False},\n+ 'ogc-1.0': {'id': 'OGC-1.0', 'deprecated': False},\n+ 'ogdl-taiwan-1.0': {'id': 'OGDL-Taiwan-1.0', 'deprecated': False},\n+ 'ogl-canada-2.0': {'id': 'OGL-Canada-2.0', 'deprecated': False},\n+ 'ogl-uk-1.0': {'id': 'OGL-UK-1.0', 'deprecated': False},\n+ 'ogl-uk-2.0': {'id': 'OGL-UK-2.0', 'deprecated': False},\n+ 'ogl-uk-3.0': {'id': 'OGL-UK-3.0', 'deprecated': False},\n+ 'ogtsl': {'id': 'OGTSL', 'deprecated': False},\n+ 'oldap-1.1': {'id': 'OLDAP-1.1', 'deprecated': False},\n+ 'oldap-1.2': {'id': 'OLDAP-1.2', 'deprecated': False},\n+ 'oldap-1.3': {'id': 'OLDAP-1.3', 'deprecated': False},\n+ 'oldap-1.4': {'id': 'OLDAP-1.4', 'deprecated': False},\n+ 'oldap-2.0': {'id': 'OLDAP-2.0', 'deprecated': False},\n+ 'oldap-2.0.1': {'id': 'OLDAP-2.0.1', 'deprecated': False},\n+ 'oldap-2.1': {'id': 'OLDAP-2.1', 'deprecated': False},\n+ 'oldap-2.2': {'id': 'OLDAP-2.2', 'deprecated': False},\n+ 'oldap-2.2.1': {'id': 'OLDAP-2.2.1', 'deprecated': False},\n+ 'oldap-2.2.2': {'id': 'OLDAP-2.2.2', 'deprecated': False},\n+ 'oldap-2.3': {'id': 'OLDAP-2.3', 'deprecated': False},\n+ 'oldap-2.4': {'id': 'OLDAP-2.4', 'deprecated': False},\n+ 'oldap-2.5': {'id': 'OLDAP-2.5', 'deprecated': False},\n+ 'oldap-2.6': {'id': 'OLDAP-2.6', 'deprecated': False},\n+ 'oldap-2.7': {'id': 'OLDAP-2.7', 'deprecated': False},\n+ 'oldap-2.8': {'id': 'OLDAP-2.8', 'deprecated': False},\n+ 'olfl-1.3': {'id': 'OLFL-1.3', 'deprecated': False},\n+ 'oml': {'id': 'OML', 'deprecated': False},\n+ 'openpbs-2.3': {'id': 'OpenPBS-2.3', 'deprecated': False},\n+ 'openssl': {'id': 'OpenSSL', 'deprecated': False},\n+ 'openssl-standalone': {'id': 'OpenSSL-standalone', 'deprecated': False},\n+ 'openvision': {'id': 'OpenVision', 'deprecated': False},\n+ 'opl-1.0': {'id': 'OPL-1.0', 'deprecated': False},\n+ 'opl-uk-3.0': {'id': 'OPL-UK-3.0', 'deprecated': False},\n+ 'opubl-1.0': {'id': 'OPUBL-1.0', 'deprecated': False},\n+ 'oset-pl-2.1': {'id': 'OSET-PL-2.1', 'deprecated': False},\n+ 'osl-1.0': {'id': 'OSL-1.0', 'deprecated': False},\n+ 'osl-1.1': {'id': 'OSL-1.1', 'deprecated': False},\n+ 'osl-2.0': {'id': 'OSL-2.0', 'deprecated': False},\n+ 'osl-2.1': {'id': 'OSL-2.1', 'deprecated': False},\n+ 'osl-3.0': {'id': 'OSL-3.0', 'deprecated': False},\n+ 'padl': {'id': 'PADL', 'deprecated': False},\n+ 'parity-6.0.0': {'id': 'Parity-6.0.0', 'deprecated': False},\n+ 'parity-7.0.0': {'id': 'Parity-7.0.0', 'deprecated': False},\n+ 'pddl-1.0': {'id': 'PDDL-1.0', 'deprecated': False},\n+ 'php-3.0': {'id': 'PHP-3.0', 'deprecated': False},\n+ 'php-3.01': {'id': 'PHP-3.01', 'deprecated': False},\n+ 'pixar': {'id': 'Pixar', 'deprecated': False},\n+ 'pkgconf': {'id': 'pkgconf', 'deprecated': False},\n+ 'plexus': {'id': 'Plexus', 'deprecated': False},\n+ 'pnmstitch': {'id': 'pnmstitch', 'deprecated': False},\n+ 'polyform-noncommercial-1.0.0': {'id': 'PolyForm-Noncommercial-1.0.0', 'deprecated': False},\n+ 'polyform-small-business-1.0.0': {'id': 'PolyForm-Small-Business-1.0.0', 'deprecated': False},\n+ 'postgresql': {'id': 'PostgreSQL', 'deprecated': False},\n+ 'ppl': {'id': 'PPL', 'deprecated': False},\n+ 'psf-2.0': {'id': 'PSF-2.0', 'deprecated': False},\n+ 'psfrag': {'id': 'psfrag', 'deprecated': False},\n+ 'psutils': {'id': 'psutils', 'deprecated': False},\n+ 'python-2.0': {'id': 'Python-2.0', 'deprecated': False},\n+ 'python-2.0.1': {'id': 'Python-2.0.1', 'deprecated': False},\n+ 'python-ldap': {'id': 'python-ldap', 'deprecated': False},\n+ 'qhull': {'id': 'Qhull', 'deprecated': False},\n+ 'qpl-1.0': {'id': 'QPL-1.0', 'deprecated': False},\n+ 'qpl-1.0-inria-2004': {'id': 'QPL-1.0-INRIA-2004', 'deprecated': False},\n+ 'radvd': {'id': 'radvd', 'deprecated': False},\n+ 'rdisc': {'id': 'Rdisc', 'deprecated': False},\n+ 'rhecos-1.1': {'id': 'RHeCos-1.1', 'deprecated': False},\n+ 'rpl-1.1': {'id': 'RPL-1.1', 'deprecated': False},\n+ 'rpl-1.5': {'id': 'RPL-1.5', 'deprecated': False},\n+ 'rpsl-1.0': {'id': 'RPSL-1.0', 'deprecated': False},\n+ 'rsa-md': {'id': 'RSA-MD', 'deprecated': False},\n+ 'rscpl': {'id': 'RSCPL', 'deprecated': False},\n+ 'ruby': {'id': 'Ruby', 'deprecated': False},\n+ 'ruby-pty': {'id': 'Ruby-pty', 'deprecated': False},\n+ 'sax-pd': {'id': 'SAX-PD', 'deprecated': False},\n+ 'sax-pd-2.0': {'id': 'SAX-PD-2.0', 'deprecated': False},\n+ 'saxpath': {'id': 'Saxpath', 'deprecated': False},\n+ 'scea': {'id': 'SCEA', 'deprecated': False},\n+ 'schemereport': {'id': 'SchemeReport', 'deprecated': False},\n+ 'sendmail': {'id': 'Sendmail', 'deprecated': False},\n+ 'sendmail-8.23': {'id': 'Sendmail-8.23', 'deprecated': False},\n+ 'sgi-b-1.0': {'id': 'SGI-B-1.0', 'deprecated': False},\n+ 'sgi-b-1.1': {'id': 'SGI-B-1.1', 'deprecated': False},\n+ 'sgi-b-2.0': {'id': 'SGI-B-2.0', 'deprecated': False},\n+ 'sgi-opengl': {'id': 'SGI-OpenGL', 'deprecated': False},\n+ 'sgp4': {'id': 'SGP4', 'deprecated': False},\n+ 'shl-0.5': {'id': 'SHL-0.5', 'deprecated': False},\n+ 'shl-0.51': {'id': 'SHL-0.51', 'deprecated': False},\n+ 'simpl-2.0': {'id': 'SimPL-2.0', 'deprecated': False},\n+ 'sissl': {'id': 'SISSL', 'deprecated': False},\n+ 'sissl-1.2': {'id': 'SISSL-1.2', 'deprecated': False},\n+ 'sl': {'id': 'SL', 'deprecated': False},\n+ 'sleepycat': {'id': 'Sleepycat', 'deprecated': False},\n+ 'smlnj': {'id': 'SMLNJ', 'deprecated': False},\n+ 'smppl': {'id': 'SMPPL', 'deprecated': False},\n+ 'snia': {'id': 'SNIA', 'deprecated': False},\n+ 'snprintf': {'id': 'snprintf', 'deprecated': False},\n+ 'softsurfer': {'id': 'softSurfer', 'deprecated': False},\n+ 'soundex': {'id': 'Soundex', 'deprecated': False},\n+ 'spencer-86': {'id': 'Spencer-86', 'deprecated': False},\n+ 'spencer-94': {'id': 'Spencer-94', 'deprecated': False},\n+ 'spencer-99': {'id': 'Spencer-99', 'deprecated': False},\n+ 'spl-1.0': {'id': 'SPL-1.0', 'deprecated': False},\n+ 'ssh-keyscan': {'id': 'ssh-keyscan', 'deprecated': False},\n+ 'ssh-openssh': {'id': 'SSH-OpenSSH', 'deprecated': False},\n+ 'ssh-short': {'id': 'SSH-short', 'deprecated': False},\n+ 'ssleay-standalone': {'id': 'SSLeay-standalone', 'deprecated': False},\n+ 'sspl-1.0': {'id': 'SSPL-1.0', 'deprecated': False},\n+ 'standardml-nj': {'id': 'StandardML-NJ', 'deprecated': True},\n+ 'sugarcrm-1.1.3': {'id': 'SugarCRM-1.1.3', 'deprecated': False},\n+ 'sun-ppp': {'id': 'Sun-PPP', 'deprecated': False},\n+ 'sun-ppp-2000': {'id': 'Sun-PPP-2000', 'deprecated': False},\n+ 'sunpro': {'id': 'SunPro', 'deprecated': False},\n+ 'swl': {'id': 'SWL', 'deprecated': False},\n+ 'swrule': {'id': 'swrule', 'deprecated': False},\n+ 'symlinks': {'id': 'Symlinks', 'deprecated': False},\n+ 'tapr-ohl-1.0': {'id': 'TAPR-OHL-1.0', 'deprecated': False},\n+ 'tcl': {'id': 'TCL', 'deprecated': False},\n+ 'tcp-wrappers': {'id': 'TCP-wrappers', 'deprecated': False},\n+ 'termreadkey': {'id': 'TermReadKey', 'deprecated': False},\n+ 'tgppl-1.0': {'id': 'TGPPL-1.0', 'deprecated': False},\n+ 'threeparttable': {'id': 'threeparttable', 'deprecated': False},\n+ 'tmate': {'id': 'TMate', 'deprecated': False},\n+ 'torque-1.1': {'id': 'TORQUE-1.1', 'deprecated': False},\n+ 'tosl': {'id': 'TOSL', 'deprecated': False},\n+ 'tpdl': {'id': 'TPDL', 'deprecated': False},\n+ 'tpl-1.0': {'id': 'TPL-1.0', 'deprecated': False},\n+ 'ttwl': {'id': 'TTWL', 'deprecated': False},\n+ 'ttyp0': {'id': 'TTYP0', 'deprecated': False},\n+ 'tu-berlin-1.0': {'id': 'TU-Berlin-1.0', 'deprecated': False},\n+ 'tu-berlin-2.0': {'id': 'TU-Berlin-2.0', 'deprecated': False},\n+ 'ubuntu-font-1.0': {'id': 'Ubuntu-font-1.0', 'deprecated': False},\n+ 'ucar': {'id': 'UCAR', 'deprecated': False},\n+ 'ucl-1.0': {'id': 'UCL-1.0', 'deprecated': False},\n+ 'ulem': {'id': 'ulem', 'deprecated': False},\n+ 'umich-merit': {'id': 'UMich-Merit', 'deprecated': False},\n+ 'unicode-3.0': {'id': 'Unicode-3.0', 'deprecated': False},\n+ 'unicode-dfs-2015': {'id': 'Unicode-DFS-2015', 'deprecated': False},\n+ 'unicode-dfs-2016': {'id': 'Unicode-DFS-2016', 'deprecated': False},\n+ 'unicode-tou': {'id': 'Unicode-TOU', 'deprecated': False},\n+ 'unixcrypt': {'id': 'UnixCrypt', 'deprecated': False},\n+ 'unlicense': {'id': 'Unlicense', 'deprecated': False},\n+ 'upl-1.0': {'id': 'UPL-1.0', 'deprecated': False},\n+ 'urt-rle': {'id': 'URT-RLE', 'deprecated': False},\n+ 'vim': {'id': 'Vim', 'deprecated': False},\n+ 'vostrom': {'id': 'VOSTROM', 'deprecated': False},\n+ 'vsl-1.0': {'id': 'VSL-1.0', 'deprecated': False},\n+ 'w3c': {'id': 'W3C', 'deprecated': False},\n+ 'w3c-19980720': {'id': 'W3C-19980720', 'deprecated': False},\n+ 'w3c-20150513': {'id': 'W3C-20150513', 'deprecated': False},\n+ 'w3m': {'id': 'w3m', 'deprecated': False},\n+ 'watcom-1.0': {'id': 'Watcom-1.0', 'deprecated': False},\n+ 'widget-workshop': {'id': 'Widget-Workshop', 'deprecated': False},\n+ 'wsuipa': {'id': 'Wsuipa', 'deprecated': False},\n+ 'wtfpl': {'id': 'WTFPL', 'deprecated': False},\n+ 'wxwindows': {'id': 'wxWindows', 'deprecated': True},\n+ 'x11': {'id': 'X11', 'deprecated': False},\n+ 'x11-distribute-modifications-variant': {'id': 'X11-distribute-modifications-variant', 'deprecated': False},\n+ 'x11-swapped': {'id': 'X11-swapped', 'deprecated': False},\n+ 'xdebug-1.03': {'id': 'Xdebug-1.03', 'deprecated': False},\n+ 'xerox': {'id': 'Xerox', 'deprecated': False},\n+ 'xfig': {'id': 'Xfig', 'deprecated': False},\n+ 'xfree86-1.1': {'id': 'XFree86-1.1', 'deprecated': False},\n+ 'xinetd': {'id': 'xinetd', 'deprecated': False},\n+ 'xkeyboard-config-zinoviev': {'id': 'xkeyboard-config-Zinoviev', 'deprecated': False},\n+ 'xlock': {'id': 'xlock', 'deprecated': False},\n+ 'xnet': {'id': 'Xnet', 'deprecated': False},\n+ 'xpp': {'id': 'xpp', 'deprecated': False},\n+ 'xskat': {'id': 'XSkat', 'deprecated': False},\n+ 'xzoom': {'id': 'xzoom', 'deprecated': False},\n+ 'ypl-1.0': {'id': 'YPL-1.0', 'deprecated': False},\n+ 'ypl-1.1': {'id': 'YPL-1.1', 'deprecated': False},\n+ 'zed': {'id': 'Zed', 'deprecated': False},\n+ 'zeeff': {'id': 'Zeeff', 'deprecated': False},\n+ 'zend-2.0': {'id': 'Zend-2.0', 'deprecated': False},\n+ 'zimbra-1.3': {'id': 'Zimbra-1.3', 'deprecated': False},\n+ 'zimbra-1.4': {'id': 'Zimbra-1.4', 'deprecated': False},\n+ 'zlib': {'id': 'Zlib', 'deprecated': False},\n+ 'zlib-acknowledgement': {'id': 'zlib-acknowledgement', 'deprecated': False},\n+ 'zpl-1.1': {'id': 'ZPL-1.1', 'deprecated': False},\n+ 'zpl-2.0': {'id': 'ZPL-2.0', 'deprecated': False},\n+ 'zpl-2.1': {'id': 'ZPL-2.1', 'deprecated': False},\n+}\n+\n+EXCEPTIONS: dict[str, SPDXException] = {\n+ '389-exception': {'id': '389-exception', 'deprecated': False},\n+ 'asterisk-exception': {'id': 'Asterisk-exception', 'deprecated': False},\n+ 'asterisk-linking-protocols-exception': {'id': 'Asterisk-linking-protocols-exception', 'deprecated': False},\n+ 'autoconf-exception-2.0': {'id': 'Autoconf-exception-2.0', 'deprecated': False},\n+ 'autoconf-exception-3.0': {'id': 'Autoconf-exception-3.0', 'deprecated': False},\n+ 'autoconf-exception-generic': {'id': 'Autoconf-exception-generic', 'deprecated': False},\n+ 'autoconf-exception-generic-3.0': {'id': 'Autoconf-exception-generic-3.0', 'deprecated': False},\n+ 'autoconf-exception-macro': {'id': 'Autoconf-exception-macro', 'deprecated': False},\n+ 'bison-exception-1.24': {'id': 'Bison-exception-1.24', 'deprecated': False},\n+ 'bison-exception-2.2': {'id': 'Bison-exception-2.2', 'deprecated': False},\n+ 'bootloader-exception': {'id': 'Bootloader-exception', 'deprecated': False},\n+ 'classpath-exception-2.0': {'id': 'Classpath-exception-2.0', 'deprecated': False},\n+ 'clisp-exception-2.0': {'id': 'CLISP-exception-2.0', 'deprecated': False},\n+ 'cryptsetup-openssl-exception': {'id': 'cryptsetup-OpenSSL-exception', 'deprecated': False},\n+ 'digirule-foss-exception': {'id': 'DigiRule-FOSS-exception', 'deprecated': False},\n+ 'ecos-exception-2.0': {'id': 'eCos-exception-2.0', 'deprecated': False},\n+ 'erlang-otp-linking-exception': {'id': 'erlang-otp-linking-exception', 'deprecated': False},\n+ 'fawkes-runtime-exception': {'id': 'Fawkes-Runtime-exception', 'deprecated': False},\n+ 'fltk-exception': {'id': 'FLTK-exception', 'deprecated': False},\n+ 'fmt-exception': {'id': 'fmt-exception', 'deprecated': False},\n+ 'font-exception-2.0': {'id': 'Font-exception-2.0', 'deprecated': False},\n+ 'freertos-exception-2.0': {'id': 'freertos-exception-2.0', 'deprecated': False},\n+ 'gcc-exception-2.0': {'id': 'GCC-exception-2.0', 'deprecated': False},\n+ 'gcc-exception-2.0-note': {'id': 'GCC-exception-2.0-note', 'deprecated': False},\n+ 'gcc-exception-3.1': {'id': 'GCC-exception-3.1', 'deprecated': False},\n+ 'gmsh-exception': {'id': 'Gmsh-exception', 'deprecated': False},\n+ 'gnat-exception': {'id': 'GNAT-exception', 'deprecated': False},\n+ 'gnome-examples-exception': {'id': 'GNOME-examples-exception', 'deprecated': False},\n+ 'gnu-compiler-exception': {'id': 'GNU-compiler-exception', 'deprecated': False},\n+ 'gnu-javamail-exception': {'id': 'gnu-javamail-exception', 'deprecated': False},\n+ 'gpl-3.0-interface-exception': {'id': 'GPL-3.0-interface-exception', 'deprecated': False},\n+ 'gpl-3.0-linking-exception': {'id': 'GPL-3.0-linking-exception', 'deprecated': False},\n+ 'gpl-3.0-linking-source-exception': {'id': 'GPL-3.0-linking-source-exception', 'deprecated': False},\n+ 'gpl-cc-1.0': {'id': 'GPL-CC-1.0', 'deprecated': False},\n+ 'gstreamer-exception-2005': {'id': 'GStreamer-exception-2005', 'deprecated': False},\n+ 'gstreamer-exception-2008': {'id': 'GStreamer-exception-2008', 'deprecated': False},\n+ 'i2p-gpl-java-exception': {'id': 'i2p-gpl-java-exception', 'deprecated': False},\n+ 'kicad-libraries-exception': {'id': 'KiCad-libraries-exception', 'deprecated': False},\n+ 'lgpl-3.0-linking-exception': {'id': 'LGPL-3.0-linking-exception', 'deprecated': False},\n+ 'libpri-openh323-exception': {'id': 'libpri-OpenH323-exception', 'deprecated': False},\n+ 'libtool-exception': {'id': 'Libtool-exception', 'deprecated': False},\n+ 'linux-syscall-note': {'id': 'Linux-syscall-note', 'deprecated': False},\n+ 'llgpl': {'id': 'LLGPL', 'deprecated': False},\n+ 'llvm-exception': {'id': 'LLVM-exception', 'deprecated': False},\n+ 'lzma-exception': {'id': 'LZMA-exception', 'deprecated': False},\n+ 'mif-exception': {'id': 'mif-exception', 'deprecated': False},\n+ 'nokia-qt-exception-1.1': {'id': 'Nokia-Qt-exception-1.1', 'deprecated': True},\n+ 'ocaml-lgpl-linking-exception': {'id': 'OCaml-LGPL-linking-exception', 'deprecated': False},\n+ 'occt-exception-1.0': {'id': 'OCCT-exception-1.0', 'deprecated': False},\n+ 'openjdk-assembly-exception-1.0': {'id': 'OpenJDK-assembly-exception-1.0', 'deprecated': False},\n+ 'openvpn-openssl-exception': {'id': 'openvpn-openssl-exception', 'deprecated': False},\n+ 'pcre2-exception': {'id': 'PCRE2-exception', 'deprecated': False},\n+ 'ps-or-pdf-font-exception-20170817': {'id': 'PS-or-PDF-font-exception-20170817', 'deprecated': False},\n+ 'qpl-1.0-inria-2004-exception': {'id': 'QPL-1.0-INRIA-2004-exception', 'deprecated': False},\n+ 'qt-gpl-exception-1.0': {'id': 'Qt-GPL-exception-1.0', 'deprecated': False},\n+ 'qt-lgpl-exception-1.1': {'id': 'Qt-LGPL-exception-1.1', 'deprecated': False},\n+ 'qwt-exception-1.0': {'id': 'Qwt-exception-1.0', 'deprecated': False},\n+ 'romic-exception': {'id': 'romic-exception', 'deprecated': False},\n+ 'rrdtool-floss-exception-2.0': {'id': 'RRDtool-FLOSS-exception-2.0', 'deprecated': False},\n+ 'sane-exception': {'id': 'SANE-exception', 'deprecated': False},\n+ 'shl-2.0': {'id': 'SHL-2.0', 'deprecated': False},\n+ 'shl-2.1': {'id': 'SHL-2.1', 'deprecated': False},\n+ 'stunnel-exception': {'id': 'stunnel-exception', 'deprecated': False},\n+ 'swi-exception': {'id': 'SWI-exception', 'deprecated': False},\n+ 'swift-exception': {'id': 'Swift-exception', 'deprecated': False},\n+ 'texinfo-exception': {'id': 'Texinfo-exception', 'deprecated': False},\n+ 'u-boot-exception-2.0': {'id': 'u-boot-exception-2.0', 'deprecated': False},\n+ 'ubdl-exception': {'id': 'UBDL-exception', 'deprecated': False},\n+ 'universal-foss-exception-1.0': {'id': 'Universal-FOSS-exception-1.0', 'deprecated': False},\n+ 'vsftpd-openssl-exception': {'id': 'vsftpd-openssl-exception', 'deprecated': False},\n+ 'wxwindows-exception-3.1': {'id': 'WxWindows-exception-3.1', 'deprecated': False},\n+ 'x11vnc-openssl-exception': {'id': 'x11vnc-openssl-exception', 'deprecated': False},\n+}"}, {"sha": "fb7f49cf8cd43ffae71e3e8d15174d7536f9da02", "filename": "lib/packaging/markers.py", "status": "modified", "additions": 15, "deletions": 9, "changes": 24, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpackaging%2Fmarkers.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpackaging%2Fmarkers.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpackaging%2Fmarkers.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -18,9 +18,9 @@\n \n __all__ = [\n \"InvalidMarker\",\n+ \"Marker\",\n \"UndefinedComparison\",\n \"UndefinedEnvironmentName\",\n- \"Marker\",\n \"default_environment\",\n ]\n \n@@ -232,7 +232,7 @@ def _evaluate_markers(markers: MarkerList, environment: dict[str, str]) -> bool:\n \n \n def format_full_version(info: sys._version_info) -> str:\n- version = \"{0.major}.{0.minor}.{0.micro}\".format(info)\n+ version = f\"{info.major}.{info.minor}.{info.micro}\"\n kind = info.releaselevel\n if kind != \"final\":\n version += kind[0] + str(info.serial)\n@@ -309,17 +309,23 @@ def evaluate(self, environment: dict[str, str] | None = None) -> bool:\n \"\"\"\n current_environment = cast(\"dict[str, str]\", default_environment())\n current_environment[\"extra\"] = \"\"\n- # Work around platform.python_version() returning something that is not PEP 440\n- # compliant for non-tagged Python builds. We preserve default_environment()'s\n- # behavior of returning platform.python_version() verbatim, and leave it to the\n- # caller to provide a syntactically valid version if they want to override it.\n- if current_environment[\"python_full_version\"].endswith(\"+\"):\n- current_environment[\"python_full_version\"] += \"local\"\n if environment is not None:\n current_environment.update(environment)\n # The API used to allow setting extra to None. We need to handle this\n # case for backwards compatibility.\n if current_environment[\"extra\"] is None:\n current_environment[\"extra\"] = \"\"\n \n- return _evaluate_markers(self._markers, current_environment)\n+ return _evaluate_markers(\n+ self._markers, _repair_python_full_version(current_environment)\n+ )\n+\n+\n+def _repair_python_full_version(env: dict[str, str]) -> dict[str, str]:\n+ \"\"\"\n+ Work around platform.python_version() returning something that is not PEP 440\n+ compliant for non-tagged Python builds.\n+ \"\"\"\n+ if env[\"python_full_version\"].endswith(\"+\"):\n+ env[\"python_full_version\"] += \"local\"\n+ return env"}, {"sha": "721f411cfc44f6d24c13112e4246b5ad776a5e0b", "filename": "lib/packaging/metadata.py", "status": "modified", "additions": 83, "deletions": 24, "changes": 107, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpackaging%2Fmetadata.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpackaging%2Fmetadata.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpackaging%2Fmetadata.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -5,6 +5,8 @@\n import email.message\n import email.parser\n import email.policy\n+import pathlib\n+import sys\n import typing\n from typing import (\n Any,\n@@ -15,15 +17,16 @@\n cast,\n )\n \n-from . import requirements, specifiers, utils\n+from . import licenses, requirements, specifiers, utils\n from . import version as version_module\n+from .licenses import NormalizedLicenseExpression\n \n T = typing.TypeVar(\"T\")\n \n \n-try:\n- ExceptionGroup\n-except NameError: # pragma: no cover\n+if sys.version_info >= (3, 11): # pragma: no cover\n+ ExceptionGroup = ExceptionGroup\n+else: # pragma: no cover\n \n class ExceptionGroup(Exception):\n \"\"\"A minimal implementation of :external:exc:`ExceptionGroup` from Python 3.11.\n@@ -42,9 +45,6 @@ def __init__(self, message: str, exceptions: list[Exception]) -> None:\n def __repr__(self) -> str:\n return f\"{self.__class__.__name__}({self.message!r}, {self.exceptions!r})\"\n \n-else: # pragma: no cover\n- ExceptionGroup = ExceptionGroup\n-\n \n class InvalidMetadata(ValueError):\n \"\"\"A metadata field contains invalid data.\"\"\"\n@@ -128,6 +128,10 @@ class RawMetadata(TypedDict, total=False):\n # No new fields were added in PEP 685, just some edge case were\n # tightened up to provide better interoptability.\n \n+ # Metadata 2.4 - PEP 639\n+ license_expression: str\n+ license_files: list[str]\n+\n \n _STRING_FIELDS = {\n \"author\",\n@@ -137,6 +141,7 @@ class RawMetadata(TypedDict, total=False):\n \"download_url\",\n \"home_page\",\n \"license\",\n+ \"license_expression\",\n \"maintainer\",\n \"maintainer_email\",\n \"metadata_version\",\n@@ -149,6 +154,7 @@ class RawMetadata(TypedDict, total=False):\n _LIST_FIELDS = {\n \"classifiers\",\n \"dynamic\",\n+ \"license_files\",\n \"obsoletes\",\n \"obsoletes_dist\",\n \"platforms\",\n@@ -167,7 +173,7 @@ class RawMetadata(TypedDict, total=False):\n \n \n def _parse_keywords(data: str) -> list[str]:\n- \"\"\"Split a string of comma-separate keyboards into a list of keywords.\"\"\"\n+ \"\"\"Split a string of comma-separated keywords into a list of keywords.\"\"\"\n return [k.strip() for k in data.split(\",\")]\n \n \n@@ -216,16 +222,18 @@ def _get_payload(msg: email.message.Message, source: bytes | str) -> str:\n # If our source is a str, then our caller has managed encodings for us,\n # and we don't need to deal with it.\n if isinstance(source, str):\n- payload: str = msg.get_payload()\n+ payload = msg.get_payload()\n+ assert isinstance(payload, str)\n return payload\n # If our source is a bytes, then we're managing the encoding and we need\n # to deal with it.\n else:\n- bpayload: bytes = msg.get_payload(decode=True)\n+ bpayload = msg.get_payload(decode=True)\n+ assert isinstance(bpayload, bytes)\n try:\n return bpayload.decode(\"utf8\", \"strict\")\n- except UnicodeDecodeError:\n- raise ValueError(\"payload in an invalid encoding\")\n+ except UnicodeDecodeError as exc:\n+ raise ValueError(\"payload in an invalid encoding\") from exc\n \n \n # The various parse_FORMAT functions here are intended to be as lenient as\n@@ -251,6 +259,8 @@ def _get_payload(msg: email.message.Message, source: bytes | str) -> str:\n \"home-page\": \"home_page\",\n \"keywords\": \"keywords\",\n \"license\": \"license\",\n+ \"license-expression\": \"license_expression\",\n+ \"license-file\": \"license_files\",\n \"maintainer\": \"maintainer\",\n \"maintainer-email\": \"maintainer_email\",\n \"metadata-version\": \"metadata_version\",\n@@ -426,7 +436,7 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:\n payload = _get_payload(parsed, data)\n except ValueError:\n unparsed.setdefault(\"description\", []).append(\n- parsed.get_payload(decode=isinstance(data, bytes))\n+ parsed.get_payload(decode=isinstance(data, bytes)) # type: ignore[call-overload]\n )\n else:\n if payload:\n@@ -453,8 +463,8 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:\n \n \n # Keep the two values in sync.\n-_VALID_METADATA_VERSIONS = [\"1.0\", \"1.1\", \"1.2\", \"2.1\", \"2.2\", \"2.3\"]\n-_MetadataVersion = Literal[\"1.0\", \"1.1\", \"1.2\", \"2.1\", \"2.2\", \"2.3\"]\n+_VALID_METADATA_VERSIONS = [\"1.0\", \"1.1\", \"1.2\", \"2.1\", \"2.2\", \"2.3\", \"2.4\"]\n+_MetadataVersion = Literal[\"1.0\", \"1.1\", \"1.2\", \"2.1\", \"2.2\", \"2.3\", \"2.4\"]\n \n _REQUIRED_ATTRS = frozenset([\"metadata_version\", \"name\", \"version\"])\n \n@@ -535,7 +545,7 @@ def _process_name(self, value: str) -> str:\n except utils.InvalidName as exc:\n raise self._invalid_metadata(\n f\"{value!r} is invalid for {{field}}\", cause=exc\n- )\n+ ) from exc\n else:\n return value\n \n@@ -547,7 +557,7 @@ def _process_version(self, value: str) -> version_module.Version:\n except version_module.InvalidVersion as exc:\n raise self._invalid_metadata(\n f\"{value!r} is invalid for {{field}}\", cause=exc\n- )\n+ ) from exc\n \n def _process_summary(self, value: str) -> str:\n \"\"\"Check the field contains no newlines.\"\"\"\n@@ -591,10 +601,12 @@ def _process_dynamic(self, value: list[str]) -> list[str]:\n for dynamic_field in map(str.lower, value):\n if dynamic_field in {\"name\", \"version\", \"metadata-version\"}:\n raise self._invalid_metadata(\n- f\"{value!r} is not allowed as a dynamic field\"\n+ f\"{dynamic_field!r} is not allowed as a dynamic field\"\n )\n elif dynamic_field not in _EMAIL_TO_RAW_MAPPING:\n- raise self._invalid_metadata(f\"{value!r} is not a valid dynamic field\")\n+ raise self._invalid_metadata(\n+ f\"{dynamic_field!r} is not a valid dynamic field\"\n+ )\n return list(map(str.lower, value))\n \n def _process_provides_extra(\n@@ -608,7 +620,7 @@ def _process_provides_extra(\n except utils.InvalidName as exc:\n raise self._invalid_metadata(\n f\"{name!r} is invalid for {{field}}\", cause=exc\n- )\n+ ) from exc\n else:\n return normalized_names\n \n@@ -618,7 +630,7 @@ def _process_requires_python(self, value: str) -> specifiers.SpecifierSet:\n except specifiers.InvalidSpecifier as exc:\n raise self._invalid_metadata(\n f\"{value!r} is invalid for {{field}}\", cause=exc\n- )\n+ ) from exc\n \n def _process_requires_dist(\n self,\n@@ -629,10 +641,49 @@ def _process_requires_dist(\n for req in value:\n reqs.append(requirements.Requirement(req))\n except requirements.InvalidRequirement as exc:\n- raise self._invalid_metadata(f\"{req!r} is invalid for {{field}}\", cause=exc)\n+ raise self._invalid_metadata(\n+ f\"{req!r} is invalid for {{field}}\", cause=exc\n+ ) from exc\n else:\n return reqs\n \n+ def _process_license_expression(\n+ self, value: str\n+ ) -> NormalizedLicenseExpression | None:\n+ try:\n+ return licenses.canonicalize_license_expression(value)\n+ except ValueError as exc:\n+ raise self._invalid_metadata(\n+ f\"{value!r} is invalid for {{field}}\", cause=exc\n+ ) from exc\n+\n+ def _process_license_files(self, value: list[str]) -> list[str]:\n+ paths = []\n+ for path in value:\n+ if \"..\" in path:\n+ raise self._invalid_metadata(\n+ f\"{path!r} is invalid for {{field}}, \"\n+ \"parent directory indicators are not allowed\"\n+ )\n+ if \"*\" in path:\n+ raise self._invalid_metadata(\n+ f\"{path!r} is invalid for {{field}}, paths must be resolved\"\n+ )\n+ if (\n+ pathlib.PurePosixPath(path).is_absolute()\n+ or pathlib.PureWindowsPath(path).is_absolute()\n+ ):\n+ raise self._invalid_metadata(\n+ f\"{path!r} is invalid for {{field}}, paths must be relative\"\n+ )\n+ if pathlib.PureWindowsPath(path).as_posix() != path:\n+ raise self._invalid_metadata(\n+ f\"{path!r} is invalid for {{field}}, \"\n+ \"paths must use '/' delimiter\"\n+ )\n+ paths.append(path)\n+ return paths\n+\n \n class Metadata:\n \"\"\"Representation of distribution metadata.\n@@ -688,8 +739,8 @@ def from_raw(cls, data: RawMetadata, *, validate: bool = True) -> Metadata:\n field = _RAW_TO_EMAIL_MAPPING[key]\n exc = InvalidMetadata(\n field,\n- \"{field} introduced in metadata version \"\n- \"{field_metadata_version}, not {metadata_version}\",\n+ f\"{field} introduced in metadata version \"\n+ f\"{field_metadata_version}, not {metadata_version}\",\n )\n exceptions.append(exc)\n continue\n@@ -733,6 +784,8 @@ def from_email(cls, data: bytes | str, *, validate: bool = True) -> Metadata:\n metadata_version: _Validator[_MetadataVersion] = _Validator()\n \"\"\":external:ref:`core-metadata-metadata-version`\n (required; validated to be a valid metadata version)\"\"\"\n+ # `name` is not normalized/typed to NormalizedName so as to provide access to\n+ # the original/raw name.\n name: _Validator[str] = _Validator()\n \"\"\":external:ref:`core-metadata-name`\n (required; validated using :func:`~packaging.utils.canonicalize_name` and its\n@@ -770,6 +823,12 @@ def from_email(cls, data: bytes | str, *, validate: bool = True) -> Metadata:\n \"\"\":external:ref:`core-metadata-maintainer-email`\"\"\"\n license: _Validator[str | None] = _Validator()\n \"\"\":external:ref:`core-metadata-license`\"\"\"\n+ license_expression: _Validator[NormalizedLicenseExpression | None] = _Validator(\n+ added=\"2.4\"\n+ )\n+ \"\"\":external:ref:`core-metadata-license-expression`\"\"\"\n+ license_files: _Validator[list[str] | None] = _Validator(added=\"2.4\")\n+ \"\"\":external:ref:`core-metadata-license-file`\"\"\"\n classifiers: _Validator[list[str] | None] = _Validator(added=\"1.1\")\n \"\"\":external:ref:`core-metadata-classifier`\"\"\"\n requires_dist: _Validator[list[requirements.Requirement] | None] = _Validator("}, {"sha": "b30926af8bf4f47efe98eea44d5ded4cb6f7e07d", "filename": "lib/packaging/specifiers.py", "status": "modified", "additions": 19, "deletions": 8, "changes": 27, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpackaging%2Fspecifiers.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpackaging%2Fspecifiers.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpackaging%2Fspecifiers.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -234,7 +234,7 @@ def __init__(self, spec: str = \"\", prereleases: bool | None = None) -> None:\n \"\"\"\n match = self._regex.search(spec)\n if not match:\n- raise InvalidSpecifier(f\"Invalid specifier: '{spec}'\")\n+ raise InvalidSpecifier(f\"Invalid specifier: {spec!r}\")\n \n self._spec: tuple[str, str] = (\n match.group(\"operator\").strip(),\n@@ -256,7 +256,7 @@ def prereleases(self) -> bool:\n # operators, and if they are if they are including an explicit\n # prerelease.\n operator, version = self._spec\n- if operator in [\"==\", \">=\", \"<=\", \"~=\", \"===\"]:\n+ if operator in [\"==\", \">=\", \"<=\", \"~=\", \"===\", \">\", \"<\"]:\n # The == specifier can include a trailing .*, if it does we\n # want to remove before parsing.\n if operator == \"==\" and version.endswith(\".*\"):\n@@ -694,12 +694,18 @@ class SpecifierSet(BaseSpecifier):\n specifiers (``>=3.0,!=3.1``), or no specifier at all.\n \"\"\"\n \n- def __init__(self, specifiers: str = \"\", prereleases: bool | None = None) -> None:\n+ def __init__(\n+ self,\n+ specifiers: str | Iterable[Specifier] = \"\",\n+ prereleases: bool | None = None,\n+ ) -> None:\n \"\"\"Initialize a SpecifierSet instance.\n \n :param specifiers:\n The string representation of a specifier or a comma-separated list of\n specifiers which will be parsed and normalized before use.\n+ May also be an iterable of ``Specifier`` instances, which will be used\n+ as is.\n :param prereleases:\n This tells the SpecifierSet if it should accept prerelease versions if\n applicable or not. The default of ``None`` will autodetect it from the\n@@ -710,12 +716,17 @@ def __init__(self, specifiers: str = \"\", prereleases: bool | None = None) -> Non\n raised.\n \"\"\"\n \n- # Split on `,` to break each individual specifier into it's own item, and\n- # strip each item to remove leading/trailing whitespace.\n- split_specifiers = [s.strip() for s in specifiers.split(\",\") if s.strip()]\n+ if isinstance(specifiers, str):\n+ # Split on `,` to break each individual specifier into its own item, and\n+ # strip each item to remove leading/trailing whitespace.\n+ split_specifiers = [s.strip() for s in specifiers.split(\",\") if s.strip()]\n \n- # Make each individual specifier a Specifier and save in a frozen set for later.\n- self._specs = frozenset(map(Specifier, split_specifiers))\n+ # Make each individual specifier a Specifier and save in a frozen set\n+ # for later.\n+ self._specs = frozenset(map(Specifier, split_specifiers))\n+ else:\n+ # Save the supplied specifiers in a frozen set.\n+ self._specs = frozenset(specifiers)\n \n # Store our prereleases value so we can use it later to determine if\n # we accept prereleases or not."}, {"sha": "f5903402abb5a0aed37bb23914f678ef7e34a554", "filename": "lib/packaging/tags.py", "status": "modified", "additions": 78, "deletions": 29, "changes": 107, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpackaging%2Ftags.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpackaging%2Ftags.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpackaging%2Ftags.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -25,7 +25,7 @@\n logger = logging.getLogger(__name__)\n \n PythonVersion = Sequence[int]\n-MacVersion = Tuple[int, int]\n+AppleVersion = Tuple[int, int]\n \n INTERPRETER_SHORT_NAMES: dict[str, str] = {\n \"python\": \"py\", # Generic.\n@@ -47,7 +47,7 @@ class Tag:\n is also supported.\n \"\"\"\n \n- __slots__ = [\"_interpreter\", \"_abi\", \"_platform\", \"_hash\"]\n+ __slots__ = [\"_abi\", \"_hash\", \"_interpreter\", \"_platform\"]\n \n def __init__(self, interpreter: str, abi: str, platform: str) -> None:\n self._interpreter = interpreter.lower()\n@@ -235,9 +235,8 @@ def cpython_tags(\n if use_abi3:\n for minor_version in range(python_version[1] - 1, 1, -1):\n for platform_ in platforms:\n- interpreter = \"cp{version}\".format(\n- version=_version_nodot((python_version[0], minor_version))\n- )\n+ version = _version_nodot((python_version[0], minor_version))\n+ interpreter = f\"cp{version}\"\n yield Tag(interpreter, \"abi3\", platform_)\n \n \n@@ -363,7 +362,7 @@ def _mac_arch(arch: str, is_32bit: bool = _32_BIT_INTERPRETER) -> str:\n return \"i386\"\n \n \n-def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> list[str]:\n+def _mac_binary_formats(version: AppleVersion, cpu_arch: str) -> list[str]:\n formats = [cpu_arch]\n if cpu_arch == \"x86_64\":\n if version < (10, 4):\n@@ -396,7 +395,7 @@ def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> list[str]:\n \n \n def mac_platforms(\n- version: MacVersion | None = None, arch: str | None = None\n+ version: AppleVersion | None = None, arch: str | None = None\n ) -> Iterator[str]:\n \"\"\"\n Yields the platform tags for a macOS system.\n@@ -408,7 +407,7 @@ def mac_platforms(\n \"\"\"\n version_str, _, cpu_arch = platform.mac_ver()\n if version is None:\n- version = cast(\"MacVersion\", tuple(map(int, version_str.split(\".\")[:2])))\n+ version = cast(\"AppleVersion\", tuple(map(int, version_str.split(\".\")[:2])))\n if version == (10, 16):\n # When built against an older macOS SDK, Python will report macOS 10.16\n # instead of the real version.\n@@ -424,7 +423,7 @@ def mac_platforms(\n stdout=subprocess.PIPE,\n text=True,\n ).stdout\n- version = cast(\"MacVersion\", tuple(map(int, version_str.split(\".\")[:2])))\n+ version = cast(\"AppleVersion\", tuple(map(int, version_str.split(\".\")[:2])))\n else:\n version = version\n if arch is None:\n@@ -435,24 +434,22 @@ def mac_platforms(\n if (10, 0) <= version and version < (11, 0):\n # Prior to Mac OS 11, each yearly release of Mac OS bumped the\n # \"minor\" version number. The major version was always 10.\n+ major_version = 10\n for minor_version in range(version[1], -1, -1):\n- compat_version = 10, minor_version\n+ compat_version = major_version, minor_version\n binary_formats = _mac_binary_formats(compat_version, arch)\n for binary_format in binary_formats:\n- yield \"macosx_{major}_{minor}_{binary_format}\".format(\n- major=10, minor=minor_version, binary_format=binary_format\n- )\n+ yield f\"macosx_{major_version}_{minor_version}_{binary_format}\"\n \n if version >= (11, 0):\n # Starting with Mac OS 11, each yearly release bumps the major version\n # number. The minor versions are now the midyear updates.\n+ minor_version = 0\n for major_version in range(version[0], 10, -1):\n- compat_version = major_version, 0\n+ compat_version = major_version, minor_version\n binary_formats = _mac_binary_formats(compat_version, arch)\n for binary_format in binary_formats:\n- yield \"macosx_{major}_{minor}_{binary_format}\".format(\n- major=major_version, minor=0, binary_format=binary_format\n- )\n+ yield f\"macosx_{major_version}_{minor_version}_{binary_format}\"\n \n if version >= (11, 0):\n # Mac OS 11 on x86_64 is compatible with binaries from previous releases.\n@@ -462,25 +459,75 @@ def mac_platforms(\n # However, the \"universal2\" binary format can have a\n # macOS version earlier than 11.0 when the x86_64 part of the binary supports\n # that version of macOS.\n+ major_version = 10\n if arch == \"x86_64\":\n for minor_version in range(16, 3, -1):\n- compat_version = 10, minor_version\n+ compat_version = major_version, minor_version\n binary_formats = _mac_binary_formats(compat_version, arch)\n for binary_format in binary_formats:\n- yield \"macosx_{major}_{minor}_{binary_format}\".format(\n- major=compat_version[0],\n- minor=compat_version[1],\n- binary_format=binary_format,\n- )\n+ yield f\"macosx_{major_version}_{minor_version}_{binary_format}\"\n else:\n for minor_version in range(16, 3, -1):\n- compat_version = 10, minor_version\n+ compat_version = major_version, minor_version\n binary_format = \"universal2\"\n- yield \"macosx_{major}_{minor}_{binary_format}\".format(\n- major=compat_version[0],\n- minor=compat_version[1],\n- binary_format=binary_format,\n- )\n+ yield f\"macosx_{major_version}_{minor_version}_{binary_format}\"\n+\n+\n+def ios_platforms(\n+ version: AppleVersion | None = None, multiarch: str | None = None\n+) -> Iterator[str]:\n+ \"\"\"\n+ Yields the platform tags for an iOS system.\n+\n+ :param version: A two-item tuple specifying the iOS version to generate\n+ platform tags for. Defaults to the current iOS version.\n+ :param multiarch: The CPU architecture+ABI to generate platform tags for -\n+ (the value used by `sys.implementation._multiarch` e.g.,\n+ `arm64_iphoneos` or `x84_64_iphonesimulator`). Defaults to the current\n+ multiarch value.\n+ \"\"\"\n+ if version is None:\n+ # if iOS is the current platform, ios_ver *must* be defined. However,\n+ # it won't exist for CPython versions before 3.13, which causes a mypy\n+ # error.\n+ _, release, _, _ = platform.ios_ver() # type: ignore[attr-defined, unused-ignore]\n+ version = cast(\"AppleVersion\", tuple(map(int, release.split(\".\")[:2])))\n+\n+ if multiarch is None:\n+ multiarch = sys.implementation._multiarch\n+ multiarch = multiarch.replace(\"-\", \"_\")\n+\n+ ios_platform_template = \"ios_{major}_{minor}_{multiarch}\"\n+\n+ # Consider any iOS major.minor version from the version requested, down to\n+ # 12.0. 12.0 is the first iOS version that is known to have enough features\n+ # to support CPython. Consider every possible minor release up to X.9. There\n+ # highest the minor has ever gone is 8 (14.8 and 15.8) but having some extra\n+ # candidates that won't ever match doesn't really hurt, and it saves us from\n+ # having to keep an explicit list of known iOS versions in the code. Return\n+ # the results descending order of version number.\n+\n+ # If the requested major version is less than 12, there won't be any matches.\n+ if version[0] < 12:\n+ return\n+\n+ # Consider the actual X.Y version that was requested.\n+ yield ios_platform_template.format(\n+ major=version[0], minor=version[1], multiarch=multiarch\n+ )\n+\n+ # Consider every minor version from X.0 to the minor version prior to the\n+ # version requested by the platform.\n+ for minor in range(version[1] - 1, -1, -1):\n+ yield ios_platform_template.format(\n+ major=version[0], minor=minor, multiarch=multiarch\n+ )\n+\n+ for major in range(version[0] - 1, 11, -1):\n+ for minor in range(9, -1, -1):\n+ yield ios_platform_template.format(\n+ major=major, minor=minor, multiarch=multiarch\n+ )\n \n \n def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]:\n@@ -512,6 +559,8 @@ def platform_tags() -> Iterator[str]:\n \"\"\"\n if platform.system() == \"Darwin\":\n return mac_platforms()\n+ elif platform.system() == \"iOS\":\n+ return ios_platforms()\n elif platform.system() == \"Linux\":\n return _linux_platforms()\n else:"}, {"sha": "23450953df74eccd9c13cd2a955ce09d1f968565", "filename": "lib/packaging/utils.py", "status": "modified", "additions": 33, "deletions": 44, "changes": 77, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpackaging%2Futils.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpackaging%2Futils.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpackaging%2Futils.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -4,11 +4,12 @@\n \n from __future__ import annotations\n \n+import functools\n import re\n from typing import NewType, Tuple, Union, cast\n \n from .tags import Tag, parse_tag\n-from .version import InvalidVersion, Version\n+from .version import InvalidVersion, Version, _TrimmedRelease\n \n BuildTag = Union[Tuple[()], Tuple[int, str]]\n NormalizedName = NewType(\"NormalizedName\", str)\n@@ -54,89 +55,77 @@ def is_normalized_name(name: str) -> bool:\n return _normalized_regex.match(name) is not None\n \n \n+@functools.singledispatch\n def canonicalize_version(\n version: Version | str, *, strip_trailing_zero: bool = True\n ) -> str:\n \"\"\"\n- This is very similar to Version.__str__, but has one subtle difference\n- with the way it handles the release segment.\n- \"\"\"\n- if isinstance(version, str):\n- try:\n- parsed = Version(version)\n- except InvalidVersion:\n- # Legacy versions cannot be normalized\n- return version\n- else:\n- parsed = version\n-\n- parts = []\n+ Return a canonical form of a version as a string.\n \n- # Epoch\n- if parsed.epoch != 0:\n- parts.append(f\"{parsed.epoch}!\")\n+ >>> canonicalize_version('1.0.1')\n+ '1.0.1'\n \n- # Release segment\n- release_segment = \".\".join(str(x) for x in parsed.release)\n- if strip_trailing_zero:\n- # NB: This strips trailing '.0's to normalize\n- release_segment = re.sub(r\"(\\.0)+$\", \"\", release_segment)\n- parts.append(release_segment)\n+ Per PEP 625, versions may have multiple canonical forms, differing\n+ only by trailing zeros.\n \n- # Pre-release\n- if parsed.pre is not None:\n- parts.append(\"\".join(str(x) for x in parsed.pre))\n+ >>> canonicalize_version('1.0.0')\n+ '1'\n+ >>> canonicalize_version('1.0.0', strip_trailing_zero=False)\n+ '1.0.0'\n \n- # Post-release\n- if parsed.post is not None:\n- parts.append(f\".post{parsed.post}\")\n+ Invalid versions are returned unaltered.\n \n- # Development release\n- if parsed.dev is not None:\n- parts.append(f\".dev{parsed.dev}\")\n+ >>> canonicalize_version('foo bar baz')\n+ 'foo bar baz'\n+ \"\"\"\n+ return str(_TrimmedRelease(str(version)) if strip_trailing_zero else version)\n \n- # Local version segment\n- if parsed.local is not None:\n- parts.append(f\"+{parsed.local}\")\n \n- return \"\".join(parts)\n+@canonicalize_version.register\n+def _(version: str, *, strip_trailing_zero: bool = True) -> str:\n+ try:\n+ parsed = Version(version)\n+ except InvalidVersion:\n+ # Legacy versions cannot be normalized\n+ return version\n+ return canonicalize_version(parsed, strip_trailing_zero=strip_trailing_zero)\n \n \n def parse_wheel_filename(\n filename: str,\n ) -> tuple[NormalizedName, Version, BuildTag, frozenset[Tag]]:\n if not filename.endswith(\".whl\"):\n raise InvalidWheelFilename(\n- f\"Invalid wheel filename (extension must be '.whl'): {filename}\"\n+ f\"Invalid wheel filename (extension must be '.whl'): {filename!r}\"\n )\n \n filename = filename[:-4]\n dashes = filename.count(\"-\")\n if dashes not in (4, 5):\n raise InvalidWheelFilename(\n- f\"Invalid wheel filename (wrong number of parts): {filename}\"\n+ f\"Invalid wheel filename (wrong number of parts): {filename!r}\"\n )\n \n parts = filename.split(\"-\", dashes - 2)\n name_part = parts[0]\n # See PEP 427 for the rules on escaping the project name.\n if \"__\" in name_part or re.match(r\"^[\\w\\d._]*$\", name_part, re.UNICODE) is None:\n- raise InvalidWheelFilename(f\"Invalid project name: {filename}\")\n+ raise InvalidWheelFilename(f\"Invalid project name: {filename!r}\")\n name = canonicalize_name(name_part)\n \n try:\n version = Version(parts[1])\n except InvalidVersion as e:\n raise InvalidWheelFilename(\n- f\"Invalid wheel filename (invalid version): {filename}\"\n+ f\"Invalid wheel filename (invalid version): {filename!r}\"\n ) from e\n \n if dashes == 5:\n build_part = parts[2]\n build_match = _build_tag_regex.match(build_part)\n if build_match is None:\n raise InvalidWheelFilename(\n- f\"Invalid build number: {build_part} in '{filename}'\"\n+ f\"Invalid build number: {build_part} in {filename!r}\"\n )\n build = cast(BuildTag, (int(build_match.group(1)), build_match.group(2)))\n else:\n@@ -153,22 +142,22 @@ def parse_sdist_filename(filename: str) -> tuple[NormalizedName, Version]:\n else:\n raise InvalidSdistFilename(\n f\"Invalid sdist filename (extension must be '.tar.gz' or '.zip'):\"\n- f\" {filename}\"\n+ f\" {filename!r}\"\n )\n \n # We are requiring a PEP 440 version, which cannot contain dashes,\n # so we split on the last dash.\n name_part, sep, version_part = file_stem.rpartition(\"-\")\n if not sep:\n- raise InvalidSdistFilename(f\"Invalid sdist filename: {filename}\")\n+ raise InvalidSdistFilename(f\"Invalid sdist filename: {filename!r}\")\n \n name = canonicalize_name(name_part)\n \n try:\n version = Version(version_part)\n except InvalidVersion as e:\n raise InvalidSdistFilename(\n- f\"Invalid sdist filename (invalid version): {filename}\"\n+ f\"Invalid sdist filename (invalid version): {filename!r}\"\n ) from e\n \n return (name, version)"}, {"sha": "c9bbda20e463b8d9389ecd65f74af33810a02bdd", "filename": "lib/packaging/version.py", "status": "modified", "additions": 26, "deletions": 7, "changes": 33, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpackaging%2Fversion.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpackaging%2Fversion.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpackaging%2Fversion.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -15,7 +15,7 @@\n \n from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType\n \n-__all__ = [\"VERSION_PATTERN\", \"parse\", \"Version\", \"InvalidVersion\"]\n+__all__ = [\"VERSION_PATTERN\", \"InvalidVersion\", \"Version\", \"parse\"]\n \n LocalType = Tuple[Union[int, str], ...]\n \n@@ -199,7 +199,7 @@ def __init__(self, version: str) -> None:\n # Validate the version and parse it into pieces\n match = self._regex.search(version)\n if not match:\n- raise InvalidVersion(f\"Invalid version: '{version}'\")\n+ raise InvalidVersion(f\"Invalid version: {version!r}\")\n \n # Store the parsed out pieces of the version\n self._version = _Version(\n@@ -232,7 +232,7 @@ def __repr__(self) -> str:\n return f\"<Version('{self}')>\"\n \n def __str__(self) -> str:\n- \"\"\"A string representation of the version that can be rounded-tripped.\n+ \"\"\"A string representation of the version that can be round-tripped.\n \n >>> str(Version(\"1.0a5\"))\n '1.0a5'\n@@ -350,8 +350,8 @@ def public(self) -> str:\n '1.2.3'\n >>> Version(\"1.2.3+abc\").public\n '1.2.3'\n- >>> Version(\"1.2.3+abc.dev1\").public\n- '1.2.3'\n+ >>> Version(\"1!1.2.3dev1+abc\").public\n+ '1!1.2.3.dev1'\n \"\"\"\n return str(self).split(\"+\", 1)[0]\n \n@@ -363,7 +363,7 @@ def base_version(self) -> str:\n '1.2.3'\n >>> Version(\"1.2.3+abc\").base_version\n '1.2.3'\n- >>> Version(\"1!1.2.3+abc.dev1\").base_version\n+ >>> Version(\"1!1.2.3dev1+abc\").base_version\n '1!1.2.3'\n \n The \"base version\" is the public version of the project without any pre or post\n@@ -451,6 +451,23 @@ def micro(self) -> int:\n return self.release[2] if len(self.release) >= 3 else 0\n \n \n+class _TrimmedRelease(Version):\n+ @property\n+ def release(self) -> tuple[int, ...]:\n+ \"\"\"\n+ Release segment without any trailing zeros.\n+\n+ >>> _TrimmedRelease('1.0.0').release\n+ (1,)\n+ >>> _TrimmedRelease('0.0').release\n+ (0,)\n+ \"\"\"\n+ rel = super().release\n+ nonzeros = (index for index, val in enumerate(rel) if val)\n+ last_nonzero = max(nonzeros, default=0)\n+ return rel[: last_nonzero + 1]\n+\n+\n def _parse_letter_version(\n letter: str | None, number: str | bytes | SupportsInt | None\n ) -> tuple[str, int] | None:\n@@ -476,7 +493,9 @@ def _parse_letter_version(\n letter = \"post\"\n \n return letter, int(number)\n- if not letter and number:\n+\n+ assert not letter\n+ if number:\n # We assume if we are given a number, but we are not given a letter\n # then this is using the implicit post release syntax (e.g. 1.0-1)\n letter = \"post\""}, {"sha": "afe8351d203db97547f40568e40853b3708af024", "filename": "lib/platformdirs/__init__.py", "status": "modified", "additions": 14, "deletions": 10, "changes": 24, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplatformdirs%2F__init__.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplatformdirs%2F__init__.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fplatformdirs%2F__init__.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -19,18 +19,18 @@\n from pathlib import Path\n from typing import Literal\n \n+if sys.platform == \"win32\":\n+ from platformdirs.windows import Windows as _Result\n+elif sys.platform == \"darwin\":\n+ from platformdirs.macos import MacOS as _Result\n+else:\n+ from platformdirs.unix import Unix as _Result\n \n-def _set_platform_dir_class() -> type[PlatformDirsABC]:\n- if sys.platform == \"win32\":\n- from platformdirs.windows import Windows as Result # noqa: PLC0415\n- elif sys.platform == \"darwin\":\n- from platformdirs.macos import MacOS as Result # noqa: PLC0415\n- else:\n- from platformdirs.unix import Unix as Result # noqa: PLC0415\n \n+def _set_platform_dir_class() -> type[PlatformDirsABC]:\n if os.getenv(\"ANDROID_DATA\") == \"/data\" and os.getenv(\"ANDROID_ROOT\") == \"/system\":\n if os.getenv(\"SHELL\") or os.getenv(\"PREFIX\"):\n- return Result\n+ return _Result\n \n from platformdirs.android import _android_folder # noqa: PLC0415\n \n@@ -39,10 +39,14 @@ def _set_platform_dir_class() -> type[PlatformDirsABC]:\n \n return Android # return to avoid redefinition of a result\n \n- return Result\n+ return _Result\n \n \n-PlatformDirs = _set_platform_dir_class() #: Currently active platform\n+if TYPE_CHECKING:\n+ # Work around mypy issue: https://github.com/python/mypy/issues/10962\n+ PlatformDirs = _Result\n+else:\n+ PlatformDirs = _set_platform_dir_class() #: Currently active platform\n AppDirs = PlatformDirs #: Backwards compatibility with appdirs\n \n "}, {"sha": "7004a852422b104d16c9cbdfb2febe7b0504a594", "filename": "lib/platformdirs/android.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplatformdirs%2Fandroid.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplatformdirs%2Fandroid.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fplatformdirs%2Fandroid.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -117,7 +117,7 @@ def site_runtime_dir(self) -> str:\n \n \n @lru_cache(maxsize=1)\n-def _android_folder() -> str | None: # noqa: C901, PLR0912\n+def _android_folder() -> str | None: # noqa: C901\n \"\"\":return: base folder for the Android OS or None if it cannot be found\"\"\"\n result: str | None = None\n # type checker isn't happy with our \"import android\", just don't do this when type checking see"}, {"sha": "18d660e4f8c90dd170f7e131a9ac55d68e60ce56", "filename": "lib/platformdirs/api.py", "status": "modified", "additions": 6, "deletions": 0, "changes": 6, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplatformdirs%2Fapi.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplatformdirs%2Fapi.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fplatformdirs%2Fapi.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -91,6 +91,12 @@ def _optionally_create_directory(self, path: str) -> None:\n if self.ensure_exists:\n Path(path).mkdir(parents=True, exist_ok=True)\n \n+ def _first_item_as_path_if_multipath(self, directory: str) -> Path:\n+ if self.multipath:\n+ # If multipath is True, the first path is returned.\n+ directory = directory.split(os.pathsep)[0]\n+ return Path(directory)\n+\n @property\n @abstractmethod\n def user_data_dir(self) -> str:"}, {"sha": "e4b0391abd7a09ab76a6351fb6757f6498cdb240", "filename": "lib/platformdirs/macos.py", "status": "modified", "additions": 14, "deletions": 0, "changes": 14, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplatformdirs%2Fmacos.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplatformdirs%2Fmacos.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fplatformdirs%2Fmacos.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -4,9 +4,13 @@\n \n import os.path\n import sys\n+from typing import TYPE_CHECKING\n \n from .api import PlatformDirsABC\n \n+if TYPE_CHECKING:\n+ from pathlib import Path\n+\n \n class MacOS(PlatformDirsABC):\n \"\"\"\n@@ -42,6 +46,11 @@ def site_data_dir(self) -> str:\n return os.pathsep.join(path_list)\n return path_list[0]\n \n+ @property\n+ def site_data_path(self) -> Path:\n+ \"\"\":return: data path shared by users. Only return the first item, even if ``multipath`` is set to ``True``\"\"\"\n+ return self._first_item_as_path_if_multipath(self.site_data_dir)\n+\n @property\n def user_config_dir(self) -> str:\n \"\"\":return: config directory tied to the user, same as `user_data_dir`\"\"\"\n@@ -74,6 +83,11 @@ def site_cache_dir(self) -> str:\n return os.pathsep.join(path_list)\n return path_list[0]\n \n+ @property\n+ def site_cache_path(self) -> Path:\n+ \"\"\":return: cache path shared by users. Only return the first item, even if ``multipath`` is set to ``True``\"\"\"\n+ return self._first_item_as_path_if_multipath(self.site_cache_dir)\n+\n @property\n def user_state_dir(self) -> str:\n \"\"\":return: state directory tied to the user, same as `user_data_dir`\"\"\""}, {"sha": "f1942e92ef47ce5195966634c2a3c572776038de", "filename": "lib/platformdirs/unix.py", "status": "modified", "additions": 0, "deletions": 6, "changes": 6, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplatformdirs%2Funix.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplatformdirs%2Funix.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fplatformdirs%2Funix.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -218,12 +218,6 @@ def site_cache_path(self) -> Path:\n \"\"\":return: cache path shared by users. Only return the first item, even if ``multipath`` is set to ``True``\"\"\"\n return self._first_item_as_path_if_multipath(self.site_cache_dir)\n \n- def _first_item_as_path_if_multipath(self, directory: str) -> Path:\n- if self.multipath:\n- # If multipath is True, the first path is returned.\n- directory = directory.split(os.pathsep)[0]\n- return Path(directory)\n-\n def iter_config_dirs(self) -> Iterator[str]:\n \"\"\":yield: all user and site configuration directories.\"\"\"\n yield self.user_config_dir"}, {"sha": "afb49243e3dee24315f75c2a9c7fbf20b4de4b5f", "filename": "lib/platformdirs/version.py", "status": "modified", "additions": 2, "deletions": 2, "changes": 4, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplatformdirs%2Fversion.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplatformdirs%2Fversion.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fplatformdirs%2Fversion.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -12,5 +12,5 @@\n __version_tuple__: VERSION_TUPLE\n version_tuple: VERSION_TUPLE\n \n-__version__ = version = '4.2.2'\n-__version_tuple__ = version_tuple = (4, 2, 2)\n+__version__ = version = '4.3.6'\n+__version_tuple__ = version_tuple = (4, 3, 6)"}, {"sha": "05d38a9c779baf054a9dbe62e5bdcf620652d028", "filename": "lib/plexapi/audio.py", "status": "modified", "additions": 2, "deletions": 0, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplexapi%2Faudio.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplexapi%2Faudio.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fplexapi%2Faudio.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -33,6 +33,7 @@ class Audio(PlexPartialObject, PlayedUnplayedMixin):\n distance (float): Sonic Distance of the item from the seed item.\n fields (List<:class:`~plexapi.media.Field`>): List of field objects.\n guid (str): Plex GUID for the artist, album, or track (plex://artist/5d07bcb0403c64029053ac4c).\n+ images (List<:class:`~plexapi.media.Image`>): List of image objects.\n index (int): Plex index number (often the track number).\n key (str): API URL (/library/metadata/<ratingkey>).\n lastRatedAt (datetime): Datetime the item was last rated.\n@@ -65,6 +66,7 @@ def _loadData(self, data):\n self.distance = utils.cast(float, data.attrib.get('distance'))\n self.fields = self.findItems(data, media.Field)\n self.guid = data.attrib.get('guid')\n+ self.images = self.findItems(data, media.Image)\n self.index = utils.cast(int, data.attrib.get('index'))\n self.key = data.attrib.get('key', '')\n self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))"}, {"sha": "675ac5d9859074429b4f52d89f1aef897be4e5f2", "filename": "lib/plexapi/base.py", "status": "modified", "additions": 2, "deletions": 2, "changes": 4, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplexapi%2Fbase.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplexapi%2Fbase.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fplexapi%2Fbase.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -17,7 +17,7 @@\n MediaContainerT = TypeVar(\"MediaContainerT\", bound=\"MediaContainer\")\n \n USER_DONT_RELOAD_FOR_KEYS = set()\n-_DONT_RELOAD_FOR_KEYS = {'key'}\n+_DONT_RELOAD_FOR_KEYS = {'key', 'sourceURI'}\n OPERATORS = {\n 'exact': lambda v, q: v == q,\n 'iexact': lambda v, q: v.lower() == q.lower(),\n@@ -71,7 +71,7 @@ def __init__(self, server, data, initpath=None, parent=None):\n self._details_key = self._buildDetailsKey()\n \n def __repr__(self):\n- uid = self._clean(self.firstAttr('_baseurl', 'ratingKey', 'id', 'key', 'playQueueID', 'uri'))\n+ uid = self._clean(self.firstAttr('_baseurl', 'ratingKey', 'id', 'key', 'playQueueID', 'uri', 'type'))\n name = self._clean(self.firstAttr('title', 'name', 'username', 'product', 'tag', 'value'))\n return f\"<{':'.join([p for p in [self.__class__.__name__, uid, name] if p])}>\"\n "}, {"sha": "63ea837300aca2b1ff1fdf8b879fa7a4077e9022", "filename": "lib/plexapi/collection.py", "status": "modified", "additions": 2, "deletions": 0, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplexapi%2Fcollection.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplexapi%2Fcollection.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fplexapi%2Fcollection.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -39,6 +39,7 @@ class Collection(\n contentRating (str) Content rating (PG-13; NR; TV-G).\n fields (List<:class:`~plexapi.media.Field`>): List of field objects.\n guid (str): Plex GUID for the collection (collection://XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX).\n+ images (List<:class:`~plexapi.media.Image`>): List of image objects.\n index (int): Plex index number for the collection.\n key (str): API URL (/library/metadata/<ratingkey>).\n labels (List<:class:`~plexapi.media.Label`>): List of label objects.\n@@ -82,6 +83,7 @@ def _loadData(self, data):\n self.contentRating = data.attrib.get('contentRating')\n self.fields = self.findItems(data, media.Field)\n self.guid = data.attrib.get('guid')\n+ self.images = self.findItems(data, media.Image)\n self.index = utils.cast(int, data.attrib.get('index'))\n self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50\n self.labels = self.findItems(data, media.Label)"}, {"sha": "93f7e034ce68341145234ad86ed44444434d3269", "filename": "lib/plexapi/const.py", "status": "modified", "additions": 2, "deletions": 2, "changes": 4, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplexapi%2Fconst.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplexapi%2Fconst.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fplexapi%2Fconst.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -3,7 +3,7 @@\n \n # Library version\n MAJOR_VERSION = 4\n-MINOR_VERSION = 15\n-PATCH_VERSION = 16\n+MINOR_VERSION = 16\n+PATCH_VERSION = 0\n __short_version__ = f\"{MAJOR_VERSION}.{MINOR_VERSION}\"\n __version__ = f\"{__short_version__}.{PATCH_VERSION}\""}, {"sha": "93801a1d7f32356785495324d580e90d525a81f3", "filename": "lib/plexapi/library.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplexapi%2Flibrary.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplexapi%2Flibrary.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fplexapi%2Flibrary.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1740,7 +1740,7 @@ def common(self, items):\n \n def _edit(self, items=None, **kwargs):\n \"\"\" Actually edit multiple objects. \"\"\"\n- if isinstance(self._edits, dict):\n+ if isinstance(self._edits, dict) and items is None:\n self._edits.update(kwargs)\n return self\n "}, {"sha": "9c6e3115b9db241f459d6a9544696b7e6a1f8de9", "filename": "lib/plexapi/media.py", "status": "modified", "additions": 29, "deletions": 0, "changes": 29, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplexapi%2Fmedia.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplexapi%2Fmedia.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fplexapi%2Fmedia.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -26,6 +26,7 @@ class Media(PlexObject):\n height (int): The height of the media in pixels (ex: 256).\n id (int): The unique ID for this media on the server.\n has64bitOffsets (bool): True if video has 64 bit offsets.\n+ hasVoiceActivity (bool): True if video has voice activity analyzed.\n optimizedForStreaming (bool): True if video is optimized for streaming.\n parts (List<:class:`~plexapi.media.MediaPart`>): List of media part objects.\n proxyType (int): Equals 42 for optimized versions.\n@@ -61,6 +62,7 @@ def _loadData(self, data):\n self.height = utils.cast(int, data.attrib.get('height'))\n self.id = utils.cast(int, data.attrib.get('id'))\n self.has64bitOffsets = utils.cast(bool, data.attrib.get('has64bitOffsets'))\n+ self.hasVoiceActivity = utils.cast(bool, data.attrib.get('hasVoiceActivity', '0'))\n self.optimizedForStreaming = utils.cast(bool, data.attrib.get('optimizedForStreaming'))\n self.parts = self.findItems(data, MediaPart)\n self.proxyType = utils.cast(int, data.attrib.get('proxyType'))\n@@ -441,6 +443,7 @@ class SubtitleStream(MediaPartStream):\n Attributes:\n TAG (str): 'Stream'\n STREAMTYPE (int): 3\n+ canAutoSync (bool): True if the subtitle stream can be auto synced.\n container (str): The container of the subtitle stream.\n forced (bool): True if this is a forced subtitle.\n format (str): The format of the subtitle stream (ex: srt).\n@@ -459,6 +462,7 @@ class SubtitleStream(MediaPartStream):\n def _loadData(self, data):\n \"\"\" Load attribute values from Plex XML response. \"\"\"\n super(SubtitleStream, self)._loadData(data)\n+ self.canAutoSync = utils.cast(bool, data.attrib.get('canAutoSync'))\n self.container = data.attrib.get('container')\n self.forced = utils.cast(bool, data.attrib.get('forced', '0'))\n self.format = data.attrib.get('format')\n@@ -954,6 +958,26 @@ def _loadData(self, data):\n self.id = data.attrib.get('id')\n \n \n+@utils.registerPlexObject\n+class Image(PlexObject):\n+ \"\"\" Represents a single Image media tag.\n+\n+ Attributes:\n+ TAG (str): 'Image'\n+ alt (str): The alt text for the image.\n+ type (str): The type of image (e.g. coverPoster, background, snapshot).\n+ url (str): The API URL (/library/metadata/<ratingKey>/thumb/<thumbid>).\n+ \"\"\"\n+ TAG = 'Image'\n+\n+ def _loadData(self, data):\n+ \"\"\" Load attribute values from Plex XML response. \"\"\"\n+ self._data = data\n+ self.alt = data.attrib.get('alt')\n+ self.type = data.attrib.get('type')\n+ self.url = data.attrib.get('url')\n+\n+\n @utils.registerPlexObject\n class Rating(PlexObject):\n \"\"\" Represents a single Rating media tag.\n@@ -1074,6 +1098,11 @@ class Art(BaseResource):\n TAG = 'Photo'\n \n \n+class Logo(BaseResource):\n+ \"\"\" Represents a single Logo object. \"\"\"\n+ TAG = 'Photo'\n+\n+\n class Poster(BaseResource):\n \"\"\" Represents a single Poster object. \"\"\"\n TAG = 'Photo'"}, {"sha": "95f785fcc9429c28413a4fa2e0a1d11455f5cc30", "filename": "lib/plexapi/mixins.py", "status": "modified", "additions": 62, "deletions": 0, "changes": 62, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplexapi%2Fmixins.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplexapi%2Fmixins.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fplexapi%2Fmixins.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -403,6 +403,63 @@ def setArt(self, art):\n return self\n \n \n+class LogoUrlMixin:\n+ \"\"\" Mixin for Plex objects that can have a logo url. \"\"\"\n+\n+ @property\n+ def logoUrl(self):\n+ \"\"\" Return the logo url for the Plex object. \"\"\"\n+ image = next((i for i in self.images if i.type == 'clearLogo'), None)\n+ return self._server.url(image.url, includeToken=True) if image else None\n+\n+\n+class LogoLockMixin:\n+ \"\"\" Mixin for Plex objects that can have a locked logo. \"\"\"\n+\n+ def lockLogo(self):\n+ \"\"\" Lock the logo for a Plex object. \"\"\"\n+ raise NotImplementedError('Logo cannot be locked through the API.')\n+\n+ def unlockLogo(self):\n+ \"\"\" Unlock the logo for a Plex object. \"\"\"\n+ raise NotImplementedError('Logo cannot be unlocked through the API.')\n+\n+\n+class LogoMixin(LogoUrlMixin, LogoLockMixin):\n+ \"\"\" Mixin for Plex objects that can have logos. \"\"\"\n+\n+ def logos(self):\n+ \"\"\" Returns list of available :class:`~plexapi.media.Logo` objects. \"\"\"\n+ return self.fetchItems(f'/library/metadata/{self.ratingKey}/clearLogos', cls=media.Logo)\n+\n+ def uploadLogo(self, url=None, filepath=None):\n+ \"\"\" Upload a logo from a url or filepath.\n+\n+ Parameters:\n+ url (str): The full URL to the image to upload.\n+ filepath (str): The full file path the the image to upload or file-like object.\n+ \"\"\"\n+ if url:\n+ key = f'/library/metadata/{self.ratingKey}/clearLogos?url={quote_plus(url)}'\n+ self._server.query(key, method=self._server._session.post)\n+ elif filepath:\n+ key = f'/library/metadata/{self.ratingKey}/clearLogos'\n+ data = openOrRead(filepath)\n+ self._server.query(key, method=self._server._session.post, data=data)\n+ return self\n+\n+ def setLogo(self, logo):\n+ \"\"\" Set the logo for a Plex object.\n+\n+ Raises:\n+ :exc:`~plexapi.exceptions.NotImplementedError`: Logo cannot be set through the API.\n+ \"\"\"\n+ raise NotImplementedError(\n+ 'Logo cannot be set through the API. '\n+ 'Re-upload the logo using \"uploadLogo\" to set it.'\n+ )\n+\n+\n class PosterUrlMixin:\n \"\"\" Mixin for Plex objects that can have a poster url. \"\"\"\n \n@@ -513,6 +570,11 @@ def uploadTheme(self, url=None, filepath=None, timeout=None):\n return self\n \n def setTheme(self, theme):\n+ \"\"\" Set the theme for a Plex object.\n+\n+ Raises:\n+ :exc:`~plexapi.exceptions.NotImplementedError`: Themes cannot be set through the API.\n+ \"\"\"\n raise NotImplementedError(\n 'Themes cannot be set through the API. '\n 'Re-upload the theme using \"uploadTheme\" to set it.'"}, {"sha": "448a2649a1b29ff3e87a6cfc58fb60c3c7d2ac6a", "filename": "lib/plexapi/myplex.py", "status": "modified", "additions": 2, "deletions": 2, "changes": 4, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplexapi%2Fmyplex.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplexapi%2Fmyplex.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fplexapi%2Fmyplex.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -283,10 +283,10 @@ def resource(self, name):\n \"\"\" Returns the :class:`~plexapi.myplex.MyPlexResource` that matches the name specified.\n \n Parameters:\n- name (str): Name to match against.\n+ name (str): Name or machine identifier to match against.\n \"\"\"\n for resource in self.resources():\n- if resource.name.lower() == name.lower():\n+ if resource.name.lower() == name.lower() or resource.clientIdentifier == name:\n return resource\n raise NotFound(f'Unable to find resource {name}')\n "}, {"sha": "4347f31a8772f4d9050949e1c6dc89376abc4b8b", "filename": "lib/plexapi/photo.py", "status": "modified", "additions": 4, "deletions": 0, "changes": 4, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplexapi%2Fphoto.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplexapi%2Fphoto.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fplexapi%2Fphoto.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -30,6 +30,7 @@ class Photoalbum(\n composite (str): URL to composite image (/library/metadata/<ratingKey>/composite/<compositeid>)\n fields (List<:class:`~plexapi.media.Field`>): List of field objects.\n guid (str): Plex GUID for the photo album (local://229674).\n+ images (List<:class:`~plexapi.media.Image`>): List of image objects.\n index (sting): Plex index number for the photo album.\n key (str): API URL (/library/metadata/<ratingkey>).\n lastRatedAt (datetime): Datetime the photo album was last rated.\n@@ -57,6 +58,7 @@ def _loadData(self, data):\n self.composite = data.attrib.get('composite')\n self.fields = self.findItems(data, media.Field)\n self.guid = data.attrib.get('guid')\n+ self.images = self.findItems(data, media.Image)\n self.index = utils.cast(int, data.attrib.get('index'))\n self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50\n self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))\n@@ -164,6 +166,7 @@ class Photo(\n createdAtTZOffset (int): Unknown (-25200).\n fields (List<:class:`~plexapi.media.Field`>): List of field objects.\n guid (str): Plex GUID for the photo (com.plexapp.agents.none://231714?lang=xn).\n+ images (List<:class:`~plexapi.media.Image`>): List of image objects.\n index (sting): Plex index number for the photo.\n key (str): API URL (/library/metadata/<ratingkey>).\n lastRatedAt (datetime): Datetime the photo was last rated.\n@@ -204,6 +207,7 @@ def _loadData(self, data):\n self.createdAtTZOffset = utils.cast(int, data.attrib.get('createdAtTZOffset'))\n self.fields = self.findItems(data, media.Field)\n self.guid = data.attrib.get('guid')\n+ self.images = self.findItems(data, media.Image)\n self.index = utils.cast(int, data.attrib.get('index'))\n self.key = data.attrib.get('key', '')\n self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))"}, {"sha": "e2c4da635c84bbca27da8b09282f52b37179f0ec", "filename": "lib/plexapi/playlist.py", "status": "modified", "additions": 14, "deletions": 0, "changes": 14, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplexapi%2Fplaylist.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplexapi%2Fplaylist.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fplexapi%2Fplaylist.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -190,6 +190,20 @@ def items(self):\n if self._items is None:\n key = f'{self.key}/items'\n items = self.fetchItems(key)\n+\n+ # Cache server connections to avoid reconnecting for each item\n+ _servers = {}\n+ for item in items:\n+ if item.sourceURI:\n+ serverID = item.sourceURI.split('/')[2]\n+ if serverID not in _servers:\n+ try:\n+ _servers[serverID] = self._server.myPlexAccount().resource(serverID).connect()\n+ except NotFound:\n+ # Override the server connection with None if the server is not found\n+ _servers[serverID] = None\n+ item._server = _servers[serverID]\n+\n self._items = items\n return self._items\n "}, {"sha": "dd1cfc9ce8c62cd4ab48a4b798807c9a532b6de9", "filename": "lib/plexapi/utils.py", "status": "modified", "additions": 2, "deletions": 0, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplexapi%2Futils.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplexapi%2Futils.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fplexapi%2Futils.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -90,6 +90,8 @@\n 'theme': 317,\n 'studio': 318,\n 'network': 319,\n+ 'showOrdering': 322,\n+ 'clearLogo': 323,\n 'place': 400,\n }\n REVERSETAGTYPES = {v: k for k, v in TAGTYPES.items()}"}, {"sha": "6e811aa4ffaefd6bca9709e56e5fce0cf2969ded", "filename": "lib/plexapi/video.py", "status": "modified", "additions": 15, "deletions": 3, "changes": 18, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplexapi%2Fvideo.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fplexapi%2Fvideo.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fplexapi%2Fvideo.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -9,7 +9,7 @@\n from plexapi.exceptions import BadRequest\n from plexapi.mixins import (\n AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin,\n- ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, ThemeUrlMixin, ThemeMixin,\n+ ArtUrlMixin, ArtMixin, LogoMixin, PosterUrlMixin, PosterMixin, ThemeUrlMixin, ThemeMixin,\n MovieEditMixins, ShowEditMixins, SeasonEditMixins, EpisodeEditMixins,\n WatchlistMixin\n )\n@@ -26,6 +26,7 @@ class Video(PlexPartialObject, PlayedUnplayedMixin):\n artBlurHash (str): BlurHash string for artwork image.\n fields (List<:class:`~plexapi.media.Field`>): List of field objects.\n guid (str): Plex GUID for the movie, show, season, episode, or clip (plex://movie/5d776b59ad5437001f79c6f8).\n+ images (List<:class:`~plexapi.media.Image`>): List of image objects.\n key (str): API URL (/library/metadata/<ratingkey>).\n lastRatedAt (datetime): Datetime the item was last rated.\n lastViewedAt (datetime): Datetime the item was last played.\n@@ -53,6 +54,7 @@ def _loadData(self, data):\n self.artBlurHash = data.attrib.get('artBlurHash')\n self.fields = self.findItems(data, media.Field)\n self.guid = data.attrib.get('guid')\n+ self.images = self.findItems(data, media.Image)\n self.key = data.attrib.get('key', '')\n self.lastRatedAt = utils.toDatetime(data.attrib.get('lastRatedAt'))\n self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt'))\n@@ -332,7 +334,7 @@ def sync(self, videoQuality, client=None, clientId=None, limit=None, unwatched=F\n class Movie(\n Video, Playable,\n AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,\n- ArtMixin, PosterMixin, ThemeMixin,\n+ ArtMixin, LogoMixin, PosterMixin, ThemeMixin,\n MovieEditMixins,\n WatchlistMixin\n ):\n@@ -447,6 +449,11 @@ def hasCreditsMarker(self):\n \"\"\" Returns True if the movie has a credits marker. \"\"\"\n return any(marker.type == 'credits' for marker in self.markers)\n \n+ @property\n+ def hasVoiceActivity(self):\n+ \"\"\" Returns True if any of the media has voice activity analyzed. \"\"\"\n+ return any(media.hasVoiceActivity for media in self.media)\n+\n @property\n def hasPreviewThumbnails(self):\n \"\"\" Returns True if any of the media parts has generated preview (BIF) thumbnails. \"\"\"\n@@ -489,7 +496,7 @@ def metadataDirectory(self):\n class Show(\n Video,\n AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,\n- ArtMixin, PosterMixin, ThemeMixin,\n+ ArtMixin, LogoMixin, PosterMixin, ThemeMixin,\n ShowEditMixins,\n WatchlistMixin\n ):\n@@ -1077,6 +1084,11 @@ def hasCreditsMarker(self):\n \"\"\" Returns True if the episode has a credits marker. \"\"\"\n return any(marker.type == 'credits' for marker in self.markers)\n \n+ @property\n+ def hasVoiceActivity(self):\n+ \"\"\" Returns True if any of the media has voice activity analyzed. \"\"\"\n+ return any(media.hasVoiceActivity for media in self.media)\n+\n @property\n def hasPreviewThumbnails(self):\n \"\"\" Returns True if any of the media parts has generated preview (BIF) thumbnails. \"\"\""}, {"sha": "64c343823fdb6852ab3255d8dd953a83e96de351", "filename": "lib/profilehooks.py", "status": "modified", "additions": 30, "deletions": 231, "changes": 261, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fprofilehooks.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fprofilehooks.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fprofilehooks.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -39,28 +39,12 @@ def fn(n):\n \n Caveats\n \n- A thread on python-dev convinced me that hotshot produces bogus numbers.\n- See https://mail.python.org/pipermail/python-dev/2005-November/058264.html\n-\n I don't know what will happen if a decorated function will try to call\n another decorated function. All decorators probably need to explicitly\n support nested profiling (currently TraceFuncCoverage is the only one\n- that supports this, while HotShotFuncProfile has support for recursive\n- functions.)\n-\n- Profiling with hotshot creates temporary files (*.prof for profiling,\n- *.cprof for coverage) in the current directory. These files are not\n- cleaned up. Exception: when you specify a filename to the profile\n- decorator (to store the pstats.Stats object for later inspection),\n- the temporary file will be the filename you specified with '.raw'\n- appended at the end.\n-\n- Coverage analysis with hotshot seems to miss some executions resulting\n- in lower line counts and some lines errorneously marked as never\n- executed. For this reason coverage analysis now uses trace.py which is\n- slower, but more accurate.\n-\n-Copyright (c) 2004--2020 Marius Gedminas <marius@gedmin.as>\n+ that supports this.)\n+\n+Copyright (c) 2004--2023 Marius Gedminas <marius@gedmin.as>\n Copyright (c) 2007 Hanno Schlichting\n Copyright (c) 2008 Florian Schulze\n \n@@ -86,76 +70,44 @@ def fn(n):\n \n (Previously it was distributed under the GNU General Public Licence.)\n \"\"\"\n-from __future__ import print_function\n-\n-__author__ = \"Marius Gedminas <marius@gedmin.as>\"\n-__copyright__ = \"Copyright 2004-2020 Marius Gedminas and contributors\"\n-__license__ = \"MIT\"\n-__version__ = '1.12.0'\n-__date__ = \"2020-08-20\"\n-\n import atexit\n-\n+import dis\n import functools\n import inspect\n import logging\n import os\n+import pstats\n import re\n import sys\n-\n-# For profiling\n-from profile import Profile\n-import pstats\n-\n-# For timecall\n import timeit\n-\n-# For hotshot profiling (inaccurate!)\n-try:\n- import hotshot\n- import hotshot.stats\n-except ImportError:\n- hotshot = None\n-\n-# For trace.py coverage\n-import trace\n-import dis\n import token\n import tokenize\n+import trace\n+from profile import Profile\n \n-# For hotshot coverage (inaccurate!; uses undocumented APIs; might break)\n-if hotshot is not None:\n- import _hotshot\n- import hotshot.log\n \n # For cProfile profiling (best)\n try:\n import cProfile\n except ImportError:\n cProfile = None\n \n-# registry of available profilers\n-AVAILABLE_PROFILERS = {}\n-\n-__all__ = ['coverage', 'coverage_with_hotshot', 'profile', 'timecall']\n \n+__author__ = \"Marius Gedminas <marius@gedmin.as>\"\n+__copyright__ = \"Copyright 2004-2020 Marius Gedminas and contributors\"\n+__license__ = \"MIT\"\n+__version__ = '1.13.0'\n+__date__ = \"2024-10-09\"\n \n-# Use tokenize.open() on Python >= 3.2, fall back to open() on Python 2\n-tokenize_open = getattr(tokenize, 'open', open)\n \n+# registry of available profilers\n+AVAILABLE_PROFILERS = {}\n \n-def _unwrap(fn):\n- # inspect.unwrap() doesn't exist on Python 2\n- if not hasattr(fn, '__wrapped__'):\n- return fn\n- else:\n- # intentionally using recursion here instead of a while loop to\n- # make cycles fail with a recursion error instead of looping forever.\n- return _unwrap(fn.__wrapped__)\n+__all__ = ['coverage', 'profile', 'timecall']\n \n \n def _identify(fn):\n- fn = _unwrap(fn)\n+ fn = inspect.unwrap(fn)\n funcname = fn.__name__\n filename = fn.__code__.co_filename\n lineno = fn.__code__.co_firstlineno\n@@ -168,7 +120,7 @@ def _is_file_like(o):\n \n def profile(fn=None, skip=0, filename=None, immediate=False, dirs=False,\n sort=None, entries=40,\n- profiler=('cProfile', 'profile', 'hotshot'),\n+ profiler=('cProfile', 'profile'),\n stdout=True):\n \"\"\"Mark `fn` for profiling.\n \n@@ -205,7 +157,7 @@ def profile(fn=None, skip=0, filename=None, immediate=False, dirs=False,\n \n `profiler` can be used to select the preferred profiler, or specify a\n sequence of them, in order of preference. The default is ('cProfile'.\n- 'profile', 'hotshot').\n+ 'profile').\n \n If `filename` is specified, the profile stats will be stored in the\n named file. You can load them with pstats.Stats(filename) or use a\n@@ -279,26 +231,7 @@ def fn(...):\n ...\n \n \"\"\"\n- fp = TraceFuncCoverage(fn) # or HotShotFuncCoverage\n- # We cannot return fp or fp.__call__ directly as that would break method\n- # definitions, instead we need to return a plain function.\n-\n- @functools.wraps(fn)\n- def new_fn(*args, **kw):\n- return fp(*args, **kw)\n- return new_fn\n-\n-\n-def coverage_with_hotshot(fn):\n- \"\"\"Mark `fn` for line coverage analysis.\n-\n- Uses the 'hotshot' module for fast coverage analysis.\n-\n- BUG: Produces inaccurate results.\n-\n- See the docstring of `coverage` for usage examples.\n- \"\"\"\n- fp = HotShotFuncCoverage(fn)\n+ fp = TraceFuncCoverage(fn)\n # We cannot return fp or fp.__call__ directly as that would break method\n # definitions, instead we need to return a plain function.\n \n@@ -424,148 +357,8 @@ class CProfileFuncProfile(FuncProfile):\n AVAILABLE_PROFILERS['cProfile'] = CProfileFuncProfile\n \n \n-if hotshot is not None:\n-\n- class HotShotFuncProfile(FuncProfile):\n- \"\"\"Profiler for a function (uses hotshot).\"\"\"\n-\n- # This flag is shared between all instances\n- in_profiler = False\n-\n- def __init__(self, fn, skip=0, filename=None, immediate=False,\n- dirs=False, sort=None, entries=40, stdout=True):\n- \"\"\"Creates a profiler for a function.\n-\n- Every profiler has its own log file (the name of which is derived\n- from the function name).\n-\n- HotShotFuncProfile registers an atexit handler that prints\n- profiling information to sys.stderr when the program terminates.\n-\n- The log file is not removed and remains there to clutter the\n- current working directory.\n- \"\"\"\n- if filename:\n- self.logfilename = filename + \".raw\"\n- else:\n- self.logfilename = \"%s.%d.prof\" % (fn.__name__, os.getpid())\n- super(HotShotFuncProfile, self).__init__(\n- fn, skip=skip, filename=filename, immediate=immediate,\n- dirs=dirs, sort=sort, entries=entries, stdout=stdout)\n-\n- def __call__(self, *args, **kw):\n- \"\"\"Profile a singe call to the function.\"\"\"\n- self.ncalls += 1\n- if self.skip > 0:\n- self.skip -= 1\n- self.skipped += 1\n- return self.fn(*args, **kw)\n- if HotShotFuncProfile.in_profiler:\n- # handle recursive calls\n- return self.fn(*args, **kw)\n- if self.profiler is None:\n- self.profiler = hotshot.Profile(self.logfilename)\n- try:\n- HotShotFuncProfile.in_profiler = True\n- return self.profiler.runcall(self.fn, *args, **kw)\n- finally:\n- HotShotFuncProfile.in_profiler = False\n- if self.immediate:\n- self.print_stats()\n- self.reset_stats()\n-\n- def print_stats(self):\n- if self.profiler is None:\n- self.stats = pstats.Stats(Profile())\n- else:\n- self.profiler.close()\n- self.stats = hotshot.stats.load(self.logfilename)\n- super(HotShotFuncProfile, self).print_stats()\n-\n- def reset_stats(self):\n- self.profiler = None\n- self.ncalls = 0\n- self.skipped = 0\n-\n- AVAILABLE_PROFILERS['hotshot'] = HotShotFuncProfile\n-\n- class HotShotFuncCoverage:\n- \"\"\"Coverage analysis for a function (uses _hotshot).\n-\n- HotShot coverage is reportedly faster than trace.py, but it appears to\n- have problems with exceptions; also line counts in coverage reports\n- are generally lower from line counts produced by TraceFuncCoverage.\n- Is this my bug, or is it a problem with _hotshot?\n- \"\"\"\n-\n- def __init__(self, fn):\n- \"\"\"Creates a profiler for a function.\n-\n- Every profiler has its own log file (the name of which is derived\n- from the function name).\n-\n- HotShotFuncCoverage registers an atexit handler that prints\n- profiling information to sys.stderr when the program terminates.\n-\n- The log file is not removed and remains there to clutter the\n- current working directory.\n- \"\"\"\n- self.fn = fn\n- self.logfilename = \"%s.%d.cprof\" % (fn.__name__, os.getpid())\n- self.profiler = _hotshot.coverage(self.logfilename)\n- self.ncalls = 0\n- atexit.register(self.atexit)\n-\n- def __call__(self, *args, **kw):\n- \"\"\"Profile a singe call to the function.\"\"\"\n- self.ncalls += 1\n- old_trace = sys.gettrace()\n- try:\n- return self.profiler.runcall(self.fn, args, kw)\n- finally: # pragma: nocover\n- sys.settrace(old_trace)\n-\n- def atexit(self):\n- \"\"\"Stop profiling and print profile information to sys.stderr.\n-\n- This function is registered as an atexit hook.\n- \"\"\"\n- self.profiler.close()\n- funcname, filename, lineno = _identify(self.fn)\n- print(\"\")\n- print(\"*** COVERAGE RESULTS ***\")\n- print(\"%s (%s:%s)\" % (funcname, filename, lineno))\n- print(\"function called %d times\" % self.ncalls)\n- print(\"\")\n- fs = FuncSource(self.fn)\n- reader = hotshot.log.LogReader(self.logfilename)\n- for what, (filename, lineno, funcname), tdelta in reader:\n- if filename != fs.filename:\n- continue\n- if what == hotshot.log.LINE:\n- fs.mark(lineno)\n- if what == hotshot.log.ENTER:\n- # hotshot gives us the line number of the function\n- # definition and never gives us a LINE event for the first\n- # statement in a function, so if we didn't perform this\n- # mapping, the first statement would be marked as never\n- # executed\n- if lineno == fs.firstlineno:\n- lineno = fs.firstcodelineno\n- fs.mark(lineno)\n- reader.close()\n- print(fs)\n- never_executed = fs.count_never_executed()\n- if never_executed:\n- print(\"%d lines were not executed.\" % never_executed)\n-\n-\n class TraceFuncCoverage:\n- \"\"\"Coverage analysis for a function (uses trace module).\n-\n- HotShot coverage analysis is reportedly faster, but it appears to have\n- problems with exceptions.\n- \"\"\"\n+ \"\"\"Coverage analysis for a function (uses trace module).\"\"\"\n \n # Shared between all instances so that nested calls work\n tracer = trace.Trace(count=True, trace=False,\n@@ -646,13 +439,19 @@ def __init__(self, fn):\n \n def find_source_lines(self):\n \"\"\"Mark all executable source lines in fn as executed 0 times.\"\"\"\n- if self.filename is None:\n+ if self.filename is None: # pragma: nocover\n+ # I don't know how to make inspect.getsourcefile() return None in\n+ # our test suite, but I've looked at its source and I know that it\n+ # can do so.\n return\n strs = self._find_docstrings(self.filename)\n lines = {\n ln\n- for off, ln in dis.findlinestarts(_unwrap(self.fn).__code__)\n- if ln not in strs\n+ for off, ln in dis.findlinestarts(inspect.unwrap(self.fn).__code__)\n+ # skipping firstlineno because Python 3.11 adds a 'RESUME' opcode\n+ # attributed to the `def` line, but then trace.py never sees it\n+ # getting executed\n+ if ln is not None and ln not in strs and ln != self.firstlineno\n }\n for lineno in lines:\n self.sourcelines.setdefault(lineno, 0)\n@@ -667,7 +466,7 @@ def _find_docstrings(self, filename):\n # Python 3.2 and removed in 3.6.\n strs = set()\n prev = token.INDENT # so module docstring is detected as docstring\n- with tokenize_open(filename) as f:\n+ with tokenize.open(filename) as f:\n tokens = tokenize.generate_tokens(f.readline)\n for ttype, tstr, start, end, line in tokens:\n if ttype == token.STRING and prev == token.INDENT:"}, {"sha": "543ceb62bd531440ab3697d17076d219efe910a6", "filename": "lib/pyparsing/__init__.py", "status": "modified", "additions": 8, "deletions": 7, "changes": 15, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpyparsing%2F__init__.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpyparsing%2F__init__.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpyparsing%2F__init__.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -120,8 +120,8 @@ def __repr__(self):\n return f\"{__name__}.{type(self).__name__}({', '.join('{}={!r}'.format(*nv) for nv in zip(self._fields, self))})\"\n \n \n-__version_info__ = version_info(3, 1, 2, \"final\", 1)\n-__version_time__ = \"06 Mar 2024 07:08 UTC\"\n+__version_info__ = version_info(3, 2, 0, \"final\", 1)\n+__version_time__ = \"13 Oct 2024 09:46 UTC\"\n __version__ = __version_info__.__version__\n __versionTime__ = __version_time__\n __author__ = \"Paul McGuire <ptmcg.gm+pyparsing@gmail.com>\"\n@@ -131,9 +131,9 @@ def __repr__(self):\n from .actions import *\n from .core import __diag__, __compat__\n from .results import *\n-from .core import * # type: ignore[misc, assignment]\n+from .core import *\n from .core import _builtin_exprs as core_builtin_exprs\n-from .helpers import * # type: ignore[misc, assignment]\n+from .helpers import *\n from .helpers import _builtin_exprs as helper_builtin_exprs\n \n from .unicode import unicode_set, UnicodeRangeList, pyparsing_unicode as unicode\n@@ -143,13 +143,13 @@ def __repr__(self):\n _builtin_exprs as common_builtin_exprs,\n )\n \n-# define backward compat synonyms\n+# Compatibility synonyms\n if \"pyparsing_unicode\" not in globals():\n pyparsing_unicode = unicode # type: ignore[misc]\n if \"pyparsing_common\" not in globals():\n- pyparsing_common = common # type: ignore[misc]\n+ pyparsing_common = common\n if \"pyparsing_test\" not in globals():\n- pyparsing_test = testing # type: ignore[misc]\n+ pyparsing_test = testing\n \n core_builtin_exprs += common_builtin_exprs + helper_builtin_exprs\n \n@@ -208,6 +208,7 @@ def __repr__(self):\n \"StringEnd\",\n \"StringStart\",\n \"Suppress\",\n+ \"Tag\",\n \"Token\",\n \"TokenConverter\",\n \"White\","}, {"sha": "1d2dce99e199f68c8a34c31b468d9aa3e1e3c023", "filename": "lib/pyparsing/actions.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpyparsing%2Factions.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpyparsing%2Factions.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpyparsing%2Factions.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -196,7 +196,7 @@ def with_class(classname, namespace=\"\"):\n return with_attribute(**{classattr: classname})\n \n \n-# pre-PEP8 compatibility symbols\n+# Compatibility synonyms\n # fmt: off\n replaceWith = replaced_by_pep8(\"replaceWith\", replace_with)\n removeQuotes = replaced_by_pep8(\"removeQuotes\", remove_quotes)"}, {"sha": "649aad009617467e066437eb816bd45d2928785a", "filename": "lib/pyparsing/common.py", "status": "modified", "additions": 9, "deletions": 14, "changes": 23, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpyparsing%2Fcommon.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpyparsing%2Fcommon.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpyparsing%2Fcommon.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -418,20 +418,15 @@ def strip_html_tags(s: str, l: int, tokens: ParseResults):\n # fmt: on\n \n # pre-PEP8 compatibility names\n- convertToInteger = convert_to_integer\n- \"\"\"Deprecated - use :class:`convert_to_integer`\"\"\"\n- convertToFloat = convert_to_float\n- \"\"\"Deprecated - use :class:`convert_to_float`\"\"\"\n- convertToDate = convert_to_date\n- \"\"\"Deprecated - use :class:`convert_to_date`\"\"\"\n- convertToDatetime = convert_to_datetime\n- \"\"\"Deprecated - use :class:`convert_to_datetime`\"\"\"\n- stripHTMLTags = strip_html_tags\n- \"\"\"Deprecated - use :class:`strip_html_tags`\"\"\"\n- upcaseTokens = upcase_tokens\n- \"\"\"Deprecated - use :class:`upcase_tokens`\"\"\"\n- downcaseTokens = downcase_tokens\n- \"\"\"Deprecated - use :class:`downcase_tokens`\"\"\"\n+ # fmt: off\n+ convertToInteger = staticmethod(replaced_by_pep8(\"convertToInteger\", convert_to_integer))\n+ convertToFloat = staticmethod(replaced_by_pep8(\"convertToFloat\", convert_to_float))\n+ convertToDate = staticmethod(replaced_by_pep8(\"convertToDate\", convert_to_date))\n+ convertToDatetime = staticmethod(replaced_by_pep8(\"convertToDatetime\", convert_to_datetime))\n+ stripHTMLTags = staticmethod(replaced_by_pep8(\"stripHTMLTags\", strip_html_tags))\n+ upcaseTokens = staticmethod(replaced_by_pep8(\"upcaseTokens\", upcase_tokens))\n+ downcaseTokens = staticmethod(replaced_by_pep8(\"downcaseTokens\", downcase_tokens))\n+ # fmt: on\n \n \n _builtin_exprs = ["}, {"sha": "4f43c3bf991cc1cd7c444e5a1878e5870e0d2244", "filename": "lib/pyparsing/core.py", "status": "modified", "additions": 451, "deletions": 311, "changes": 762, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpyparsing%2Fcore.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpyparsing%2Fcore.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpyparsing%2Fcore.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,20 +1,19 @@\n #\n # core.py\n #\n+from __future__ import annotations\n \n+import collections.abc\n from collections import deque\n import os\n import typing\n from typing import (\n Any,\n Callable,\n Generator,\n- List,\n NamedTuple,\n Sequence,\n- Set,\n TextIO,\n- Tuple,\n Union,\n cast,\n )\n@@ -51,7 +50,7 @@\n from .unicode import pyparsing_unicode\n \n _MAX_INT = sys.maxsize\n-str_type: Tuple[type, ...] = (str, bytes)\n+str_type: tuple[type, ...] = (str, bytes)\n \n #\n # Copyright (c) 2003-2022 Paul T. McGuire\n@@ -76,18 +75,7 @@\n # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n #\n \n-\n-if sys.version_info >= (3, 8):\n- from functools import cached_property\n-else:\n-\n- class cached_property:\n- def __init__(self, func):\n- self._func = func\n-\n- def __get__(self, instance, owner=None):\n- ret = instance.__dict__[self._func.__name__] = self._func(instance)\n- return ret\n+from functools import cached_property\n \n \n class __compat__(__config_flags):\n@@ -218,22 +206,14 @@ def _should_enable_warnings(\n \n \n # build list of single arg builtins, that can be used as parse actions\n+# fmt: off\n _single_arg_builtins = {\n- sum,\n- len,\n- sorted,\n- reversed,\n- list,\n- tuple,\n- set,\n- any,\n- all,\n- min,\n- max,\n+ sum, len, sorted, reversed, list, tuple, set, any, all, min, max\n }\n+# fmt: on\n \n _generatorType = types.GeneratorType\n-ParseImplReturnType = Tuple[int, Any]\n+ParseImplReturnType = tuple[int, Any]\n PostParseReturnType = Union[ParseResults, Sequence[ParseResults]]\n ParseAction = Union[\n Callable[[], Any],\n@@ -255,20 +235,34 @@ def _should_enable_warnings(\n DebugExceptionAction = Callable[[str, int, \"ParserElement\", Exception, bool], None]\n \n \n-alphas = string.ascii_uppercase + string.ascii_lowercase\n-identchars = pyparsing_unicode.Latin1.identchars\n-identbodychars = pyparsing_unicode.Latin1.identbodychars\n-nums = \"0123456789\"\n-hexnums = nums + \"ABCDEFabcdef\"\n-alphanums = alphas + nums\n-printables = \"\".join([c for c in string.printable if c not in string.whitespace])\n+alphas: str = string.ascii_uppercase + string.ascii_lowercase\n+identchars: str = pyparsing_unicode.Latin1.identchars\n+identbodychars: str = pyparsing_unicode.Latin1.identbodychars\n+nums: str = \"0123456789\"\n+hexnums: str = nums + \"ABCDEFabcdef\"\n+alphanums: str = alphas + nums\n+printables: str = \"\".join([c for c in string.printable if c not in string.whitespace])\n+\n+\n+class _ParseActionIndexError(Exception):\n+ \"\"\"\n+ Internal wrapper around IndexError so that IndexErrors raised inside\n+ parse actions aren't misinterpreted as IndexErrors raised inside\n+ ParserElement parseImpl methods.\n+ \"\"\"\n+\n+ def __init__(self, msg: str, exc: BaseException):\n+ self.msg: str = msg\n+ self.exc: BaseException = exc\n+\n \n _trim_arity_call_line: traceback.StackSummary = None # type: ignore[assignment]\n+pa_call_line_synth = ()\n \n \n def _trim_arity(func, max_limit=3):\n \"\"\"decorator to trim function calls to match the arity of the target\"\"\"\n- global _trim_arity_call_line\n+ global _trim_arity_call_line, pa_call_line_synth\n \n if func in _single_arg_builtins:\n return lambda s, l, t: func(t)\n@@ -280,14 +274,16 @@ def _trim_arity(func, max_limit=3):\n # user's parse action 'func', so that we don't incur call penalty at parse time\n \n # fmt: off\n- LINE_DIFF = 7\n+ LINE_DIFF = 9\n # IF ANY CODE CHANGES, EVEN JUST COMMENTS OR BLANK LINES, BETWEEN THE NEXT LINE AND\n # THE CALL TO FUNC INSIDE WRAPPER, LINE_DIFF MUST BE MODIFIED!!!!\n- _trim_arity_call_line = (_trim_arity_call_line or traceback.extract_stack(limit=2)[-1])\n- pa_call_line_synth = (_trim_arity_call_line[0], _trim_arity_call_line[1] + LINE_DIFF)\n+ _trim_arity_call_line = _trim_arity_call_line or traceback.extract_stack(limit=2)[-1]\n+ pa_call_line_synth = pa_call_line_synth or (_trim_arity_call_line[0], _trim_arity_call_line[1] + LINE_DIFF)\n \n def wrapper(*args):\n nonlocal found_arity, limit\n+ if found_arity:\n+ return func(*args[limit:])\n while 1:\n try:\n ret = func(*args[limit:])\n@@ -312,6 +308,11 @@ def wrapper(*args):\n continue\n \n raise\n+ except IndexError as ie:\n+ # wrap IndexErrors inside a _ParseActionIndexError\n+ raise _ParseActionIndexError(\n+ \"IndexError raised in parse action\", ie\n+ ).with_traceback(None)\n # fmt: on\n \n # copy func name to wrapper for sensible debug output\n@@ -352,7 +353,7 @@ def pa(s, l, t):\n \n \n def _default_start_debug_action(\n- instring: str, loc: int, expr: \"ParserElement\", cache_hit: bool = False\n+ instring: str, loc: int, expr: ParserElement, cache_hit: bool = False\n ):\n cache_hit_str = \"*\" if cache_hit else \"\"\n print(\n@@ -368,7 +369,7 @@ def _default_success_debug_action(\n instring: str,\n startloc: int,\n endloc: int,\n- expr: \"ParserElement\",\n+ expr: ParserElement,\n toks: ParseResults,\n cache_hit: bool = False,\n ):\n@@ -379,7 +380,7 @@ def _default_success_debug_action(\n def _default_exception_debug_action(\n instring: str,\n loc: int,\n- expr: \"ParserElement\",\n+ expr: ParserElement,\n exc: Exception,\n cache_hit: bool = False,\n ):\n@@ -444,7 +445,7 @@ def inline_literals_using(cls: type) -> None:\n @classmethod\n def using_each(cls, seq, **class_kwargs):\n \"\"\"\n- Yields a sequence of class(obj, **class_kwargs) for obj in seq.\n+ Yields a sequence of ``class(obj, **class_kwargs)`` for obj in seq.\n \n Example::\n \n@@ -459,7 +460,7 @@ class DebugActions(NamedTuple):\n debug_fail: typing.Optional[DebugExceptionAction]\n \n def __init__(self, savelist: bool = False):\n- self.parseAction: List[ParseAction] = list()\n+ self.parseAction: list[ParseAction] = list()\n self.failAction: typing.Optional[ParseFailAction] = None\n self.customName: str = None # type: ignore[assignment]\n self._defaultName: typing.Optional[str] = None\n@@ -471,22 +472,22 @@ def __init__(self, savelist: bool = False):\n # used when checking for left-recursion\n self.mayReturnEmpty = False\n self.keepTabs = False\n- self.ignoreExprs: List[\"ParserElement\"] = list()\n+ self.ignoreExprs: list[ParserElement] = list()\n self.debug = False\n self.streamlined = False\n # optimize exception handling for subclasses that don't advance parse index\n self.mayIndexError = True\n- self.errmsg = \"\"\n+ self.errmsg: Union[str, None] = \"\"\n # mark results names as modal (report only last) or cumulative (list all)\n self.modalResults = True\n # custom debug actions\n self.debugActions = self.DebugActions(None, None, None)\n # avoid redundant calls to preParse\n self.callPreparse = True\n self.callDuringTry = False\n- self.suppress_warnings_: List[Diagnostics] = []\n+ self.suppress_warnings_: list[Diagnostics] = []\n \n- def suppress_warning(self, warning_type: Diagnostics) -> \"ParserElement\":\n+ def suppress_warning(self, warning_type: Diagnostics) -> ParserElement:\n \"\"\"\n Suppress warnings emitted for a particular diagnostic on this expression.\n \n@@ -519,7 +520,7 @@ def visit_all(self):\n to_visit.extend(cur.recurse())\n yield cur\n \n- def copy(self) -> \"ParserElement\":\n+ def copy(self) -> ParserElement:\n \"\"\"\n Make a copy of this :class:`ParserElement`. Useful for defining\n different parse actions for the same parsing pattern, using copies of\n@@ -550,7 +551,7 @@ def copy(self) -> \"ParserElement\":\n \n def set_results_name(\n self, name: str, list_all_matches: bool = False, *, listAllMatches: bool = False\n- ) -> \"ParserElement\":\n+ ) -> ParserElement:\n \"\"\"\n Define name for referencing matching tokens as a nested attribute\n of the returned parse results.\n@@ -582,18 +583,18 @@ def set_results_name(\n listAllMatches = listAllMatches or list_all_matches\n return self._setResultsName(name, listAllMatches)\n \n- def _setResultsName(self, name, listAllMatches=False):\n+ def _setResultsName(self, name, list_all_matches=False) -> ParserElement:\n if name is None:\n return self\n newself = self.copy()\n if name.endswith(\"*\"):\n name = name[:-1]\n- listAllMatches = True\n+ list_all_matches = True\n newself.resultsName = name\n- newself.modalResults = not listAllMatches\n+ newself.modalResults = not list_all_matches\n return newself\n \n- def set_break(self, break_flag: bool = True) -> \"ParserElement\":\n+ def set_break(self, break_flag: bool = True) -> ParserElement:\n \"\"\"\n Method to invoke the Python pdb debugger when this element is\n about to be parsed. Set ``break_flag`` to ``True`` to enable, ``False`` to\n@@ -602,20 +603,18 @@ def set_break(self, break_flag: bool = True) -> \"ParserElement\":\n if break_flag:\n _parseMethod = self._parse\n \n- def breaker(instring, loc, doActions=True, callPreParse=True):\n- import pdb\n-\n- # this call to pdb.set_trace() is intentional, not a checkin error\n- pdb.set_trace()\n- return _parseMethod(instring, loc, doActions, callPreParse)\n+ def breaker(instring, loc, do_actions=True, callPreParse=True):\n+ # this call to breakpoint() is intentional, not a checkin error\n+ breakpoint()\n+ return _parseMethod(instring, loc, do_actions, callPreParse)\n \n breaker._originalParseMethod = _parseMethod # type: ignore [attr-defined]\n- self._parse = breaker # type: ignore [assignment]\n+ self._parse = breaker # type: ignore [method-assign]\n elif hasattr(self._parse, \"_originalParseMethod\"):\n- self._parse = self._parse._originalParseMethod # type: ignore [attr-defined, assignment]\n+ self._parse = self._parse._originalParseMethod # type: ignore [method-assign]\n return self\n \n- def set_parse_action(self, *fns: ParseAction, **kwargs) -> \"ParserElement\":\n+ def set_parse_action(self, *fns: ParseAction, **kwargs: Any) -> ParserElement:\n \"\"\"\n Define one or more actions to perform when successfully matching parse element definition.\n \n@@ -691,19 +690,19 @@ def is_valid_date(instring, loc, toks):\n ''')\n \"\"\"\n if list(fns) == [None]:\n- self.parseAction = []\n+ self.parseAction.clear()\n return self\n \n if not all(callable(fn) for fn in fns):\n raise TypeError(\"parse actions must be callable\")\n- self.parseAction = [_trim_arity(fn) for fn in fns]\n+ self.parseAction[:] = [_trim_arity(fn) for fn in fns]\n self.callDuringTry = kwargs.get(\n \"call_during_try\", kwargs.get(\"callDuringTry\", False)\n )\n \n return self\n \n- def add_parse_action(self, *fns: ParseAction, **kwargs) -> \"ParserElement\":\n+ def add_parse_action(self, *fns: ParseAction, **kwargs: Any) -> ParserElement:\n \"\"\"\n Add one or more parse actions to expression's list of parse actions. See :class:`set_parse_action`.\n \n@@ -715,7 +714,7 @@ def add_parse_action(self, *fns: ParseAction, **kwargs) -> \"ParserElement\":\n )\n return self\n \n- def add_condition(self, *fns: ParseCondition, **kwargs) -> \"ParserElement\":\n+ def add_condition(self, *fns: ParseCondition, **kwargs: Any) -> ParserElement:\n \"\"\"Add a boolean predicate function to expression's list of parse actions. See\n :class:`set_parse_action` for function call signatures. Unlike ``set_parse_action``,\n functions passed to ``add_condition`` need to return boolean success/fail of the condition.\n@@ -752,7 +751,7 @@ def add_condition(self, *fns: ParseCondition, **kwargs) -> \"ParserElement\":\n )\n return self\n \n- def set_fail_action(self, fn: ParseFailAction) -> \"ParserElement\":\n+ def set_fail_action(self, fn: ParseFailAction) -> ParserElement:\n \"\"\"\n Define action to perform if parsing fails at this expression.\n Fail acton fn is a callable function that takes the arguments\n@@ -801,18 +800,17 @@ def preParse(self, instring: str, loc: int) -> int:\n \n return loc\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n return loc, []\n \n def postParse(self, instring, loc, tokenlist):\n return tokenlist\n \n # @profile\n def _parseNoCache(\n- self, instring, loc, doActions=True, callPreParse=True\n- ) -> Tuple[int, ParseResults]:\n- TRY, MATCH, FAIL = 0, 1, 2\n- debugging = self.debug # and doActions)\n+ self, instring, loc, do_actions=True, callPreParse=True\n+ ) -> tuple[int, ParseResults]:\n+ debugging = self.debug # and do_actions)\n len_instring = len(instring)\n \n if debugging or self.failAction:\n@@ -827,11 +825,11 @@ def _parseNoCache(\n self.debugActions.debug_try(instring, tokens_start, self, False)\n if self.mayIndexError or pre_loc >= len_instring:\n try:\n- loc, tokens = self.parseImpl(instring, pre_loc, doActions)\n+ loc, tokens = self.parseImpl(instring, pre_loc, do_actions)\n except IndexError:\n raise ParseException(instring, len_instring, self.errmsg, self)\n else:\n- loc, tokens = self.parseImpl(instring, pre_loc, doActions)\n+ loc, tokens = self.parseImpl(instring, pre_loc, do_actions)\n except Exception as err:\n # print(\"Exception raised:\", err)\n if self.debugActions.debug_fail:\n@@ -849,18 +847,18 @@ def _parseNoCache(\n tokens_start = pre_loc\n if self.mayIndexError or pre_loc >= len_instring:\n try:\n- loc, tokens = self.parseImpl(instring, pre_loc, doActions)\n+ loc, tokens = self.parseImpl(instring, pre_loc, do_actions)\n except IndexError:\n raise ParseException(instring, len_instring, self.errmsg, self)\n else:\n- loc, tokens = self.parseImpl(instring, pre_loc, doActions)\n+ loc, tokens = self.parseImpl(instring, pre_loc, do_actions)\n \n tokens = self.postParse(instring, loc, tokens)\n \n ret_tokens = ParseResults(\n tokens, self.resultsName, asList=self.saveAsList, modal=self.modalResults\n )\n- if self.parseAction and (doActions or self.callDuringTry):\n+ if self.parseAction and (do_actions or self.callDuringTry):\n if debugging:\n try:\n for fn in self.parseAction:\n@@ -919,7 +917,7 @@ def try_parse(\n do_actions: bool = False,\n ) -> int:\n try:\n- return self._parse(instring, loc, doActions=do_actions)[0]\n+ return self._parse(instring, loc, do_actions=do_actions)[0]\n except ParseFatalException:\n if raise_fatal:\n raise\n@@ -935,43 +933,59 @@ def can_parse_next(self, instring: str, loc: int, do_actions: bool = False) -> b\n \n # cache for left-recursion in Forward references\n recursion_lock = RLock()\n- recursion_memos: typing.Dict[\n- Tuple[int, \"Forward\", bool], Tuple[int, Union[ParseResults, Exception]]\n+ recursion_memos: collections.abc.MutableMapping[\n+ tuple[int, Forward, bool], tuple[int, Union[ParseResults, Exception]]\n ] = {}\n \n- class _CacheType(dict):\n+ class _CacheType(typing.Protocol):\n \"\"\"\n- class to help type checking\n+ Class to be used for packrat and left-recursion cacheing of results\n+ and exceptions.\n \"\"\"\n \n not_in_cache: bool\n \n- def get(self, *args): ...\n+ def get(self, *args) -> typing.Any: ...\n+\n+ def set(self, *args) -> None: ...\n+\n+ def clear(self) -> None: ...\n+\n+ class NullCache(dict):\n+ \"\"\"\n+ A null cache type for initialization of the packrat_cache class variable.\n+ If/when enable_packrat() is called, this null cache will be replaced by a\n+ proper _CacheType class instance.\n+ \"\"\"\n+\n+ not_in_cache: bool = True\n+\n+ def get(self, *args) -> typing.Any: ...\n \n- def set(self, *args): ...\n+ def set(self, *args) -> None: ...\n \n- # argument cache for optimizing repeated calls when backtracking through recursive expressions\n- packrat_cache = (\n- _CacheType()\n- ) # set later by enable_packrat(); this is here so that reset_cache() doesn't fail\n+ def clear(self) -> None: ...\n+\n+ # class-level argument cache for optimizing repeated calls when backtracking\n+ # through recursive expressions\n+ packrat_cache: _CacheType = NullCache()\n packrat_cache_lock = RLock()\n packrat_cache_stats = [0, 0]\n \n # this method gets repeatedly called during backtracking with the same arguments -\n # we can cache these arguments and save ourselves the trouble of re-parsing the contained expression\n def _parseCache(\n- self, instring, loc, doActions=True, callPreParse=True\n- ) -> Tuple[int, ParseResults]:\n+ self, instring, loc, do_actions=True, callPreParse=True\n+ ) -> tuple[int, ParseResults]:\n HIT, MISS = 0, 1\n- TRY, MATCH, FAIL = 0, 1, 2\n- lookup = (self, instring, loc, callPreParse, doActions)\n+ lookup = (self, instring, loc, callPreParse, do_actions)\n with ParserElement.packrat_cache_lock:\n cache = ParserElement.packrat_cache\n value = cache.get(lookup)\n if value is cache.not_in_cache:\n ParserElement.packrat_cache_stats[MISS] += 1\n try:\n- value = self._parseNoCache(instring, loc, doActions, callPreParse)\n+ value = self._parseNoCache(instring, loc, do_actions, callPreParse)\n except ParseBaseException as pe:\n # cache a copy of the exception, without the traceback\n cache.set(lookup, pe.__class__(*pe.args))\n@@ -996,7 +1010,7 @@ def _parseCache(\n pass\n raise value\n \n- value = cast(Tuple[int, ParseResults, int], value)\n+ value = cast(tuple[int, ParseResults, int], value)\n loc_, result, endloc = value[0], value[1].copy(), value[2]\n if self.debug and self.debugActions.debug_match:\n try:\n@@ -1076,7 +1090,7 @@ def enable_left_recursion(\n elif ParserElement._packratEnabled:\n raise RuntimeError(\"Packrat and Bounded Recursion are not compatible\")\n if cache_size_limit is None:\n- ParserElement.recursion_memos = _UnboundedMemo() # type: ignore[assignment]\n+ ParserElement.recursion_memos = _UnboundedMemo()\n elif cache_size_limit > 0:\n ParserElement.recursion_memos = _LRUMemo(capacity=cache_size_limit) # type: ignore[assignment]\n else:\n@@ -1129,7 +1143,7 @@ def enable_packrat(\n if cache_size_limit is None:\n ParserElement.packrat_cache = _UnboundedCache()\n else:\n- ParserElement.packrat_cache = _FifoCache(cache_size_limit) # type: ignore[assignment]\n+ ParserElement.packrat_cache = _FifoCache(cache_size_limit)\n ParserElement._parse = ParserElement._parseCache\n \n def parse_string(\n@@ -1190,14 +1204,16 @@ def parse_string(\n loc, tokens = self._parse(instring, 0)\n if parseAll:\n loc = self.preParse(instring, loc)\n- se = Empty() + StringEnd()\n+ se = Empty() + StringEnd().set_debug(False)\n se._parse(instring, loc)\n+ except _ParseActionIndexError as pa_exc:\n+ raise pa_exc.exc\n except ParseBaseException as exc:\n if ParserElement.verbose_stacktrace:\n raise\n- else:\n- # catch and re-raise exception from here, clearing out pyparsing internal stack trace\n- raise exc.with_traceback(None)\n+\n+ # catch and re-raise exception from here, clearing out pyparsing internal stack trace\n+ raise exc.with_traceback(None)\n else:\n return tokens\n \n@@ -1206,10 +1222,11 @@ def scan_string(\n instring: str,\n max_matches: int = _MAX_INT,\n overlap: bool = False,\n+ always_skip_whitespace=True,\n *,\n debug: bool = False,\n maxMatches: int = _MAX_INT,\n- ) -> Generator[Tuple[ParseResults, int, int], None, None]:\n+ ) -> Generator[tuple[ParseResults, int, int], None, None]:\n \"\"\"\n Scan the input string for expression matches. Each match will return the\n matching tokens, start location, and end location. May be called with optional\n@@ -1250,7 +1267,13 @@ def scan_string(\n instring = str(instring).expandtabs()\n instrlen = len(instring)\n loc = 0\n- preparseFn = self.preParse\n+ if always_skip_whitespace:\n+ preparser = Empty()\n+ preparser.ignoreExprs = self.ignoreExprs\n+ preparser.whiteChars = self.whiteChars\n+ preparseFn = preparser.preParse\n+ else:\n+ preparseFn = self.preParse\n parseFn = self._parse\n ParserElement.resetCache()\n matches = 0\n@@ -1312,14 +1335,15 @@ def transform_string(self, instring: str, *, debug: bool = False) -> str:\n \n Now Is The Winter Of Our Discontent Made Glorious Summer By This Sun Of York.\n \"\"\"\n- out: List[str] = []\n+ out: list[str] = []\n lastE = 0\n # force preservation of <TAB>s, to minimize unwanted transformation of string, and to\n # keep string locs straight between transform_string and scan_string\n self.keepTabs = True\n try:\n for t, s, e in self.scan_string(instring, debug=debug):\n- out.append(instring[lastE:s])\n+ if s > lastE:\n+ out.append(instring[lastE:s])\n lastE = e\n \n if not t:\n@@ -1373,7 +1397,12 @@ def search_string(\n maxMatches = min(maxMatches, max_matches)\n try:\n return ParseResults(\n- [t for t, s, e in self.scan_string(instring, maxMatches, debug=debug)]\n+ [\n+ t\n+ for t, s, e in self.scan_string(\n+ instring, maxMatches, always_skip_whitespace=False, debug=debug\n+ )\n+ ]\n )\n except ParseBaseException as exc:\n if ParserElement.verbose_stacktrace:\n@@ -1414,7 +1443,7 @@ def split(\n last = e\n yield instring[last:]\n \n- def __add__(self, other) -> \"ParserElement\":\n+ def __add__(self, other) -> ParserElement:\n \"\"\"\n Implementation of ``+`` operator - returns :class:`And`. Adding strings to a :class:`ParserElement`\n converts them to :class:`Literal`\\\\ s by default.\n@@ -1450,7 +1479,7 @@ def __add__(self, other) -> \"ParserElement\":\n return NotImplemented\n return And([self, other])\n \n- def __radd__(self, other) -> \"ParserElement\":\n+ def __radd__(self, other) -> ParserElement:\n \"\"\"\n Implementation of ``+`` operator when left operand is not a :class:`ParserElement`\n \"\"\"\n@@ -1463,7 +1492,7 @@ def __radd__(self, other) -> \"ParserElement\":\n return NotImplemented\n return other + self\n \n- def __sub__(self, other) -> \"ParserElement\":\n+ def __sub__(self, other) -> ParserElement:\n \"\"\"\n Implementation of ``-`` operator, returns :class:`And` with error stop\n \"\"\"\n@@ -1473,7 +1502,7 @@ def __sub__(self, other) -> \"ParserElement\":\n return NotImplemented\n return self + And._ErrorStop() + other\n \n- def __rsub__(self, other) -> \"ParserElement\":\n+ def __rsub__(self, other) -> ParserElement:\n \"\"\"\n Implementation of ``-`` operator when left operand is not a :class:`ParserElement`\n \"\"\"\n@@ -1483,7 +1512,7 @@ def __rsub__(self, other) -> \"ParserElement\":\n return NotImplemented\n return other - self\n \n- def __mul__(self, other) -> \"ParserElement\":\n+ def __mul__(self, other) -> ParserElement:\n \"\"\"\n Implementation of ``*`` operator, allows use of ``expr * 3`` in place of\n ``expr + expr + expr``. Expressions may also be multiplied by a 2-integer\n@@ -1563,10 +1592,10 @@ def makeOptionalList(n):\n ret = And([self] * minElements)\n return ret\n \n- def __rmul__(self, other) -> \"ParserElement\":\n+ def __rmul__(self, other) -> ParserElement:\n return self.__mul__(other)\n \n- def __or__(self, other) -> \"ParserElement\":\n+ def __or__(self, other) -> ParserElement:\n \"\"\"\n Implementation of ``|`` operator - returns :class:`MatchFirst`\n \"\"\"\n@@ -1582,7 +1611,7 @@ def __or__(self, other) -> \"ParserElement\":\n return NotImplemented\n return MatchFirst([self, other])\n \n- def __ror__(self, other) -> \"ParserElement\":\n+ def __ror__(self, other) -> ParserElement:\n \"\"\"\n Implementation of ``|`` operator when left operand is not a :class:`ParserElement`\n \"\"\"\n@@ -1592,7 +1621,7 @@ def __ror__(self, other) -> \"ParserElement\":\n return NotImplemented\n return other | self\n \n- def __xor__(self, other) -> \"ParserElement\":\n+ def __xor__(self, other) -> ParserElement:\n \"\"\"\n Implementation of ``^`` operator - returns :class:`Or`\n \"\"\"\n@@ -1602,7 +1631,7 @@ def __xor__(self, other) -> \"ParserElement\":\n return NotImplemented\n return Or([self, other])\n \n- def __rxor__(self, other) -> \"ParserElement\":\n+ def __rxor__(self, other) -> ParserElement:\n \"\"\"\n Implementation of ``^`` operator when left operand is not a :class:`ParserElement`\n \"\"\"\n@@ -1612,7 +1641,7 @@ def __rxor__(self, other) -> \"ParserElement\":\n return NotImplemented\n return other ^ self\n \n- def __and__(self, other) -> \"ParserElement\":\n+ def __and__(self, other) -> ParserElement:\n \"\"\"\n Implementation of ``&`` operator - returns :class:`Each`\n \"\"\"\n@@ -1622,7 +1651,7 @@ def __and__(self, other) -> \"ParserElement\":\n return NotImplemented\n return Each([self, other])\n \n- def __rand__(self, other) -> \"ParserElement\":\n+ def __rand__(self, other) -> ParserElement:\n \"\"\"\n Implementation of ``&`` operator when left operand is not a :class:`ParserElement`\n \"\"\"\n@@ -1632,7 +1661,7 @@ def __rand__(self, other) -> \"ParserElement\":\n return NotImplemented\n return other & self\n \n- def __invert__(self) -> \"ParserElement\":\n+ def __invert__(self) -> ParserElement:\n \"\"\"\n Implementation of ``~`` operator - returns :class:`NotAny`\n \"\"\"\n@@ -1702,7 +1731,7 @@ def __getitem__(self, key):\n \n return ret\n \n- def __call__(self, name: typing.Optional[str] = None) -> \"ParserElement\":\n+ def __call__(self, name: typing.Optional[str] = None) -> ParserElement:\n \"\"\"\n Shortcut for :class:`set_results_name`, with ``list_all_matches=False``.\n \n@@ -1722,14 +1751,14 @@ def __call__(self, name: typing.Optional[str] = None) -> \"ParserElement\":\n \n return self.copy()\n \n- def suppress(self) -> \"ParserElement\":\n+ def suppress(self) -> ParserElement:\n \"\"\"\n Suppresses the output of this :class:`ParserElement`; useful to keep punctuation from\n cluttering up returned output.\n \"\"\"\n return Suppress(self)\n \n- def ignore_whitespace(self, recursive: bool = True) -> \"ParserElement\":\n+ def ignore_whitespace(self, recursive: bool = True) -> ParserElement:\n \"\"\"\n Enables the skipping of whitespace before matching the characters in the\n :class:`ParserElement`'s defined pattern.\n@@ -1739,7 +1768,7 @@ def ignore_whitespace(self, recursive: bool = True) -> \"ParserElement\":\n self.skipWhitespace = True\n return self\n \n- def leave_whitespace(self, recursive: bool = True) -> \"ParserElement\":\n+ def leave_whitespace(self, recursive: bool = True) -> ParserElement:\n \"\"\"\n Disables the skipping of whitespace before matching the characters in the\n :class:`ParserElement`'s defined pattern. This is normally only used internally by\n@@ -1751,8 +1780,8 @@ def leave_whitespace(self, recursive: bool = True) -> \"ParserElement\":\n return self\n \n def set_whitespace_chars(\n- self, chars: Union[Set[str], str], copy_defaults: bool = False\n- ) -> \"ParserElement\":\n+ self, chars: Union[set[str], str], copy_defaults: bool = False\n+ ) -> ParserElement:\n \"\"\"\n Overrides the default whitespace chars\n \"\"\"\n@@ -1761,7 +1790,7 @@ def set_whitespace_chars(\n self.copyDefaultWhiteChars = copy_defaults\n return self\n \n- def parse_with_tabs(self) -> \"ParserElement\":\n+ def parse_with_tabs(self) -> ParserElement:\n \"\"\"\n Overrides default behavior to expand ``<TAB>`` s to spaces before parsing the input string.\n Must be called before ``parse_string`` when the input grammar contains elements that\n@@ -1770,7 +1799,7 @@ def parse_with_tabs(self) -> \"ParserElement\":\n self.keepTabs = True\n return self\n \n- def ignore(self, other: \"ParserElement\") -> \"ParserElement\":\n+ def ignore(self, other: ParserElement) -> ParserElement:\n \"\"\"\n Define expression to be ignored (e.g., comments) while doing pattern\n matching; may be called repeatedly, to define multiple comment or other\n@@ -1801,7 +1830,7 @@ def set_debug_actions(\n start_action: DebugStartAction,\n success_action: DebugSuccessAction,\n exception_action: DebugExceptionAction,\n- ) -> \"ParserElement\":\n+ ) -> ParserElement:\n \"\"\"\n Customize display of debugging messages while doing pattern matching:\n \n@@ -1822,7 +1851,7 @@ def set_debug_actions(\n self.debug = True\n return self\n \n- def set_debug(self, flag: bool = True, recurse: bool = False) -> \"ParserElement\":\n+ def set_debug(self, flag: bool = True, recurse: bool = False) -> ParserElement:\n \"\"\"\n Enable display of debugging messages while doing pattern matching.\n Set ``flag`` to ``True`` to enable, ``False`` to disable.\n@@ -1887,9 +1916,14 @@ def _generateDefaultName(self) -> str:\n Child classes must define this method, which defines how the ``default_name`` is set.\n \"\"\"\n \n- def set_name(self, name: str) -> \"ParserElement\":\n+ def set_name(self, name: typing.Optional[str]) -> ParserElement:\n \"\"\"\n- Define name for this expression, makes debugging and exception messages clearer.\n+ Define name for this expression, makes debugging and exception messages clearer. If\n+ `__diag__.enable_debug_on_named_expressions` is set to True, setting a name will also\n+ enable debug for this expression.\n+\n+ If `name` is None, clears any custom name for this expression, and clears the\n+ debug flag is it was enabled via `__diag__.enable_debug_on_named_expressions`.\n \n Example::\n \n@@ -1899,29 +1933,35 @@ def set_name(self, name: str) -> \"ParserElement\":\n integer.set_name(\"integer\")\n integer.parse_string(\"ABC\") # -> Exception: Expected integer (at char 0), (line:1, col:1)\n \"\"\"\n- self.customName = name\n- self.errmsg = f\"Expected {self.name}\"\n+ self.customName = name # type: ignore[assignment]\n+ self.errmsg = f\"Expected {str(self)}\"\n+\n if __diag__.enable_debug_on_named_expressions:\n- self.set_debug()\n+ self.set_debug(name is not None)\n+\n return self\n \n @property\n def name(self) -> str:\n # This will use a user-defined name if available, but otherwise defaults back to the auto-generated name\n return self.customName if self.customName is not None else self.default_name\n \n+ @name.setter\n+ def name(self, new_name) -> None:\n+ self.set_name(new_name)\n+\n def __str__(self) -> str:\n return self.name\n \n def __repr__(self) -> str:\n return str(self)\n \n- def streamline(self) -> \"ParserElement\":\n+ def streamline(self) -> ParserElement:\n self.streamlined = True\n self._defaultName = None\n return self\n \n- def recurse(self) -> List[\"ParserElement\"]:\n+ def recurse(self) -> list[ParserElement]:\n return []\n \n def _checkRecursion(self, parseElementList):\n@@ -2008,22 +2048,26 @@ def matches(\n \n def run_tests(\n self,\n- tests: Union[str, List[str]],\n+ tests: Union[str, list[str]],\n parse_all: bool = True,\n- comment: typing.Optional[Union[\"ParserElement\", str]] = \"#\",\n+ comment: typing.Optional[Union[ParserElement, str]] = \"#\",\n full_dump: bool = True,\n print_results: bool = True,\n failure_tests: bool = False,\n- post_parse: typing.Optional[Callable[[str, ParseResults], str]] = None,\n+ post_parse: typing.Optional[\n+ Callable[[str, ParseResults], typing.Optional[str]]\n+ ] = None,\n file: typing.Optional[TextIO] = None,\n with_line_numbers: bool = False,\n *,\n parseAll: bool = True,\n fullDump: bool = True,\n printResults: bool = True,\n failureTests: bool = False,\n- postParse: typing.Optional[Callable[[str, ParseResults], str]] = None,\n- ) -> Tuple[bool, List[Tuple[str, Union[ParseResults, Exception]]]]:\n+ postParse: typing.Optional[\n+ Callable[[str, ParseResults], typing.Optional[str]]\n+ ] = None,\n+ ) -> tuple[bool, list[tuple[str, Union[ParseResults, Exception]]]]:\n \"\"\"\n Execute the parse expression on a series of test strings, showing each\n test, the parsed results or where the parse failed. Quick and easy way to\n@@ -2141,8 +2185,8 @@ def run_tests(\n print_ = file.write\n \n result: Union[ParseResults, Exception]\n- allResults: List[Tuple[str, Union[ParseResults, Exception]]] = []\n- comments: List[str] = []\n+ allResults: list[tuple[str, Union[ParseResults, Exception]]] = []\n+ comments: list[str] = []\n success = True\n NL = Literal(r\"\\n\").add_parse_action(replace_with(\"\\n\")).ignore(quoted_string)\n BOM = \"\\ufeff\"\n@@ -2159,7 +2203,7 @@ def run_tests(\n f\"{nlstr}{nlstr.join(comments) if comments else ''}\",\n pyparsing_test.with_line_numbers(t) if with_line_numbers else t,\n ]\n- comments = []\n+ comments.clear()\n try:\n # convert newline marks to actual newlines, and strip leading BOM if present\n t = NL.transform_string(t.lstrip(BOM))\n@@ -2173,7 +2217,18 @@ def run_tests(\n success = success and failureTests\n result = pe\n except Exception as exc:\n- out.append(f\"FAIL-EXCEPTION: {type(exc).__name__}: {exc}\")\n+ tag = \"FAIL-EXCEPTION\"\n+\n+ # see if this exception was raised in a parse action\n+ tb = exc.__traceback__\n+ it = iter(traceback.walk_tb(tb))\n+ for f, line in it:\n+ if (f.f_code.co_filename, line) == pa_call_line_synth:\n+ next_f = next(it)[0]\n+ tag += f\" (raised in parse action {next_f.f_code.co_name!r})\"\n+ break\n+\n+ out.append(f\"{tag}: {type(exc).__name__}: {exc}\")\n if ParserElement.verbose_stacktrace:\n out.extend(traceback.format_tb(exc.__traceback__))\n success = success and failureTests\n@@ -2264,19 +2319,22 @@ def create_diagram(\n \n # Compatibility synonyms\n # fmt: off\n- inlineLiteralsUsing = replaced_by_pep8(\"inlineLiteralsUsing\", inline_literals_using)\n- setDefaultWhitespaceChars = replaced_by_pep8(\n+ inlineLiteralsUsing = staticmethod(replaced_by_pep8(\"inlineLiteralsUsing\", inline_literals_using))\n+ setDefaultWhitespaceChars = staticmethod(replaced_by_pep8(\n \"setDefaultWhitespaceChars\", set_default_whitespace_chars\n- )\n+ ))\n+ disableMemoization = staticmethod(replaced_by_pep8(\"disableMemoization\", disable_memoization))\n+ enableLeftRecursion = staticmethod(replaced_by_pep8(\"enableLeftRecursion\", enable_left_recursion))\n+ enablePackrat = staticmethod(replaced_by_pep8(\"enablePackrat\", enable_packrat))\n+ resetCache = staticmethod(replaced_by_pep8(\"resetCache\", reset_cache))\n+\n setResultsName = replaced_by_pep8(\"setResultsName\", set_results_name)\n setBreak = replaced_by_pep8(\"setBreak\", set_break)\n setParseAction = replaced_by_pep8(\"setParseAction\", set_parse_action)\n addParseAction = replaced_by_pep8(\"addParseAction\", add_parse_action)\n addCondition = replaced_by_pep8(\"addCondition\", add_condition)\n setFailAction = replaced_by_pep8(\"setFailAction\", set_fail_action)\n tryParse = replaced_by_pep8(\"tryParse\", try_parse)\n- enableLeftRecursion = replaced_by_pep8(\"enableLeftRecursion\", enable_left_recursion)\n- enablePackrat = replaced_by_pep8(\"enablePackrat\", enable_packrat)\n parseString = replaced_by_pep8(\"parseString\", parse_string)\n scanString = replaced_by_pep8(\"scanString\", scan_string)\n transformString = replaced_by_pep8(\"transformString\", transform_string)\n@@ -2290,8 +2348,7 @@ def create_diagram(\n setName = replaced_by_pep8(\"setName\", set_name)\n parseFile = replaced_by_pep8(\"parseFile\", parse_file)\n runTests = replaced_by_pep8(\"runTests\", run_tests)\n- canParseNext = can_parse_next\n- resetCache = reset_cache\n+ canParseNext = replaced_by_pep8(\"canParseNext\", can_parse_next)\n defaultName = default_name\n # fmt: on\n \n@@ -2307,7 +2364,7 @@ def __init__(self, expr: ParserElement, must_skip: bool = False):\n def _generateDefaultName(self) -> str:\n return str(self.anchor + Empty()).replace(\"Empty\", \"...\")\n \n- def __add__(self, other) -> \"ParserElement\":\n+ def __add__(self, other) -> ParserElement:\n skipper = SkipTo(other).set_name(\"...\")(\"_skipped*\")\n if self.must_skip:\n \n@@ -2331,7 +2388,7 @@ def show_skip(t):\n def __repr__(self):\n return self.defaultName\n \n- def parseImpl(self, *args):\n+ def parseImpl(self, *args) -> ParseImplReturnType:\n raise Exception(\n \"use of `...` expression without following SkipTo target expression\"\n )\n@@ -2360,7 +2417,7 @@ def __init__(self):\n self.mayIndexError = False\n self.errmsg = \"Unmatchable token\"\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n raise ParseException(instring, loc, self.errmsg, self)\n \n \n@@ -2409,7 +2466,7 @@ def __init__(self, match_string: str = \"\", *, matchString: str = \"\"):\n def _generateDefaultName(self) -> str:\n return repr(self.match)\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n if instring[loc] == self.firstMatchChar and instring.startswith(\n self.match, loc\n ):\n@@ -2430,12 +2487,12 @@ def __init__(self, match_string=\"\", *, matchString=\"\"):\n def _generateDefaultName(self) -> str:\n return \"Empty\"\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n return loc, []\n \n \n class _SingleCharLiteral(Literal):\n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n if instring[loc] == self.firstMatchChar:\n return loc + 1, self.match\n raise ParseException(instring, loc, self.errmsg, self)\n@@ -2489,9 +2546,8 @@ def __init__(\n match_string = matchString or match_string\n self.match = match_string\n self.matchLen = len(match_string)\n- try:\n- self.firstMatchChar = match_string[0]\n- except IndexError:\n+ self.firstMatchChar = match_string[:1]\n+ if not self.firstMatchChar:\n raise ValueError(\"null string passed to Keyword; use Empty() instead\")\n self.errmsg = f\"Expected {type(self).__name__} {self.name}\"\n self.mayReturnEmpty = False\n@@ -2505,8 +2561,8 @@ def __init__(\n def _generateDefaultName(self) -> str:\n return repr(self.match)\n \n- def parseImpl(self, instring, loc, doActions=True):\n- errmsg = self.errmsg\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n+ errmsg = self.errmsg or \"\"\n errloc = loc\n if self.caseless:\n if instring[loc : loc + self.matchLen].upper() == self.caselessmatch:\n@@ -2556,7 +2612,10 @@ def set_default_keyword_chars(chars) -> None:\n \"\"\"\n Keyword.DEFAULT_KEYWORD_CHARS = chars\n \n- setDefaultKeywordChars = set_default_keyword_chars\n+ # Compatibility synonyms\n+ setDefaultKeywordChars = staticmethod(\n+ replaced_by_pep8(\"setDefaultKeywordChars\", set_default_keyword_chars)\n+ )\n \n \n class CaselessLiteral(Literal):\n@@ -2580,7 +2639,7 @@ def __init__(self, match_string: str = \"\", *, matchString: str = \"\"):\n self.returnString = match_string\n self.errmsg = f\"Expected {self.name}\"\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n if instring[loc : loc + self.matchLen].upper() == self.match:\n return loc + self.matchLen, self.returnString\n raise ParseException(instring, loc, self.errmsg, self)\n@@ -2666,7 +2725,7 @@ def __init__(\n def _generateDefaultName(self) -> str:\n return f\"{type(self).__name__}:{self.match_string!r}\"\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n start = loc\n instrlen = len(instring)\n maxloc = start + len(self.match_string)\n@@ -2881,7 +2940,7 @@ def __init__(\n self.re = None # type: ignore[assignment]\n else:\n self.re_match = self.re.match\n- self.parseImpl = self.parseImpl_regex # type: ignore[assignment]\n+ self.parseImpl = self.parseImpl_regex # type: ignore[method-assign]\n \n def _generateDefaultName(self) -> str:\n def charsAsStr(s):\n@@ -2911,36 +2970,36 @@ def charsAsStr(s):\n return base + f\"{{{self.minLen},{self.maxLen}}}\"\n return base\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n if instring[loc] not in self.initChars:\n raise ParseException(instring, loc, self.errmsg, self)\n \n start = loc\n loc += 1\n instrlen = len(instring)\n- bodychars = self.bodyChars\n+ body_chars: set[str] = self.bodyChars\n maxloc = start + self.maxLen\n maxloc = min(maxloc, instrlen)\n- while loc < maxloc and instring[loc] in bodychars:\n+ while loc < maxloc and instring[loc] in body_chars:\n loc += 1\n \n- throwException = False\n+ throw_exception = False\n if loc - start < self.minLen:\n- throwException = True\n- elif self.maxSpecified and loc < instrlen and instring[loc] in bodychars:\n- throwException = True\n+ throw_exception = True\n+ elif self.maxSpecified and loc < instrlen and instring[loc] in body_chars:\n+ throw_exception = True\n elif self.asKeyword and (\n- (start > 0 and instring[start - 1] in bodychars)\n- or (loc < instrlen and instring[loc] in bodychars)\n+ (start > 0 and instring[start - 1] in body_chars)\n+ or (loc < instrlen and instring[loc] in body_chars)\n ):\n- throwException = True\n+ throw_exception = True\n \n- if throwException:\n+ if throw_exception:\n raise ParseException(instring, loc, self.errmsg, self)\n \n return loc, instring[start:loc]\n \n- def parseImpl_regex(self, instring, loc, doActions=True):\n+ def parseImpl_regex(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n result = self.re_match(instring, loc)\n if not result:\n raise ParseException(instring, loc, self.errmsg, self)\n@@ -3021,49 +3080,67 @@ def __init__(\n \n self._re = None\n self.reString = self.pattern = pattern\n- self.flags = flags\n \n elif hasattr(pattern, \"pattern\") and hasattr(pattern, \"match\"):\n self._re = pattern\n self.pattern = self.reString = pattern.pattern\n- self.flags = flags\n+\n+ elif callable(pattern):\n+ # defer creating this pattern until we really need it\n+ self.pattern = pattern\n+ self._re = None\n \n else:\n raise TypeError(\n- \"Regex may only be constructed with a string or a compiled RE object\"\n+ \"Regex may only be constructed with a string or a compiled RE object,\"\n+ \" or a callable that takes no arguments and returns a string or a\"\n+ \" compiled RE object\"\n )\n \n+ self.flags = flags\n self.errmsg = f\"Expected {self.name}\"\n self.mayIndexError = False\n self.asGroupList = asGroupList\n self.asMatch = asMatch\n if self.asGroupList:\n- self.parseImpl = self.parseImplAsGroupList # type: ignore [assignment]\n+ self.parseImpl = self.parseImplAsGroupList # type: ignore [method-assign]\n if self.asMatch:\n- self.parseImpl = self.parseImplAsMatch # type: ignore [assignment]\n+ self.parseImpl = self.parseImplAsMatch # type: ignore [method-assign]\n \n @cached_property\n- def re(self):\n+ def re(self) -> re.Pattern:\n if self._re:\n return self._re\n \n+ if callable(self.pattern):\n+ # replace self.pattern with the string returned by calling self.pattern()\n+ self.pattern = cast(Callable[[], str], self.pattern)()\n+\n+ # see if we got a compiled RE back instead of a str - if so, we're done\n+ if hasattr(self.pattern, \"pattern\") and hasattr(self.pattern, \"match\"):\n+ self._re = cast(re.Pattern[str], self.pattern)\n+ self.pattern = self.reString = self._re.pattern\n+ return self._re\n+\n try:\n- return re.compile(self.pattern, self.flags)\n+ self._re = re.compile(self.pattern, self.flags)\n+ return self._re\n except re.error:\n raise ValueError(f\"invalid pattern ({self.pattern!r}) passed to Regex\")\n \n @cached_property\n- def re_match(self):\n+ def re_match(self) -> Callable[[str, int], Any]:\n return self.re.match\n \n @cached_property\n- def mayReturnEmpty(self):\n- return self.re_match(\"\") is not None\n+ def mayReturnEmpty(self) -> bool: # type: ignore[override]\n+ return self.re_match(\"\", 0) is not None\n \n def _generateDefaultName(self) -> str:\n- return \"Re:({})\".format(repr(self.pattern).replace(\"\\\\\\\\\", \"\\\\\"))\n+ unescaped = repr(self.pattern).replace(\"\\\\\\\\\", \"\\\\\")\n+ return f\"Re:({unescaped})\"\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n result = self.re_match(instring, loc)\n if not result:\n raise ParseException(instring, loc, self.errmsg, self)\n@@ -3077,7 +3154,7 @@ def parseImpl(self, instring, loc, doActions=True):\n \n return loc, ret\n \n- def parseImplAsGroupList(self, instring, loc, doActions=True):\n+ def parseImplAsGroupList(self, instring, loc, do_actions=True):\n result = self.re_match(instring, loc)\n if not result:\n raise ParseException(instring, loc, self.errmsg, self)\n@@ -3086,7 +3163,7 @@ def parseImplAsGroupList(self, instring, loc, doActions=True):\n ret = result.groups()\n return loc, ret\n \n- def parseImplAsMatch(self, instring, loc, doActions=True):\n+ def parseImplAsMatch(self, instring, loc, do_actions=True):\n result = self.re_match(instring, loc)\n if not result:\n raise ParseException(instring, loc, self.errmsg, self)\n@@ -3223,7 +3300,7 @@ def __init__(\n \n # fmt: off\n # build up re pattern for the content between the quote delimiters\n- inner_pattern = []\n+ inner_pattern: list[str] = []\n \n if esc_quote:\n inner_pattern.append(rf\"(?:{re.escape(esc_quote)})\")\n@@ -3245,12 +3322,12 @@ def __init__(\n self.re_flags |= re.MULTILINE | re.DOTALL\n inner_pattern.append(\n rf\"(?:[^{_escape_regex_range_chars(self.end_quote_char[0])}\"\n- rf\"{(_escape_regex_range_chars(esc_char) if self.has_esc_char else '')}])\"\n+ rf\"{(_escape_regex_range_chars(self.esc_char) if self.has_esc_char else '')}])\"\n )\n else:\n inner_pattern.append(\n rf\"(?:[^{_escape_regex_range_chars(self.end_quote_char[0])}\\n\\r\"\n- rf\"{(_escape_regex_range_chars(esc_char) if self.has_esc_char else '')}])\"\n+ rf\"{(_escape_regex_range_chars(self.esc_char) if self.has_esc_char else '')}])\"\n )\n \n self.pattern = \"\".join(\n@@ -3267,6 +3344,7 @@ def __init__(\n if self.convert_whitespace_escapes:\n self.unquote_scan_re = re.compile(\n rf\"({'|'.join(re.escape(k) for k in self.ws_map)})\"\n+ rf\"|(\\\\[0-7]{3}|\\\\0|\\\\x[0-9a-fA-F]{2}|\\\\u[0-9a-fA-F]{4})\"\n rf\"|({re.escape(self.esc_char)}.)\"\n rf\"|(\\n|.)\",\n flags=self.re_flags,\n@@ -3298,7 +3376,7 @@ def _generateDefaultName(self) -> str:\n \n return f\"quoted string, starting with {self.quote_char} ending with {self.end_quote_char}\"\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n # check first character of opening quote to see if that is a match\n # before doing the more complicated regex match\n result = (\n@@ -3313,6 +3391,16 @@ def parseImpl(self, instring, loc, doActions=True):\n loc = result.end()\n ret = result.group()\n \n+ def convert_escaped_numerics(s: str) -> str:\n+ if s == \"0\":\n+ return \"\\0\"\n+ if s.isdigit() and len(s) == 3:\n+ return chr(int(s, base=8))\n+ elif s.startswith((\"u\", \"x\")):\n+ return chr(int(s[1:], base=16))\n+ else:\n+ return s\n+\n if self.unquote_results:\n # strip off quotes\n ret = ret[self.quote_char_len : -self.end_quote_char_len]\n@@ -3326,10 +3414,13 @@ def parseImpl(self, instring, loc, doActions=True):\n ret = \"\".join(\n # match group 1 matches \\t, \\n, etc.\n self.ws_map[match.group(1)] if match.group(1)\n- # match group 2 matches escaped characters\n- else match.group(2)[-1] if match.group(2)\n- # match group 3 matches any character\n- else match.group(3)\n+ # match group 2 matches escaped octal, null, hex, and Unicode\n+ # sequences\n+ else convert_escaped_numerics(match.group(2)[1:]) if match.group(2)\n+ # match group 3 matches escaped characters\n+ else match.group(3)[-1] if match.group(3)\n+ # match group 4 matches any character\n+ else match.group(4)\n for match in self.unquote_scan_re.finditer(ret)\n )\n else:\n@@ -3412,7 +3503,7 @@ def _generateDefaultName(self) -> str:\n else:\n return f\"!W:({self.notChars})\"\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n notchars = self.notCharsSet\n if instring[loc] in notchars:\n raise ParseException(instring, loc, self.errmsg, self)\n@@ -3490,7 +3581,7 @@ def __init__(self, ws: str = \" \\t\\r\\n\", min: int = 1, max: int = 0, exact: int =\n def _generateDefaultName(self) -> str:\n return \"\".join(White.whiteStrs[c] for c in self.matchWhite)\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n if instring[loc] not in self.matchWhite:\n raise ParseException(instring, loc, self.errmsg, self)\n start = loc\n@@ -3538,7 +3629,7 @@ def preParse(self, instring: str, loc: int) -> int:\n \n return loc\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n thiscol = col(loc, instring)\n if thiscol > self.col:\n raise ParseException(instring, loc, \"Text not in expected column\", self)\n@@ -3576,7 +3667,7 @@ def __init__(self):\n self.orig_whiteChars = set() | self.whiteChars\n self.whiteChars.discard(\"\\n\")\n self.skipper = Empty().set_whitespace_chars(self.whiteChars)\n- self.errmsg = \"Expected start of line\"\n+ self.set_name(\"start of line\")\n \n def preParse(self, instring: str, loc: int) -> int:\n if loc == 0:\n@@ -3590,7 +3681,7 @@ def preParse(self, instring: str, loc: int) -> int:\n \n return ret\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n if col(loc, instring) == 1:\n return loc, []\n raise ParseException(instring, loc, self.errmsg, self)\n@@ -3605,9 +3696,9 @@ def __init__(self):\n super().__init__()\n self.whiteChars.discard(\"\\n\")\n self.set_whitespace_chars(self.whiteChars, copy_defaults=False)\n- self.errmsg = \"Expected end of line\"\n+ self.set_name(\"end of line\")\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n if loc < len(instring):\n if instring[loc] == \"\\n\":\n return loc + 1, \"\\n\"\n@@ -3626,9 +3717,9 @@ class StringStart(PositionToken):\n \n def __init__(self):\n super().__init__()\n- self.errmsg = \"Expected start of text\"\n+ self.set_name(\"start of text\")\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n # see if entire string up to here is just whitespace and ignoreables\n if loc != 0 and loc != self.preParse(instring, 0):\n raise ParseException(instring, loc, self.errmsg, self)\n@@ -3643,9 +3734,9 @@ class StringEnd(PositionToken):\n \n def __init__(self):\n super().__init__()\n- self.errmsg = \"Expected end of text\"\n+ self.set_name(\"end of text\")\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n if loc < len(instring):\n raise ParseException(instring, loc, self.errmsg, self)\n if loc == len(instring):\n@@ -3670,9 +3761,9 @@ def __init__(self, word_chars: str = printables, *, wordChars: str = printables)\n wordChars = word_chars if wordChars == printables else wordChars\n super().__init__()\n self.wordChars = set(wordChars)\n- self.errmsg = \"Not at the start of a word\"\n+ self.set_name(\"start of a word\")\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n if loc != 0:\n if (\n instring[loc - 1] in self.wordChars\n@@ -3696,9 +3787,9 @@ def __init__(self, word_chars: str = printables, *, wordChars: str = printables)\n super().__init__()\n self.wordChars = set(wordChars)\n self.skipWhitespace = False\n- self.errmsg = \"Not at the end of a word\"\n+ self.set_name(\"end of a word\")\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n instrlen = len(instring)\n if instrlen > 0 and loc < instrlen:\n if (\n@@ -3709,14 +3800,56 @@ def parseImpl(self, instring, loc, doActions=True):\n return loc, []\n \n \n+class Tag(Token):\n+ \"\"\"\n+ A meta-element for inserting a named result into the parsed\n+ tokens that may be checked later in a parse action or while\n+ processing the parsed results. Accepts an optional tag value,\n+ defaulting to `True`.\n+\n+ Example::\n+\n+ end_punc = \".\" | (\"!\" + Tag(\"enthusiastic\")))\n+ greeting = \"Hello,\" + Word(alphas) + end_punc\n+\n+ result = greeting.parse_string(\"Hello, World.\")\n+ print(result.dump())\n+\n+ result = greeting.parse_string(\"Hello, World!\")\n+ print(result.dump())\n+\n+ prints::\n+\n+ ['Hello,', 'World', '.']\n+\n+ ['Hello,', 'World', '!']\n+ - enthusiastic: True\n+ \"\"\"\n+\n+ def __init__(self, tag_name: str, value: Any = True):\n+ super().__init__()\n+ self.mayReturnEmpty = True\n+ self.mayIndexError = False\n+ self.leave_whitespace()\n+ self.tag_name = tag_name\n+ self.tag_value = value\n+ self.add_parse_action(self._add_tag)\n+\n+ def _add_tag(self, tokens: ParseResults):\n+ tokens[self.tag_name] = self.tag_value\n+\n+ def _generateDefaultName(self) -> str:\n+ return f\"{type(self).__name__}:{self.tag_name}={self.tag_value!r}\"\n+\n+\n class ParseExpression(ParserElement):\n \"\"\"Abstract subclass of ParserElement, for combining and\n post-processing parsed tokens.\n \"\"\"\n \n def __init__(self, exprs: typing.Iterable[ParserElement], savelist: bool = False):\n super().__init__(savelist)\n- self.exprs: List[ParserElement]\n+ self.exprs: list[ParserElement]\n if isinstance(exprs, _generatorType):\n exprs = list(exprs)\n \n@@ -3740,7 +3873,7 @@ def __init__(self, exprs: typing.Iterable[ParserElement], savelist: bool = False\n self.exprs = [exprs]\n self.callPreparse = False\n \n- def recurse(self) -> List[ParserElement]:\n+ def recurse(self) -> list[ParserElement]:\n return self.exprs[:]\n \n def append(self, other) -> ParserElement:\n@@ -3846,13 +3979,13 @@ def copy(self) -> ParserElement:\n ret.exprs = [e.copy() for e in self.exprs]\n return ret\n \n- def _setResultsName(self, name, listAllMatches=False):\n+ def _setResultsName(self, name, list_all_matches=False) -> ParserElement:\n if not (\n __diag__.warn_ungrouped_named_tokens_in_collection\n and Diagnostics.warn_ungrouped_named_tokens_in_collection\n not in self.suppress_warnings_\n ):\n- return super()._setResultsName(name, listAllMatches)\n+ return super()._setResultsName(name, list_all_matches)\n \n for e in self.exprs:\n if (\n@@ -3871,7 +4004,7 @@ def _setResultsName(self, name, listAllMatches=False):\n warnings.warn(warning, stacklevel=3)\n break\n \n- return super()._setResultsName(name, listAllMatches)\n+ return super()._setResultsName(name, list_all_matches)\n \n # Compatibility synonyms\n # fmt: off\n@@ -3882,7 +4015,7 @@ def _setResultsName(self, name, listAllMatches=False):\n \n class And(ParseExpression):\n \"\"\"\n- Requires all given :class:`ParseExpression` s to be found in the given order.\n+ Requires all given :class:`ParserElement` s to be found in the given order.\n Expressions may be separated by whitespace.\n May be constructed using the ``'+'`` operator.\n May also be constructed using the ``'-'`` operator, which will\n@@ -3909,9 +4042,9 @@ def _generateDefaultName(self) -> str:\n def __init__(\n self, exprs_arg: typing.Iterable[ParserElement], savelist: bool = True\n ):\n- exprs: List[ParserElement] = list(exprs_arg)\n+ exprs: list[ParserElement] = list(exprs_arg)\n if exprs and Ellipsis in exprs:\n- tmp = []\n+ tmp: list[ParserElement] = []\n for i, expr in enumerate(exprs):\n if expr is not Ellipsis:\n tmp.append(expr)\n@@ -3991,11 +4124,11 @@ def streamline(self) -> ParserElement:\n self.mayReturnEmpty = all(e.mayReturnEmpty for e in self.exprs)\n return self\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True):\n # pass False as callPreParse arg to _parse for first element, since we already\n # pre-parsed the string as part of our And pre-parsing\n loc, resultlist = self.exprs[0]._parse(\n- instring, loc, doActions, callPreParse=False\n+ instring, loc, do_actions, callPreParse=False\n )\n errorStop = False\n for e in self.exprs[1:]:\n@@ -4005,7 +4138,7 @@ def parseImpl(self, instring, loc, doActions=True):\n continue\n if errorStop:\n try:\n- loc, exprtokens = e._parse(instring, loc, doActions)\n+ loc, exprtokens = e._parse(instring, loc, do_actions)\n except ParseSyntaxException:\n raise\n except ParseBaseException as pe:\n@@ -4016,7 +4149,7 @@ def parseImpl(self, instring, loc, doActions=True):\n instring, len(instring), self.errmsg, self\n )\n else:\n- loc, exprtokens = e._parse(instring, loc, doActions)\n+ loc, exprtokens = e._parse(instring, loc, do_actions)\n resultlist += exprtokens\n return loc, resultlist\n \n@@ -4043,7 +4176,7 @@ def _generateDefaultName(self) -> str:\n \n \n class Or(ParseExpression):\n- \"\"\"Requires that at least one :class:`ParseExpression` is found. If\n+ \"\"\"Requires that at least one :class:`ParserElement` is found. If\n two expressions match, the expression that matches the longest\n string will be used. May be constructed using the ``'^'``\n operator.\n@@ -4080,11 +4213,11 @@ def streamline(self) -> ParserElement:\n self.saveAsList = False\n return self\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n maxExcLoc = -1\n maxException = None\n- matches = []\n- fatals = []\n+ matches: list[tuple[int, ParserElement]] = []\n+ fatals: list[ParseFatalException] = []\n if all(e.callPreparse for e in self.exprs):\n loc = self.preParse(instring, loc)\n for e in self.exprs:\n@@ -4117,20 +4250,20 @@ def parseImpl(self, instring, loc, doActions=True):\n # might change whether or how much they match of the input.\n matches.sort(key=itemgetter(0), reverse=True)\n \n- if not doActions:\n+ if not do_actions:\n # no further conditions or parse actions to change the selection of\n # alternative, so the first match will be the best match\n best_expr = matches[0][1]\n- return best_expr._parse(instring, loc, doActions)\n+ return best_expr._parse(instring, loc, do_actions)\n \n- longest = -1, None\n+ longest: tuple[int, typing.Optional[ParseResults]] = -1, None\n for loc1, expr1 in matches:\n if loc1 <= longest[0]:\n # already have a longer match than this one will deliver, we are done\n return longest\n \n try:\n- loc2, toks = expr1._parse(instring, loc, doActions)\n+ loc2, toks = expr1._parse(instring, loc, do_actions)\n except ParseException as err:\n err.__traceback__ = None\n if err.loc > maxExcLoc:\n@@ -4158,7 +4291,7 @@ def parseImpl(self, instring, loc, doActions=True):\n # infer from this check that all alternatives failed at the current position\n # so emit this collective error message instead of any single error message\n if maxExcLoc == loc:\n- maxException.msg = self.errmsg\n+ maxException.msg = self.errmsg or \"\"\n raise maxException\n \n raise ParseException(instring, loc, \"no defined alternatives to match\", self)\n@@ -4173,7 +4306,7 @@ def __ixor__(self, other):\n def _generateDefaultName(self) -> str:\n return f\"{{{' ^ '.join(str(e) for e in self.exprs)}}}\"\n \n- def _setResultsName(self, name, listAllMatches=False):\n+ def _setResultsName(self, name, list_all_matches=False) -> ParserElement:\n if (\n __diag__.warn_multiple_tokens_in_named_alternation\n and Diagnostics.warn_multiple_tokens_in_named_alternation\n@@ -4194,11 +4327,11 @@ def _setResultsName(self, name, listAllMatches=False):\n )\n warnings.warn(warning, stacklevel=3)\n \n- return super()._setResultsName(name, listAllMatches)\n+ return super()._setResultsName(name, list_all_matches)\n \n \n class MatchFirst(ParseExpression):\n- \"\"\"Requires that at least one :class:`ParseExpression` is found. If\n+ \"\"\"Requires that at least one :class:`ParserElement` is found. If\n more than one expression matches, the first one listed is the one that will\n match. May be constructed using the ``'|'`` operator.\n \n@@ -4239,13 +4372,13 @@ def streamline(self) -> ParserElement:\n self.mayReturnEmpty = True\n return self\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n maxExcLoc = -1\n maxException = None\n \n for e in self.exprs:\n try:\n- return e._parse(instring, loc, doActions)\n+ return e._parse(instring, loc, do_actions)\n except ParseFatalException as pfe:\n pfe.__traceback__ = None\n pfe.parser_element = e\n@@ -4265,7 +4398,7 @@ def parseImpl(self, instring, loc, doActions=True):\n # infer from this check that all alternatives failed at the current position\n # so emit this collective error message instead of any individual error message\n if maxExcLoc == loc:\n- maxException.msg = self.errmsg\n+ maxException.msg = self.errmsg or \"\"\n raise maxException\n \n raise ParseException(instring, loc, \"no defined alternatives to match\", self)\n@@ -4280,7 +4413,7 @@ def __ior__(self, other):\n def _generateDefaultName(self) -> str:\n return f\"{{{' | '.join(str(e) for e in self.exprs)}}}\"\n \n- def _setResultsName(self, name, listAllMatches=False):\n+ def _setResultsName(self, name, list_all_matches=False) -> ParserElement:\n if (\n __diag__.warn_multiple_tokens_in_named_alternation\n and Diagnostics.warn_multiple_tokens_in_named_alternation\n@@ -4301,11 +4434,11 @@ def _setResultsName(self, name, listAllMatches=False):\n )\n warnings.warn(warning, stacklevel=3)\n \n- return super()._setResultsName(name, listAllMatches)\n+ return super()._setResultsName(name, list_all_matches)\n \n \n class Each(ParseExpression):\n- \"\"\"Requires all given :class:`ParseExpression` s to be found, but in\n+ \"\"\"Requires all given :class:`ParserElement` s to be found, but in\n any order. Expressions may be separated by whitespace.\n \n May be constructed using the ``'&'`` operator.\n@@ -4387,7 +4520,7 @@ def streamline(self) -> ParserElement:\n self.mayReturnEmpty = True\n return self\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n if self.initExprGroups:\n self.opt1map = dict(\n (id(e.expr), e) for e in self.exprs if isinstance(e, Opt)\n@@ -4419,11 +4552,11 @@ def parseImpl(self, instring, loc, doActions=True):\n tmpReqd = self.required[:]\n tmpOpt = self.optionals[:]\n multis = self.multioptionals[:]\n- matchOrder = []\n+ matchOrder: list[ParserElement] = []\n \n keepMatching = True\n- failed = []\n- fatals = []\n+ failed: list[ParserElement] = []\n+ fatals: list[ParseFatalException] = []\n while keepMatching:\n tmpExprs = tmpReqd + tmpOpt + multis\n failed.clear()\n@@ -4469,7 +4602,7 @@ def parseImpl(self, instring, loc, doActions=True):\n \n total_results = ParseResults([])\n for e in matchOrder:\n- loc, results = e._parse(instring, loc, doActions)\n+ loc, results = e._parse(instring, loc, do_actions)\n total_results += results\n \n return loc, total_results\n@@ -4506,17 +4639,22 @@ def __init__(self, expr: Union[ParserElement, str], savelist: bool = False):\n self.callPreparse = expr.callPreparse\n self.ignoreExprs.extend(expr.ignoreExprs)\n \n- def recurse(self) -> List[ParserElement]:\n+ def recurse(self) -> list[ParserElement]:\n return [self.expr] if self.expr is not None else []\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True):\n if self.expr is None:\n raise ParseException(instring, loc, \"No expression defined\", self)\n \n try:\n- return self.expr._parse(instring, loc, doActions, callPreParse=False)\n+ return self.expr._parse(instring, loc, do_actions, callPreParse=False)\n+ except ParseSyntaxException:\n+ raise\n except ParseBaseException as pbe:\n- if not isinstance(self, Forward) or self.customName is not None:\n+ pbe.pstr = pbe.pstr or instring\n+ pbe.loc = pbe.loc or loc\n+ pbe.parser_element = pbe.parser_element or self\n+ if not isinstance(self, Forward) and self.customName is not None:\n if self.errmsg:\n pbe.msg = self.errmsg\n raise\n@@ -4611,14 +4749,14 @@ def __init__(\n self._grouped = grouped\n self.parent_anchor = 1\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n # advance parse position to non-whitespace by using an Empty()\n # this should be the column to be used for all subsequent indented lines\n anchor_loc = Empty().preParse(instring, loc)\n \n # see if self.expr matches at the current location - if not it will raise an exception\n # and no further work is necessary\n- self.expr.try_parse(instring, anchor_loc, do_actions=doActions)\n+ self.expr.try_parse(instring, anchor_loc, do_actions=do_actions)\n \n indent_col = col(anchor_loc, instring)\n peer_detect_expr = self._Indent(indent_col)\n@@ -4641,9 +4779,9 @@ def parseImpl(self, instring, loc, doActions=True):\n if self._grouped:\n wrapper = Group\n else:\n- wrapper = lambda expr: expr\n+ wrapper = lambda expr: expr # type: ignore[misc, assignment]\n return (wrapper(block) + Optional(trailing_undent)).parseImpl(\n- instring, anchor_loc, doActions\n+ instring, anchor_loc, do_actions\n )\n \n \n@@ -4662,10 +4800,10 @@ def __init__(self, expr: Union[ParserElement, str]):\n super().__init__(expr)\n self.callPreparse = False\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n if loc != 0:\n raise ParseException(instring, loc, \"not found at string start\")\n- return super().parseImpl(instring, loc, doActions)\n+ return super().parseImpl(instring, loc, do_actions)\n \n \n class AtLineStart(ParseElementEnhance):\n@@ -4695,10 +4833,10 @@ def __init__(self, expr: Union[ParserElement, str]):\n super().__init__(expr)\n self.callPreparse = False\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n if col(loc, instring) != 1:\n raise ParseException(instring, loc, \"not found at line start\")\n- return super().parseImpl(instring, loc, doActions)\n+ return super().parseImpl(instring, loc, do_actions)\n \n \n class FollowedBy(ParseElementEnhance):\n@@ -4728,10 +4866,10 @@ def __init__(self, expr: Union[ParserElement, str]):\n super().__init__(expr)\n self.mayReturnEmpty = True\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n # by using self._expr.parse and deleting the contents of the returned ParseResults list\n # we keep any named results that were defined in the FollowedBy expression\n- _, ret = self.expr._parse(instring, loc, doActions=doActions)\n+ _, ret = self.expr._parse(instring, loc, do_actions=do_actions)\n del ret[:]\n \n return loc, ret\n@@ -4767,9 +4905,7 @@ class PrecededBy(ParseElementEnhance):\n \n \"\"\"\n \n- def __init__(\n- self, expr: Union[ParserElement, str], retreat: typing.Optional[int] = None\n- ):\n+ def __init__(self, expr: Union[ParserElement, str], retreat: int = 0):\n super().__init__(expr)\n self.expr = self.expr().leave_whitespace()\n self.mayReturnEmpty = True\n@@ -4793,18 +4929,18 @@ def __init__(\n self.skipWhitespace = False\n self.parseAction.append(lambda s, l, t: t.__delitem__(slice(None, None)))\n \n- def parseImpl(self, instring, loc=0, doActions=True):\n+ def parseImpl(self, instring, loc=0, do_actions=True) -> ParseImplReturnType:\n if self.exact:\n if loc < self.retreat:\n- raise ParseException(instring, loc, self.errmsg)\n+ raise ParseException(instring, loc, self.errmsg, self)\n start = loc - self.retreat\n _, ret = self.expr._parse(instring, start)\n return loc, ret\n \n # retreat specified a maximum lookbehind window, iterate\n test_expr = self.expr + StringEnd()\n instring_slice = instring[max(0, loc - self.retreat) : loc]\n- last_expr = ParseException(instring, loc, self.errmsg)\n+ last_expr: ParseBaseException = ParseException(instring, loc, self.errmsg, self)\n \n for offset in range(1, min(loc, self.retreat + 1) + 1):\n try:\n@@ -4848,9 +4984,9 @@ class Located(ParseElementEnhance):\n \n \"\"\"\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n start = loc\n- loc, tokens = self.expr._parse(instring, start, doActions, callPreParse=False)\n+ loc, tokens = self.expr._parse(instring, start, do_actions, callPreParse=False)\n ret_tokens = ParseResults([start, tokens, loc])\n ret_tokens[\"locn_start\"] = start\n ret_tokens[\"value\"] = tokens\n@@ -4896,8 +5032,8 @@ def __init__(self, expr: Union[ParserElement, str]):\n self.mayReturnEmpty = True\n self.errmsg = f\"Found unwanted token, {self.expr}\"\n \n- def parseImpl(self, instring, loc, doActions=True):\n- if self.expr.can_parse_next(instring, loc, do_actions=doActions):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n+ if self.expr.can_parse_next(instring, loc, do_actions=do_actions):\n raise ParseException(instring, loc, self.errmsg, self)\n return loc, []\n \n@@ -4927,7 +5063,7 @@ def stopOn(self, ender) -> ParserElement:\n self.not_ender = ~ender if ender is not None else None\n return self\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n self_expr_parse = self.expr._parse\n self_skip_ignorables = self._skipIgnorables\n check_ender = self.not_ender is not None\n@@ -4938,7 +5074,7 @@ def parseImpl(self, instring, loc, doActions=True):\n # if so, fail)\n if check_ender:\n try_not_ender(instring, loc)\n- loc, tokens = self_expr_parse(instring, loc, doActions)\n+ loc, tokens = self_expr_parse(instring, loc, do_actions)\n try:\n hasIgnoreExprs = not not self.ignoreExprs\n while 1:\n@@ -4948,14 +5084,14 @@ def parseImpl(self, instring, loc, doActions=True):\n preloc = self_skip_ignorables(instring, loc)\n else:\n preloc = loc\n- loc, tmptokens = self_expr_parse(instring, preloc, doActions)\n+ loc, tmptokens = self_expr_parse(instring, preloc, do_actions)\n tokens += tmptokens\n except (ParseException, IndexError):\n pass\n \n return loc, tokens\n \n- def _setResultsName(self, name, listAllMatches=False):\n+ def _setResultsName(self, name, list_all_matches=False) -> ParserElement:\n if (\n __diag__.warn_ungrouped_named_tokens_in_collection\n and Diagnostics.warn_ungrouped_named_tokens_in_collection\n@@ -4978,7 +5114,7 @@ def _setResultsName(self, name, listAllMatches=False):\n warnings.warn(warning, stacklevel=3)\n break\n \n- return super()._setResultsName(name, listAllMatches)\n+ return super()._setResultsName(name, list_all_matches)\n \n \n class OneOrMore(_MultipleMatch):\n@@ -5037,9 +5173,9 @@ def __init__(\n super().__init__(expr, stopOn=stopOn or stop_on)\n self.mayReturnEmpty = True\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n try:\n- return super().parseImpl(instring, loc, doActions)\n+ return super().parseImpl(instring, loc, do_actions)\n except (ParseException, IndexError):\n return loc, ParseResults([], name=self.resultsName)\n \n@@ -5170,20 +5306,22 @@ def __init__(\n self.defaultValue = default\n self.mayReturnEmpty = True\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n self_expr = self.expr\n try:\n- loc, tokens = self_expr._parse(instring, loc, doActions, callPreParse=False)\n+ loc, tokens = self_expr._parse(\n+ instring, loc, do_actions, callPreParse=False\n+ )\n except (ParseException, IndexError):\n default_value = self.defaultValue\n if default_value is not self.__optionalNotMatched:\n if self_expr.resultsName:\n tokens = ParseResults([default_value])\n tokens[self_expr.resultsName] = default_value\n else:\n- tokens = [default_value]\n+ tokens = [default_value] # type: ignore[assignment]\n else:\n- tokens = []\n+ tokens = [] # type: ignore[assignment]\n return loc, tokens\n \n def _generateDefaultName(self) -> str:\n@@ -5279,7 +5417,7 @@ def __init__(\n self.failOn = self._literalStringClass(failOn)\n else:\n self.failOn = failOn\n- self.errmsg = \"No match found for \" + str(self.expr)\n+ self.errmsg = f\"No match found for {self.expr}\"\n self.ignorer = Empty().leave_whitespace()\n self._update_ignorer()\n \n@@ -5295,7 +5433,7 @@ def ignore(self, expr):\n super().ignore(expr)\n self._update_ignorer()\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True):\n startloc = loc\n instrlen = len(instring)\n self_expr_parse = self.expr._parse\n@@ -5325,7 +5463,7 @@ def parseImpl(self, instring, loc, doActions=True):\n prev_tmploc = tmploc\n \n try:\n- self_expr_parse(instring, tmploc, doActions=False, callPreParse=False)\n+ self_expr_parse(instring, tmploc, do_actions=False, callPreParse=False)\n except (ParseException, IndexError):\n # no match, advance loc in string\n tmploc += 1\n@@ -5343,7 +5481,7 @@ def parseImpl(self, instring, loc, doActions=True):\n skipresult = ParseResults(skiptext)\n \n if self.includeMatch:\n- loc, mat = self_expr_parse(instring, loc, doActions, callPreParse=False)\n+ loc, mat = self_expr_parse(instring, loc, do_actions, callPreParse=False)\n skipresult += mat\n \n return loc, skipresult\n@@ -5383,7 +5521,7 @@ def __init__(self, other: typing.Optional[Union[ParserElement, str]] = None):\n super().__init__(other, savelist=False) # type: ignore[arg-type]\n self.lshift_line = None\n \n- def __lshift__(self, other) -> \"Forward\":\n+ def __lshift__(self, other) -> Forward:\n if hasattr(self, \"caller_frame\"):\n del self.caller_frame\n if isinstance(other, str_type):\n@@ -5405,13 +5543,13 @@ def __lshift__(self, other) -> \"Forward\":\n self.lshift_line = traceback.extract_stack(limit=2)[-2] # type: ignore[assignment]\n return self\n \n- def __ilshift__(self, other) -> \"Forward\":\n+ def __ilshift__(self, other) -> Forward:\n if not isinstance(other, ParserElement):\n return NotImplemented\n \n return self << other\n \n- def __or__(self, other) -> \"ParserElement\":\n+ def __or__(self, other) -> ParserElement:\n caller_line = traceback.extract_stack(limit=2)[-2]\n if (\n __diag__.warn_on_match_first_with_lshift_operator\n@@ -5440,7 +5578,7 @@ def __del__(self):\n lineno=self.caller_frame.lineno,\n )\n \n- def parseImpl(self, instring, loc, doActions=True):\n+ def parseImpl(self, instring, loc, do_actions=True) -> ParseImplReturnType:\n if (\n self.expr is None\n and __diag__.warn_on_parse_using_empty_Forward\n@@ -5466,7 +5604,7 @@ def parseImpl(self, instring, loc, doActions=True):\n stacklevel=stacklevel,\n )\n if not ParserElement._left_recursion_enabled:\n- return super().parseImpl(instring, loc, doActions)\n+ return super().parseImpl(instring, loc, do_actions)\n # ## Bounded Recursion algorithm ##\n # Recursion only needs to be processed at ``Forward`` elements, since they are\n # the only ones that can actually refer to themselves. The general idea is\n@@ -5484,28 +5622,28 @@ def parseImpl(self, instring, loc, doActions=True):\n #\n # There is a complication since we not only *parse* but also *transform* via\n # actions: We do not want to run the actions too often while expanding. Thus,\n- # we expand using `doActions=False` and only run `doActions=True` if the next\n+ # we expand using `do_actions=False` and only run `do_actions=True` if the next\n # recursion level is acceptable.\n with ParserElement.recursion_lock:\n memo = ParserElement.recursion_memos\n try:\n # we are parsing at a specific recursion expansion - use it as-is\n- prev_loc, prev_result = memo[loc, self, doActions]\n+ prev_loc, prev_result = memo[loc, self, do_actions]\n if isinstance(prev_result, Exception):\n raise prev_result\n return prev_loc, prev_result.copy()\n except KeyError:\n act_key = (loc, self, True)\n peek_key = (loc, self, False)\n # we are searching for the best recursion expansion - keep on improving\n- # both `doActions` cases must be tracked separately here!\n+ # both `do_actions` cases must be tracked separately here!\n prev_loc, prev_peek = memo[peek_key] = (\n loc - 1,\n ParseException(\n instring, loc, \"Forward recursion without base case\", self\n ),\n )\n- if doActions:\n+ if do_actions:\n memo[act_key] = memo[peek_key]\n while True:\n try:\n@@ -5517,16 +5655,16 @@ def parseImpl(self, instring, loc, doActions=True):\n new_loc, new_peek = prev_loc, prev_peek\n # the match did not get better: we are done\n if new_loc <= prev_loc:\n- if doActions:\n- # replace the match for doActions=False as well,\n+ if do_actions:\n+ # replace the match for do_actions=False as well,\n # in case the action did backtrack\n prev_loc, prev_result = memo[peek_key] = memo[act_key]\n del memo[peek_key], memo[act_key]\n- return prev_loc, prev_result.copy()\n+ return prev_loc, copy.copy(prev_result)\n del memo[peek_key]\n- return prev_loc, prev_peek.copy()\n+ return prev_loc, copy.copy(prev_peek)\n # the match did get better: see if we can improve further\n- if doActions:\n+ if do_actions:\n try:\n memo[act_key] = super().parseImpl(instring, loc, True)\n except ParseException as e:\n@@ -5586,7 +5724,7 @@ def copy(self) -> ParserElement:\n ret <<= self\n return ret\n \n- def _setResultsName(self, name, list_all_matches=False):\n+ def _setResultsName(self, name, list_all_matches=False) -> ParserElement:\n # fmt: off\n if (\n __diag__.warn_name_set_on_empty_Forward\n@@ -5612,7 +5750,7 @@ def _setResultsName(self, name, list_all_matches=False):\n \n class TokenConverter(ParseElementEnhance):\n \"\"\"\n- Abstract subclass of :class:`ParseExpression`, for converting parsed results.\n+ Abstract subclass of :class:`ParseElementEnhance`, for converting parsed results.\n \"\"\"\n \n def __init__(self, expr: Union[ParserElement, str], savelist=False):\n@@ -5837,13 +5975,13 @@ def __init__(self, expr: Union[ParserElement, str], savelist: bool = False):\n expr = _PendingSkip(NoMatch())\n super().__init__(expr)\n \n- def __add__(self, other) -> \"ParserElement\":\n+ def __add__(self, other) -> ParserElement:\n if isinstance(self.expr, _PendingSkip):\n return Suppress(SkipTo(other)) + other\n \n return super().__add__(other)\n \n- def __sub__(self, other) -> \"ParserElement\":\n+ def __sub__(self, other) -> ParserElement:\n if isinstance(self.expr, _PendingSkip):\n return Suppress(SkipTo(other)) - other\n \n@@ -5892,7 +6030,9 @@ def z(*paArgs):\n try:\n ret = f(*paArgs)\n except Exception as exc:\n- sys.stderr.write(f\"<<leaving {thisFunc} (exception: {exc})\\n\")\n+ sys.stderr.write(\n+ f\"<<leaving {thisFunc} (exception: {type(exc).__name__}: {exc})\\n\"\n+ )\n raise\n sys.stderr.write(f\"<<leaving {thisFunc} (ret: {ret!r})\\n\")\n return ret\n@@ -6066,11 +6206,11 @@ def autoname_elements() -> None:\n \n # build list of built-in expressions, for future reference if a global default value\n # gets updated\n-_builtin_exprs: List[ParserElement] = [\n+_builtin_exprs: list[ParserElement] = [\n v for v in vars().values() if isinstance(v, ParserElement)\n ]\n \n-# backward compatibility names\n+# Compatibility synonyms\n # fmt: off\n sglQuotedString = sgl_quoted_string\n dblQuotedString = dbl_quoted_string"}, {"sha": "7926f2c3552863166b185a0f2f0d010e6acb6c09", "filename": "lib/pyparsing/diagram/__init__.py", "status": "modified", "additions": 90, "deletions": 54, "changes": 144, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpyparsing%2Fdiagram%2F__init__.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpyparsing%2Fdiagram%2F__init__.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpyparsing%2Fdiagram%2F__init__.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,20 +1,20 @@\n # mypy: ignore-errors\n+from __future__ import annotations\n+\n import railroad\n import pyparsing\n+import dataclasses\n import typing\n from typing import (\n- List,\n- NamedTuple,\n Generic,\n TypeVar,\n- Dict,\n Callable,\n- Set,\n Iterable,\n )\n from jinja2 import Template\n from io import StringIO\n import inspect\n+import re\n \n \n jinja2_template_source = \"\"\"\\\n@@ -36,6 +36,7 @@\n </head>\n <body>\n {% endif %}\n+<meta charset=\"UTF-8\"/>\n {{ body | safe }}\n {% for diagram in diagrams %}\n <div class=\"railroad-group\">\n@@ -54,14 +55,23 @@\n \n template = Template(jinja2_template_source)\n \n-# Note: ideally this would be a dataclass, but we're supporting Python 3.5+ so we can't do this yet\n-NamedDiagram = NamedTuple(\n- \"NamedDiagram\",\n- [(\"name\", str), (\"diagram\", typing.Optional[railroad.DiagramItem]), (\"index\", int)],\n-)\n-\"\"\"\n-A simple structure for associating a name with a railroad diagram\n-\"\"\"\n+\n+def _collapse_verbose_regex(regex_str: str) -> str:\n+ collapsed = pyparsing.Regex(r\"#.*\").suppress().transform_string(regex_str)\n+ collapsed = re.sub(r\"\\s*\\n\\s*\", \"\", collapsed)\n+ return collapsed\n+\n+\n+@dataclasses.dataclass\n+class NamedDiagram:\n+ \"\"\"\n+ A simple structure for associating a name with a railroad diagram\n+ \"\"\"\n+\n+ name: str\n+ index: int\n+ diagram: railroad.DiagramItem = None\n+\n \n T = TypeVar(\"T\")\n \n@@ -89,7 +99,7 @@ class AnnotatedItem(railroad.Group):\n \"\"\"\n \n def __init__(self, label: str, item):\n- super().__init__(item=item, label=\"[{}]\".format(label) if label else label)\n+ super().__init__(item=item, label=f\"[{label}]\")\n \n \n class EditablePartial(Generic[T]):\n@@ -107,7 +117,7 @@ def __init__(self, func: Callable[..., T], args: list, kwargs: dict):\n self.kwargs = kwargs\n \n @classmethod\n- def from_call(cls, func: Callable[..., T], *args, **kwargs) -> \"EditablePartial[T]\":\n+ def from_call(cls, func: Callable[..., T], *args, **kwargs) -> EditablePartial[T]:\n \"\"\"\n If you call this function in the same way that you would call the constructor, it will store the arguments\n as you expect. For example EditablePartial.from_call(Fraction, 1, 3)() == Fraction(1, 3)\n@@ -134,7 +144,7 @@ def __call__(self) -> T:\n return self.func(*args, **kwargs)\n \n \n-def railroad_to_html(diagrams: List[NamedDiagram], embed=False, **kwargs) -> str:\n+def railroad_to_html(diagrams: list[NamedDiagram], embed=False, **kwargs) -> str:\n \"\"\"\n Given a list of NamedDiagram, produce a single HTML string that visualises those diagrams\n :params kwargs: kwargs to be passed in to the template\n@@ -145,7 +155,7 @@ def railroad_to_html(diagrams: List[NamedDiagram], embed=False, **kwargs) -> str\n continue\n io = StringIO()\n try:\n- css = kwargs.get('css')\n+ css = kwargs.get(\"css\")\n diagram.diagram.writeStandalone(io.write, css=css)\n except AttributeError:\n diagram.diagram.writeSvg(io.write)\n@@ -157,7 +167,7 @@ def railroad_to_html(diagrams: List[NamedDiagram], embed=False, **kwargs) -> str\n return template.render(diagrams=data, embed=embed, **kwargs)\n \n \n-def resolve_partial(partial: \"EditablePartial[T]\") -> T:\n+def resolve_partial(partial: EditablePartial[T]) -> T:\n \"\"\"\n Recursively resolves a collection of Partials into whatever type they are\n \"\"\"\n@@ -179,7 +189,7 @@ def to_railroad(\n vertical: int = 3,\n show_results_names: bool = False,\n show_groups: bool = False,\n-) -> List[NamedDiagram]:\n+) -> list[NamedDiagram]:\n \"\"\"\n Convert a pyparsing element tree into a list of diagrams. This is the recommended entrypoint to diagram\n creation if you want to access the Railroad tree before it is converted to HTML\n@@ -243,40 +253,31 @@ def _should_vertical(\n return len(_visible_exprs(exprs)) >= specification\n \n \n+@dataclasses.dataclass\n class ElementState:\n \"\"\"\n State recorded for an individual pyparsing Element\n \"\"\"\n \n- # Note: this should be a dataclass, but we have to support Python 3.5\n- def __init__(\n- self,\n- element: pyparsing.ParserElement,\n- converted: EditablePartial,\n- parent: EditablePartial,\n- number: int,\n- name: str = None,\n- parent_index: typing.Optional[int] = None,\n- ):\n- #: The pyparsing element that this represents\n- self.element: pyparsing.ParserElement = element\n- #: The name of the element\n- self.name: typing.Optional[str] = name\n- #: The output Railroad element in an unconverted state\n- self.converted: EditablePartial = converted\n- #: The parent Railroad element, which we store so that we can extract this if it's duplicated\n- self.parent: EditablePartial = parent\n- #: The order in which we found this element, used for sorting diagrams if this is extracted into a diagram\n- self.number: int = number\n- #: The index of this inside its parent\n- self.parent_index: typing.Optional[int] = parent_index\n- #: If true, we should extract this out into a subdiagram\n- self.extract: bool = False\n- #: If true, all of this element's children have been filled out\n- self.complete: bool = False\n+ #: The pyparsing element that this represents\n+ element: pyparsing.ParserElement\n+ #: The output Railroad element in an unconverted state\n+ converted: EditablePartial\n+ #: The parent Railroad element, which we store so that we can extract this if it's duplicated\n+ parent: EditablePartial\n+ #: The order in which we found this element, used for sorting diagrams if this is extracted into a diagram\n+ number: int\n+ #: The name of the element\n+ name: str = None\n+ #: The index of this inside its parent\n+ parent_index: typing.Optional[int] = None\n+ #: If true, we should extract this out into a subdiagram\n+ extract: bool = False\n+ #: If true, all of this element's children have been filled out\n+ complete: bool = False\n \n def mark_for_extraction(\n- self, el_id: int, state: \"ConverterState\", name: str = None, force: bool = False\n+ self, el_id: int, state: ConverterState, name: str = None, force: bool = False\n ):\n \"\"\"\n Called when this instance has been seen twice, and thus should eventually be extracted into a sub-diagram\n@@ -312,16 +313,16 @@ class ConverterState:\n \n def __init__(self, diagram_kwargs: typing.Optional[dict] = None):\n #: A dictionary mapping ParserElements to state relating to them\n- self._element_diagram_states: Dict[int, ElementState] = {}\n+ self._element_diagram_states: dict[int, ElementState] = {}\n #: A dictionary mapping ParserElement IDs to subdiagrams generated from them\n- self.diagrams: Dict[int, EditablePartial[NamedDiagram]] = {}\n+ self.diagrams: dict[int, EditablePartial[NamedDiagram]] = {}\n #: The index of the next unnamed element\n self.unnamed_index: int = 1\n #: The index of the next element. This is used for sorting\n self.index: int = 0\n #: Shared kwargs that are used to customize the construction of diagrams\n self.diagram_kwargs: dict = diagram_kwargs or {}\n- self.extracted_diagram_names: Set[str] = set()\n+ self.extracted_diagram_names: set[str] = set()\n \n def __setitem__(self, key: int, value: ElementState):\n self._element_diagram_states[key] = value\n@@ -425,9 +426,11 @@ def _inner(\n element_results_name = element.resultsName\n if element_results_name:\n # add \"*\" to indicate if this is a \"list all results\" name\n- element_results_name += \"\" if element.modalResults else \"*\"\n+ modal_tag = \"\" if element.modalResults else \"*\"\n ret = EditablePartial.from_call(\n- railroad.Group, item=ret, label=element_results_name\n+ railroad.Group,\n+ item=ret,\n+ label=f\"{repr(element_results_name)}{modal_tag}\",\n )\n \n return ret\n@@ -510,7 +513,7 @@ def _to_diagram_element(\n \n # If the element isn't worth extracting, we always treat it as the first time we say it\n if _worth_extracting(element):\n- if el_id in lookup:\n+ if el_id in lookup and lookup[el_id].name is not None:\n # If we've seen this element exactly once before, we are only just now finding out that it's a duplicate,\n # so we have to extract it into a new diagram.\n looked_up = lookup[el_id]\n@@ -534,7 +537,7 @@ def _to_diagram_element(\n # (all will have the same name, and resultsName)\n if not exprs:\n return None\n- if len(set((e.name, e.resultsName) for e in exprs)) == 1:\n+ if len(set((e.name, e.resultsName) for e in exprs)) == 1 and len(exprs) > 2:\n ret = EditablePartial.from_call(\n railroad.OneOrMore, item=\"\", repeat=str(len(exprs))\n )\n@@ -563,7 +566,7 @@ def _to_diagram_element(\n if show_groups:\n ret = EditablePartial.from_call(AnnotatedItem, label=\"\", item=\"\")\n else:\n- ret = EditablePartial.from_call(railroad.Group, label=\"\", item=\"\")\n+ ret = EditablePartial.from_call(railroad.Sequence, items=[])\n elif isinstance(element, pyparsing.TokenConverter):\n label = type(element).__name__.lower()\n if label == \"tokenconverter\":\n@@ -573,8 +576,36 @@ def _to_diagram_element(\n elif isinstance(element, pyparsing.Opt):\n ret = EditablePartial.from_call(railroad.Optional, item=\"\")\n elif isinstance(element, pyparsing.OneOrMore):\n- ret = EditablePartial.from_call(railroad.OneOrMore, item=\"\")\n+ if element.not_ender is not None:\n+ args = [\n+ parent,\n+ lookup,\n+ vertical,\n+ index,\n+ name_hint,\n+ show_results_names,\n+ show_groups,\n+ ]\n+ return _to_diagram_element(\n+ (~element.not_ender.expr + element.expr)[1, ...].set_name(element.name),\n+ *args,\n+ )\n+ ret = EditablePartial.from_call(railroad.OneOrMore, item=None)\n elif isinstance(element, pyparsing.ZeroOrMore):\n+ if element.not_ender is not None:\n+ args = [\n+ parent,\n+ lookup,\n+ vertical,\n+ index,\n+ name_hint,\n+ show_results_names,\n+ show_groups,\n+ ]\n+ return _to_diagram_element(\n+ (~element.not_ender.expr + element.expr)[...].set_name(element.name),\n+ *args,\n+ )\n ret = EditablePartial.from_call(railroad.ZeroOrMore, item=\"\")\n elif isinstance(element, pyparsing.Group):\n ret = EditablePartial.from_call(\n@@ -587,6 +618,11 @@ def _to_diagram_element(\n ret = EditablePartial.from_call(railroad.Sequence, items=[])\n elif len(exprs) > 0 and not element_results_name:\n ret = EditablePartial.from_call(railroad.Group, item=\"\", label=name)\n+ elif isinstance(element, pyparsing.Regex):\n+ patt = _collapse_verbose_regex(element.pattern)\n+ element.pattern = patt\n+ element._defaultName = None\n+ ret = EditablePartial.from_call(railroad.Terminal, element.defaultName)\n elif len(exprs) > 0:\n ret = EditablePartial.from_call(railroad.Sequence, items=[])\n else:"}, {"sha": "57a1579d121e46e407efe73f5cc827d29eb352e8", "filename": "lib/pyparsing/exceptions.py", "status": "modified", "additions": 51, "deletions": 37, "changes": 88, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpyparsing%2Fexceptions.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpyparsing%2Fexceptions.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpyparsing%2Fexceptions.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,17 +1,20 @@\n # exceptions.py\n+from __future__ import annotations\n \n+import copy\n import re\n import sys\n import typing\n+from functools import cached_property\n \n+from .unicode import pyparsing_unicode as ppu\n from .util import (\n+ _collapse_string_to_ranges,\n col,\n line,\n lineno,\n- _collapse_string_to_ranges,\n replaced_by_pep8,\n )\n-from .unicode import pyparsing_unicode as ppu\n \n \n class _ExceptionWordUnicodeSet(\n@@ -31,7 +34,7 @@ class ParseBaseException(Exception):\n msg: str\n pstr: str\n parser_element: typing.Any # \"ParserElement\"\n- args: typing.Tuple[str, int, typing.Optional[str]]\n+ args: tuple[str, int, typing.Optional[str]]\n \n __slots__ = (\n \"loc\",\n@@ -50,18 +53,17 @@ def __init__(\n msg: typing.Optional[str] = None,\n elem=None,\n ):\n- self.loc = loc\n if msg is None:\n- self.msg = pstr\n- self.pstr = \"\"\n- else:\n- self.msg = msg\n- self.pstr = pstr\n+ msg, pstr = pstr, \"\"\n+\n+ self.loc = loc\n+ self.msg = msg\n+ self.pstr = pstr\n self.parser_element = elem\n self.args = (pstr, loc, msg)\n \n @staticmethod\n- def explain_exception(exc, depth=16):\n+ def explain_exception(exc: Exception, depth: int = 16) -> str:\n \"\"\"\n Method to take an exception and translate the Python internal traceback into a list\n of the pyparsing expressions that caused the exception to be raised.\n@@ -82,17 +84,17 @@ def explain_exception(exc, depth=16):\n \n if depth is None:\n depth = sys.getrecursionlimit()\n- ret = []\n+ ret: list[str] = []\n if isinstance(exc, ParseBaseException):\n ret.append(exc.line)\n- ret.append(\" \" * (exc.column - 1) + \"^\")\n+ ret.append(f\"{' ' * (exc.column - 1)}^\")\n ret.append(f\"{type(exc).__name__}: {exc}\")\n \n- if depth <= 0:\n+ if depth <= 0 or exc.__traceback__ is None:\n return \"\\n\".join(ret)\n \n callers = inspect.getinnerframes(exc.__traceback__, context=depth)\n- seen = set()\n+ seen: set[int] = set()\n for ff in callers[-depth:]:\n frm = ff[0]\n \n@@ -125,41 +127,58 @@ def explain_exception(exc, depth=16):\n return \"\\n\".join(ret)\n \n @classmethod\n- def _from_exception(cls, pe):\n+ def _from_exception(cls, pe) -> ParseBaseException:\n \"\"\"\n internal factory method to simplify creating one type of ParseException\n from another - avoids having __init__ signature conflicts among subclasses\n \"\"\"\n return cls(pe.pstr, pe.loc, pe.msg, pe.parser_element)\n \n- @property\n+ @cached_property\n def line(self) -> str:\n \"\"\"\n Return the line of text where the exception occurred.\n \"\"\"\n return line(self.loc, self.pstr)\n \n- @property\n+ @cached_property\n def lineno(self) -> int:\n \"\"\"\n Return the 1-based line number of text where the exception occurred.\n \"\"\"\n return lineno(self.loc, self.pstr)\n \n- @property\n+ @cached_property\n def col(self) -> int:\n \"\"\"\n Return the 1-based column on the line of text where the exception occurred.\n \"\"\"\n return col(self.loc, self.pstr)\n \n- @property\n+ @cached_property\n def column(self) -> int:\n \"\"\"\n Return the 1-based column on the line of text where the exception occurred.\n \"\"\"\n return col(self.loc, self.pstr)\n \n+ @cached_property\n+ def found(self) -> str:\n+ if not self.pstr:\n+ return \"\"\n+\n+ if self.loc >= len(self.pstr):\n+ return \"end of text\"\n+\n+ # pull out next word at error location\n+ found_match = _exception_word_extractor.match(self.pstr, self.loc)\n+ if found_match is not None:\n+ found_text = found_match.group(0)\n+ else:\n+ found_text = self.pstr[self.loc : self.loc + 1]\n+\n+ return repr(found_text).replace(r\"\\\\\", \"\\\\\")\n+\n # pre-PEP8 compatibility\n @property\n def parserElement(self):\n@@ -169,21 +188,15 @@ def parserElement(self):\n def parserElement(self, elem):\n self.parser_element = elem\n \n+ def copy(self):\n+ return copy.copy(self)\n+\n+ def formatted_message(self) -> str:\n+ found_phrase = f\", found {self.found}\" if self.found else \"\"\n+ return f\"{self.msg}{found_phrase} (at char {self.loc}), (line:{self.lineno}, col:{self.column})\"\n+\n def __str__(self) -> str:\n- if self.pstr:\n- if self.loc >= len(self.pstr):\n- foundstr = \", found end of text\"\n- else:\n- # pull out next word at error location\n- found_match = _exception_word_extractor.match(self.pstr, self.loc)\n- if found_match is not None:\n- found = found_match.group(0)\n- else:\n- found = self.pstr[self.loc : self.loc + 1]\n- foundstr = (\", found %r\" % found).replace(r\"\\\\\", \"\\\\\")\n- else:\n- foundstr = \"\"\n- return f\"{self.msg}{foundstr} (at char {self.loc}), (line:{self.lineno}, col:{self.column})\"\n+ return self.formatted_message()\n \n def __repr__(self):\n return str(self)\n@@ -199,12 +212,10 @@ def mark_input_line(\n line_str = self.line\n line_column = self.column - 1\n if markerString:\n- line_str = \"\".join(\n- (line_str[:line_column], markerString, line_str[line_column:])\n- )\n+ line_str = f\"{line_str[:line_column]}{markerString}{line_str[line_column:]}\"\n return line_str.strip()\n \n- def explain(self, depth=16) -> str:\n+ def explain(self, depth: int = 16) -> str:\n \"\"\"\n Method to translate the Python internal traceback into a list\n of the pyparsing expressions that caused the exception to be raised.\n@@ -245,6 +256,7 @@ def explain(self, depth=16) -> str:\n \"\"\"\n return self.explain_exception(self, depth)\n \n+ # Compatibility synonyms\n # fmt: off\n markInputline = replaced_by_pep8(\"markInputline\", mark_input_line)\n # fmt: on\n@@ -291,6 +303,8 @@ class RecursiveGrammarException(Exception):\n Exception thrown by :class:`ParserElement.validate` if the\n grammar could be left-recursive; parser may need to enable\n left recursion using :class:`ParserElement.enable_left_recursion<ParserElement.enable_left_recursion>`\n+\n+ Deprecated: only used by deprecated method ParserElement.validate.\n \"\"\"\n \n def __init__(self, parseElementList):"}, {"sha": "d2bd05f3d39a6652ed8dfb44769cdc4bd5a17754", "filename": "lib/pyparsing/helpers.py", "status": "modified", "additions": 40, "deletions": 32, "changes": 72, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpyparsing%2Fhelpers.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpyparsing%2Fhelpers.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpyparsing%2Fhelpers.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,5 +1,6 @@\n # helpers.py\n import html.entities\n+import operator\n import re\n import sys\n import typing\n@@ -10,6 +11,7 @@\n _bslash,\n _flatten,\n _escape_regex_range_chars,\n+ make_compressed_re,\n replaced_by_pep8,\n )\n \n@@ -203,15 +205,15 @@ def one_of(\n )\n \n if caseless:\n- isequal = lambda a, b: a.upper() == b.upper()\n+ is_equal = lambda a, b: a.upper() == b.upper()\n masks = lambda a, b: b.upper().startswith(a.upper())\n- parseElementClass = CaselessKeyword if asKeyword else CaselessLiteral\n+ parse_element_class = CaselessKeyword if asKeyword else CaselessLiteral\n else:\n- isequal = lambda a, b: a == b\n+ is_equal = operator.eq\n masks = lambda a, b: b.startswith(a)\n- parseElementClass = Keyword if asKeyword else Literal\n+ parse_element_class = Keyword if asKeyword else Literal\n \n- symbols: List[str] = []\n+ symbols: list[str]\n if isinstance(strs, str_type):\n strs = typing.cast(str, strs)\n symbols = strs.split()\n@@ -224,20 +226,19 @@ def one_of(\n \n # reorder given symbols to take care to avoid masking longer choices with shorter ones\n # (but only if the given symbols are not just single characters)\n- if any(len(sym) > 1 for sym in symbols):\n- i = 0\n- while i < len(symbols) - 1:\n- cur = symbols[i]\n- for j, other in enumerate(symbols[i + 1 :]):\n- if isequal(other, cur):\n- del symbols[i + j + 1]\n- break\n- if masks(cur, other):\n- del symbols[i + j + 1]\n- symbols.insert(i, other)\n- break\n- else:\n- i += 1\n+ i = 0\n+ while i < len(symbols) - 1:\n+ cur = symbols[i]\n+ for j, other in enumerate(symbols[i + 1 :]):\n+ if is_equal(other, cur):\n+ del symbols[i + j + 1]\n+ break\n+ if len(other) > len(cur) and masks(cur, other):\n+ del symbols[i + j + 1]\n+ symbols.insert(i, other)\n+ break\n+ else:\n+ i += 1\n \n if useRegex:\n re_flags: int = re.IGNORECASE if caseless else 0\n@@ -269,7 +270,7 @@ def one_of(\n )\n \n # last resort, just use MatchFirst\n- return MatchFirst(parseElementClass(sym) for sym in symbols).set_name(\n+ return MatchFirst(parse_element_class(sym) for sym in symbols).set_name(\n \" | \".join(symbols)\n )\n \n@@ -602,7 +603,7 @@ def _makeTags(tagStr, xml, suppress_LT=Suppress(\"<\"), suppress_GT=Suppress(\">\"))\n \n def make_html_tags(\n tag_str: Union[str, ParserElement]\n-) -> Tuple[ParserElement, ParserElement]:\n+) -> tuple[ParserElement, ParserElement]:\n \"\"\"Helper to construct opening and closing tag expressions for HTML,\n given a tag name. Matches tags in either upper or lower case,\n attributes with namespaces and with quoted or unquoted values.\n@@ -629,7 +630,7 @@ def make_html_tags(\n \n def make_xml_tags(\n tag_str: Union[str, ParserElement]\n-) -> Tuple[ParserElement, ParserElement]:\n+) -> tuple[ParserElement, ParserElement]:\n \"\"\"Helper to construct opening and closing tag expressions for XML,\n given a tag name. Matches tags only in the given upper/lower case.\n \n@@ -645,9 +646,12 @@ def make_xml_tags(\n )\n \n _htmlEntityMap = {k.rstrip(\";\"): v for k, v in html.entities.html5.items()}\n-common_html_entity = Regex(\"&(?P<entity>\" + \"|\".join(_htmlEntityMap) + \");\").set_name(\n- \"common HTML entity\"\n+_most_common_entities = \"nbsp lt gt amp quot apos cent pound euro copy\".replace(\n+ \" \", \"|\"\n )\n+common_html_entity = Regex(\n+ lambda: f\"&(?P<entity>{_most_common_entities}|{make_compressed_re(_htmlEntityMap)});\"\n+).set_name(\"common HTML entity\")\n \n \n def replace_html_entity(s, l, t):\n@@ -664,16 +668,16 @@ class OpAssoc(Enum):\n \n \n InfixNotationOperatorArgType = Union[\n- ParserElement, str, Tuple[Union[ParserElement, str], Union[ParserElement, str]]\n+ ParserElement, str, tuple[Union[ParserElement, str], Union[ParserElement, str]]\n ]\n InfixNotationOperatorSpec = Union[\n- Tuple[\n+ tuple[\n InfixNotationOperatorArgType,\n int,\n OpAssoc,\n typing.Optional[ParseAction],\n ],\n- Tuple[\n+ tuple[\n InfixNotationOperatorArgType,\n int,\n OpAssoc,\n@@ -683,7 +687,7 @@ class OpAssoc(Enum):\n \n def infix_notation(\n base_expr: ParserElement,\n- op_list: List[InfixNotationOperatorSpec],\n+ op_list: list[InfixNotationOperatorSpec],\n lpar: Union[str, ParserElement] = Suppress(\"(\"),\n rpar: Union[str, ParserElement] = Suppress(\")\"),\n ) -> ParserElement:\n@@ -782,9 +786,12 @@ def parseImpl(self, instring, loc, doActions=True):\n \n # if lpar and rpar are not suppressed, wrap in group\n if not (isinstance(lpar, Suppress) and isinstance(rpar, Suppress)):\n- lastExpr = base_expr | Group(lpar + ret + rpar)\n+ lastExpr = base_expr | Group(lpar + ret + rpar).set_name(\n+ f\"nested_{base_expr.name}\"\n+ )\n else:\n- lastExpr = base_expr | (lpar + ret + rpar)\n+ lastExpr = base_expr | (lpar + ret + rpar).set_name(f\"nested_{base_expr.name}\")\n+ root_expr = lastExpr\n \n arity: int\n rightLeftAssoc: opAssoc\n@@ -855,6 +862,7 @@ def parseImpl(self, instring, loc, doActions=True):\n thisExpr <<= (matchExpr | lastExpr).setName(term_name)\n lastExpr = thisExpr\n ret <<= lastExpr\n+ root_expr.set_name(\"base_expr\")\n return ret\n \n \n@@ -1028,7 +1036,7 @@ def checkUnindent(s, l, t):\n \n # build list of built-in expressions, for future reference if a global default value\n # gets updated\n-_builtin_exprs: List[ParserElement] = [\n+_builtin_exprs: list[ParserElement] = [\n v for v in vars().values() if isinstance(v, ParserElement)\n ]\n \n@@ -1049,7 +1057,7 @@ def delimited_list(\n )\n \n \n-# pre-PEP8 compatible names\n+# Compatibility synonyms\n # fmt: off\n opAssoc = OpAssoc\n anyOpenTag = any_open_tag"}, {"sha": "245847832a8f5a975a8c3d56d390385923508dcd", "filename": "lib/pyparsing/results.py", "status": "modified", "additions": 51, "deletions": 33, "changes": 84, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpyparsing%2Fresults.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpyparsing%2Fresults.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpyparsing%2Fresults.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,25 +1,30 @@\n # results.py\n+from __future__ import annotations\n+\n+import collections\n from collections.abc import (\n MutableMapping,\n Mapping,\n MutableSequence,\n Iterator,\n- Sequence,\n- Container,\n+ Iterable,\n )\n import pprint\n-from typing import Tuple, Any, Dict, Set, List\n+from typing import Any\n+\n+from .util import replaced_by_pep8\n+\n \n-str_type: Tuple[type, ...] = (str, bytes)\n+str_type: tuple[type, ...] = (str, bytes)\n _generator_type = type((_ for _ in ()))\n \n \n class _ParseResultsWithOffset:\n- tup: Tuple[\"ParseResults\", int]\n+ tup: tuple[ParseResults, int]\n __slots__ = [\"tup\"]\n \n- def __init__(self, p1: \"ParseResults\", p2: int):\n- self.tup: Tuple[ParseResults, int] = (p1, p2)\n+ def __init__(self, p1: ParseResults, p2: int):\n+ self.tup: tuple[ParseResults, int] = (p1, p2)\n \n def __getitem__(self, i):\n return self.tup[i]\n@@ -77,14 +82,14 @@ def test(s, fn=repr):\n - year: '1999'\n \"\"\"\n \n- _null_values: Tuple[Any, ...] = (None, [], ())\n+ _null_values: tuple[Any, ...] = (None, [], ())\n \n _name: str\n- _parent: \"ParseResults\"\n- _all_names: Set[str]\n+ _parent: ParseResults\n+ _all_names: set[str]\n _modal: bool\n- _toklist: List[Any]\n- _tokdict: Dict[str, Any]\n+ _toklist: list[Any]\n+ _tokdict: dict[str, Any]\n \n __slots__ = (\n \"_name\",\n@@ -170,8 +175,8 @@ def __new__(cls, toklist=None, name=None, **kwargs):\n # constructor as small and fast as possible\n def __init__(\n self, toklist=None, name=None, asList=True, modal=True, isinstance=isinstance\n- ):\n- self._tokdict: Dict[str, _ParseResultsWithOffset]\n+ ) -> None:\n+ self._tokdict: dict[str, _ParseResultsWithOffset]\n self._modal = modal\n \n if name is None or name == \"\":\n@@ -224,7 +229,7 @@ def __setitem__(self, k, v, isinstance=isinstance):\n self._toklist[k] = v\n sub = v\n else:\n- self._tokdict[k] = self._tokdict.get(k, list()) + [\n+ self._tokdict[k] = self._tokdict.get(k, []) + [\n _ParseResultsWithOffset(v, 0)\n ]\n sub = v\n@@ -441,12 +446,12 @@ def __getattr__(self, name):\n raise AttributeError(name)\n return \"\"\n \n- def __add__(self, other: \"ParseResults\") -> \"ParseResults\":\n+ def __add__(self, other: ParseResults) -> ParseResults:\n ret = self.copy()\n ret += other\n return ret\n \n- def __iadd__(self, other: \"ParseResults\") -> \"ParseResults\":\n+ def __iadd__(self, other: ParseResults) -> ParseResults:\n if not other:\n return self\n \n@@ -468,7 +473,7 @@ def __iadd__(self, other: \"ParseResults\") -> \"ParseResults\":\n self._all_names |= other._all_names\n return self\n \n- def __radd__(self, other) -> \"ParseResults\":\n+ def __radd__(self, other) -> ParseResults:\n if isinstance(other, int) and other == 0:\n # useful for merging many ParseResults using sum() builtin\n return self.copy()\n@@ -502,9 +507,10 @@ def _asStringList(self, sep=\"\"):\n out.append(str(item))\n return out\n \n- def as_list(self) -> list:\n+ def as_list(self, *, flatten: bool = False) -> list:\n \"\"\"\n Returns the parse results as a nested list of matching tokens, all converted to strings.\n+ If flatten is True, all the nesting levels in the returned list are collapsed.\n \n Example::\n \n@@ -517,10 +523,22 @@ def as_list(self) -> list:\n result_list = result.as_list()\n print(type(result_list), result_list) # -> <class 'list'> ['sldkj', 'lsdkj', 'sldkj']\n \"\"\"\n- return [\n- res.as_list() if isinstance(res, ParseResults) else res\n- for res in self._toklist\n- ]\n+ def flattened(pr):\n+ to_visit = collections.deque([*self])\n+ while to_visit:\n+ to_do = to_visit.popleft()\n+ if isinstance(to_do, ParseResults):\n+ to_visit.extendleft(to_do[::-1])\n+ else:\n+ yield to_do\n+\n+ if flatten:\n+ return [*flattened(self)]\n+ else:\n+ return [\n+ res.as_list() if isinstance(res, ParseResults) else res\n+ for res in self._toklist\n+ ]\n \n def as_dict(self) -> dict:\n \"\"\"\n@@ -551,7 +569,7 @@ def to_item(obj):\n \n return dict((k, to_item(v)) for k, v in self.items())\n \n- def copy(self) -> \"ParseResults\":\n+ def copy(self) -> ParseResults:\n \"\"\"\n Returns a new shallow copy of a :class:`ParseResults` object. `ParseResults`\n items contained within the source are shared with the copy. Use\n@@ -565,28 +583,28 @@ def copy(self) -> \"ParseResults\":\n ret._name = self._name\n return ret\n \n- def deepcopy(self) -> \"ParseResults\":\n+ def deepcopy(self) -> ParseResults:\n \"\"\"\n Returns a new deep copy of a :class:`ParseResults` object.\n \"\"\"\n ret = self.copy()\n # replace values with copies if they are of known mutable types\n for i, obj in enumerate(self._toklist):\n if isinstance(obj, ParseResults):\n- self._toklist[i] = obj.deepcopy()\n+ ret._toklist[i] = obj.deepcopy()\n elif isinstance(obj, (str, bytes)):\n pass\n elif isinstance(obj, MutableMapping):\n- self._toklist[i] = dest = type(obj)()\n+ ret._toklist[i] = dest = type(obj)()\n for k, v in obj.items():\n dest[k] = v.deepcopy() if isinstance(v, ParseResults) else v\n- elif isinstance(obj, Container):\n- self._toklist[i] = type(obj)(\n- v.deepcopy() if isinstance(v, ParseResults) else v for v in obj\n+ elif isinstance(obj, Iterable):\n+ ret._toklist[i] = type(obj)(\n+ v.deepcopy() if isinstance(v, ParseResults) else v for v in obj # type: ignore[call-arg]\n )\n return ret\n \n- def get_name(self):\n+ def get_name(self) -> str | None:\n r\"\"\"\n Returns the results name for this token expression. Useful when several\n different expressions might match at a particular location.\n@@ -614,7 +632,7 @@ def get_name(self):\n if self._name:\n return self._name\n elif self._parent:\n- par: \"ParseResults\" = self._parent\n+ par: ParseResults = self._parent\n parent_tokdict_items = par._tokdict.items()\n return next(\n (\n@@ -759,7 +777,7 @@ def __dir__(self):\n return dir(type(self)) + list(self.keys())\n \n @classmethod\n- def from_dict(cls, other, name=None) -> \"ParseResults\":\n+ def from_dict(cls, other, name=None) -> ParseResults:\n \"\"\"\n Helper classmethod to construct a ``ParseResults`` from a ``dict``, preserving the\n name-value relations as results names. If an optional ``name`` argument is"}, {"sha": "836b2f86fbeb0ad7c12f36996f4099bcf96e492a", "filename": "lib/pyparsing/testing.py", "status": "modified", "additions": 28, "deletions": 11, "changes": 39, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpyparsing%2Ftesting.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpyparsing%2Ftesting.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpyparsing%2Ftesting.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -257,10 +257,14 @@ def with_line_numbers(\n eol_mark: str = \"|\",\n mark_spaces: typing.Optional[str] = None,\n mark_control: typing.Optional[str] = None,\n+ *,\n+ indent: typing.Union[str, int] = \"\",\n+ base_1: bool = True,\n ) -> str:\n \"\"\"\n Helpful method for debugging a parser - prints a string with line and column numbers.\n- (Line and column numbers are 1-based.)\n+ (Line and column numbers are 1-based by default - if debugging a parse action,\n+ pass base_1=False, to correspond to the loc value passed to the parse action.)\n \n :param s: tuple(bool, str - string to be printed with line and column numbers\n :param start_line: int - (optional) starting line number in s to print (default=1)\n@@ -273,11 +277,18 @@ def with_line_numbers(\n - \"unicode\" - replaces control chars with Unicode symbols, such as \"\u240d\" and \"\u240a\"\n - any single character string - replace control characters with given string\n - None (default) - string is displayed as-is\n+ :param indent: str | int - (optional) string to indent with line and column numbers; if an int\n+ is passed, converted to \" \" * indent\n+ :param base_1: bool - (optional) whether to label string using base 1; if False, string will be\n+ labeled based at 0 (default=True)\n \n :return: str - input string with leading line numbers and column number headers\n \"\"\"\n if expand_tabs:\n s = s.expandtabs()\n+ if isinstance(indent, int):\n+ indent = \" \" * indent\n+ indent = indent.expandtabs()\n if mark_control is not None:\n mark_control = typing.cast(str, mark_control)\n if mark_control == \"unicode\":\n@@ -300,46 +311,52 @@ def with_line_numbers(\n else:\n s = s.replace(\" \", mark_spaces)\n if start_line is None:\n- start_line = 1\n+ start_line = 0\n if end_line is None:\n end_line = len(s)\n end_line = min(end_line, len(s))\n- start_line = min(max(1, start_line), end_line)\n+ start_line = min(max(0, start_line), end_line)\n \n if mark_control != \"unicode\":\n- s_lines = s.splitlines()[start_line - 1 : end_line]\n+ s_lines = s.splitlines()[start_line - base_1 : end_line]\n else:\n- s_lines = [line + \"\u240a\" for line in s.split(\"\u240a\")[start_line - 1 : end_line]]\n+ s_lines = [\n+ line + \"\u240a\" for line in s.split(\"\u240a\")[start_line - base_1 : end_line]\n+ ]\n if not s_lines:\n return \"\"\n \n lineno_width = len(str(end_line))\n max_line_len = max(len(line) for line in s_lines)\n- lead = \" \" * (lineno_width + 1)\n+ lead = indent + \" \" * (lineno_width + 1)\n if max_line_len >= 99:\n header0 = (\n lead\n+ + (\"\" if base_1 else \" \")\n + \"\".join(\n f\"{' ' * 99}{(i + 1) % 100}\"\n- for i in range(max(max_line_len // 100, 1))\n+ for i in range(1 if base_1 else 0, max(max_line_len // 100, 1))\n )\n + \"\\n\"\n )\n else:\n header0 = \"\"\n header1 = (\n- header0\n+ (\"\" if base_1 else \" \")\n + lead\n + \"\".join(f\" {(i + 1) % 10}\" for i in range(-(-max_line_len // 10)))\n + \"\\n\"\n )\n- header2 = lead + \"1234567890\" * (-(-max_line_len // 10)) + \"\\n\"\n+ digits = \"1234567890\"\n+ header2 = (\n+ lead + (\"\" if base_1 else \"0\") + digits * (-(-max_line_len // 10)) + \"\\n\"\n+ )\n return (\n header1\n + header2\n + \"\\n\".join(\n- f\"{i:{lineno_width}d}:{line}{eol_mark}\"\n- for i, line in enumerate(s_lines, start=start_line)\n+ f\"{indent}{i:{lineno_width}d}:{line}{eol_mark}\"\n+ for i, line in enumerate(s_lines, start=start_line + base_1)\n )\n + \"\\n\"\n )"}, {"sha": "066486c28eea020d420c2e90fdda76f69f1c9ead", "filename": "lib/pyparsing/unicode.py", "status": "modified", "additions": 20, "deletions": 18, "changes": 38, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpyparsing%2Funicode.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpyparsing%2Funicode.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpyparsing%2Funicode.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -2,7 +2,7 @@\n \n import sys\n from itertools import filterfalse\n-from typing import List, Tuple, Union\n+from typing import Union\n \n \n class _lazyclassproperty:\n@@ -25,7 +25,7 @@ def __get__(self, obj, cls):\n return cls._intern[attrname]\n \n \n-UnicodeRangeList = List[Union[Tuple[int, int], Tuple[int]]]\n+UnicodeRangeList = list[Union[tuple[int, int], tuple[int]]]\n \n \n class unicode_set:\n@@ -53,59 +53,61 @@ class CJK(Chinese, Japanese, Korean):\n _ranges: UnicodeRangeList = []\n \n @_lazyclassproperty\n- def _chars_for_ranges(cls):\n- ret = []\n- for cc in cls.__mro__:\n+ def _chars_for_ranges(cls) -> list[str]:\n+ ret: list[int] = []\n+ for cc in cls.__mro__: # type: ignore[attr-defined]\n if cc is unicode_set:\n break\n for rr in getattr(cc, \"_ranges\", ()):\n ret.extend(range(rr[0], rr[-1] + 1))\n- return [chr(c) for c in sorted(set(ret))]\n+ return sorted(chr(c) for c in set(ret))\n \n @_lazyclassproperty\n- def printables(cls):\n+ def printables(cls) -> str:\n \"\"\"all non-whitespace characters in this range\"\"\"\n return \"\".join(filterfalse(str.isspace, cls._chars_for_ranges))\n \n @_lazyclassproperty\n- def alphas(cls):\n+ def alphas(cls) -> str:\n \"\"\"all alphabetic characters in this range\"\"\"\n return \"\".join(filter(str.isalpha, cls._chars_for_ranges))\n \n @_lazyclassproperty\n- def nums(cls):\n+ def nums(cls) -> str:\n \"\"\"all numeric digit characters in this range\"\"\"\n return \"\".join(filter(str.isdigit, cls._chars_for_ranges))\n \n @_lazyclassproperty\n- def alphanums(cls):\n+ def alphanums(cls) -> str:\n \"\"\"all alphanumeric characters in this range\"\"\"\n return cls.alphas + cls.nums\n \n @_lazyclassproperty\n- def identchars(cls):\n+ def identchars(cls) -> str:\n \"\"\"all characters in this range that are valid identifier characters, plus underscore '_'\"\"\"\n return \"\".join(\n sorted(\n- set(\n- \"\".join(filter(str.isidentifier, cls._chars_for_ranges))\n- + \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\u00aa\u00b5\u00ba\"\n- + \"\u00c0\u00c1\u00c2\u00c3\u00c4\u00c5\u00c6\u00c7\u00c8\u00c9\u00ca\u00cb\u00cc\u00cd\u00ce\u00cf\u00d0\u00d1\u00d2\u00d3\u00d4\u00d5\u00d6\u00d8\u00d9\u00da\u00db\u00dc\u00dd\u00de\u00df\u00e0\u00e1\u00e2\u00e3\u00e4\u00e5\u00e6\u00e7\u00e8\u00e9\u00ea\u00eb\u00ec\u00ed\u00ee\u00ef\u00f0\u00f1\u00f2\u00f3\u00f4\u00f5\u00f6\u00f8\u00f9\u00fa\u00fb\u00fc\u00fd\u00fe\u00ff\"\n- + \"_\"\n+ set(filter(str.isidentifier, cls._chars_for_ranges))\n+ | set(\n+ \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\u00aa\u00b5\u00ba\"\n+ \"\u00c0\u00c1\u00c2\u00c3\u00c4\u00c5\u00c6\u00c7\u00c8\u00c9\u00ca\u00cb\u00cc\u00cd\u00ce\u00cf\u00d0\u00d1\u00d2\u00d3\u00d4\u00d5\u00d6\u00d8\u00d9\u00da\u00db\u00dc\u00dd\u00de\u00df\u00e0\u00e1\u00e2\u00e3\u00e4\u00e5\u00e6\u00e7\u00e8\u00e9\u00ea\u00eb\u00ec\u00ed\u00ee\u00ef\u00f0\u00f1\u00f2\u00f3\u00f4\u00f5\u00f6\u00f8\u00f9\u00fa\u00fb\u00fc\u00fd\u00fe\u00ff\"\n+ \"_\"\n )\n )\n )\n \n @_lazyclassproperty\n- def identbodychars(cls):\n+ def identbodychars(cls) -> str:\n \"\"\"\n all characters in this range that are valid identifier body characters,\n plus the digits 0-9, and \u00b7 (Unicode MIDDLE DOT)\n \"\"\"\n identifier_chars = set(\n c for c in cls._chars_for_ranges if (\"_\" + c).isidentifier()\n )\n- return \"\".join(sorted(identifier_chars | set(cls.identchars + \"0123456789\u00b7\")))\n+ return \"\".join(\n+ sorted(identifier_chars | set(cls.identchars) | set(\"0123456789\u00b7\"))\n+ )\n \n @_lazyclassproperty\n def identifier(cls):"}, {"sha": "1487019c272d48cfa43a372cf27f838672074db2", "filename": "lib/pyparsing/util.py", "status": "modified", "additions": 167, "deletions": 47, "changes": 214, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpyparsing%2Futil.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpyparsing%2Futil.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpyparsing%2Futil.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,11 +1,11 @@\n # util.py\n+import contextlib\n+from functools import lru_cache, wraps\n import inspect\n-import warnings\n-import types\n-import collections\n import itertools\n-from functools import lru_cache, wraps\n-from typing import Callable, List, Union, Iterable, TypeVar, cast\n+import types\n+from typing import Callable, Union, Iterable, TypeVar, cast\n+import warnings\n \n _bslash = chr(92)\n C = TypeVar(\"C\", bound=Callable)\n@@ -14,8 +14,8 @@\n class __config_flags:\n \"\"\"Internal class for defining compatibility and debugging flags\"\"\"\n \n- _all_names: List[str] = []\n- _fixed_names: List[str] = []\n+ _all_names: list[str] = []\n+ _fixed_names: list[str] = []\n _type_desc = \"configuration\"\n \n @classmethod\n@@ -100,27 +100,24 @@ def clear(_):\n \n class _FifoCache:\n def __init__(self, size):\n- self.not_in_cache = not_in_cache = object()\n cache = {}\n- keyring = [object()] * size\n+ self.size = size\n+ self.not_in_cache = not_in_cache = object()\n cache_get = cache.get\n cache_pop = cache.pop\n- keyiter = itertools.cycle(range(size))\n \n def get(_, key):\n return cache_get(key, not_in_cache)\n \n def set_(_, key, value):\n cache[key] = value\n- i = next(keyiter)\n- cache_pop(keyring[i], None)\n- keyring[i] = key\n+ while len(cache) > size:\n+ # pop oldest element in cache by getting the first key\n+ cache_pop(next(iter(cache)))\n \n def clear(_):\n cache.clear()\n- keyring[:] = [object()] * size\n \n- self.size = size\n self.get = types.MethodType(get, self)\n self.set = types.MethodType(set_, self)\n self.clear = types.MethodType(clear, self)\n@@ -137,13 +134,13 @@ class LRUMemo:\n def __init__(self, capacity):\n self._capacity = capacity\n self._active = {}\n- self._memory = collections.OrderedDict()\n+ self._memory = {}\n \n def __getitem__(self, key):\n try:\n return self._active[key]\n except KeyError:\n- self._memory.move_to_end(key)\n+ self._memory[key] = self._memory.pop(key)\n return self._memory[key]\n \n def __setitem__(self, key, value):\n@@ -156,8 +153,9 @@ def __delitem__(self, key):\n except KeyError:\n pass\n else:\n- while len(self._memory) >= self._capacity:\n- self._memory.popitem(last=False)\n+ oldest_keys = list(self._memory)[: -(self._capacity + 1)]\n+ for key_to_delete in oldest_keys:\n+ self._memory.pop(key_to_delete)\n self._memory[key] = value\n \n def clear(self):\n@@ -183,60 +181,182 @@ def _escape_regex_range_chars(s: str) -> str:\n return str(s)\n \n \n+class _GroupConsecutive:\n+ \"\"\"\n+ Used as a callable `key` for itertools.groupby to group\n+ characters that are consecutive:\n+ itertools.groupby(\"abcdejkmpqrs\", key=IsConsecutive())\n+ yields:\n+ (0, iter(['a', 'b', 'c', 'd', 'e']))\n+ (1, iter(['j', 'k']))\n+ (2, iter(['m']))\n+ (3, iter(['p', 'q', 'r', 's']))\n+ \"\"\"\n+ def __init__(self):\n+ self.prev = 0\n+ self.counter = itertools.count()\n+ self.value = -1\n+\n+ def __call__(self, char: str) -> int:\n+ c_int = ord(char)\n+ self.prev, prev = c_int, self.prev\n+ if c_int - prev > 1:\n+ self.value = next(self.counter)\n+ return self.value\n+\n+\n def _collapse_string_to_ranges(\n s: Union[str, Iterable[str]], re_escape: bool = True\n ) -> str:\n- def is_consecutive(c):\n- c_int = ord(c)\n- is_consecutive.prev, prev = c_int, is_consecutive.prev\n- if c_int - prev > 1:\n- is_consecutive.value = next(is_consecutive.counter)\n- return is_consecutive.value\n+ r\"\"\"\n+ Take a string or list of single-character strings, and return\n+ a string of the consecutive characters in that string collapsed\n+ into groups, as might be used in a regular expression '[a-z]'\n+ character set:\n+ 'a' -> 'a' -> '[a]'\n+ 'bc' -> 'bc' -> '[bc]'\n+ 'defgh' -> 'd-h' -> '[d-h]'\n+ 'fdgeh' -> 'd-h' -> '[d-h]'\n+ 'jklnpqrtu' -> 'j-lnp-rtu' -> '[j-lnp-rtu]'\n+ Duplicates get collapsed out:\n+ 'aaa' -> 'a' -> '[a]'\n+ 'bcbccb' -> 'bc' -> '[bc]'\n+ 'defghhgf' -> 'd-h' -> '[d-h]'\n+ 'jklnpqrjjjtu' -> 'j-lnp-rtu' -> '[j-lnp-rtu]'\n+ Spaces are preserved:\n+ 'ab c' -> ' a-c' -> '[ a-c]'\n+ Characters that are significant when defining regex ranges\n+ get escaped:\n+ 'acde[]-' -> r'\\-\\[\\]ac-e' -> r'[\\-\\[\\]ac-e]'\n+ \"\"\"\n \n- is_consecutive.prev = 0 # type: ignore [attr-defined]\n- is_consecutive.counter = itertools.count() # type: ignore [attr-defined]\n- is_consecutive.value = -1 # type: ignore [attr-defined]\n+ # Developer notes:\n+ # - Do not optimize this code assuming that the given input string\n+ # or internal lists will be short (such as in loading generators into\n+ # lists to make it easier to find the last element); this method is also\n+ # used to generate regex ranges for character sets in the pyparsing.unicode\n+ # classes, and these can be _very_ long lists of strings\n \n- def escape_re_range_char(c):\n+ def escape_re_range_char(c: str) -> str:\n return \"\\\\\" + c if c in r\"\\^-][\" else c\n \n- def no_escape_re_range_char(c):\n+ def no_escape_re_range_char(c: str) -> str:\n return c\n \n if not re_escape:\n escape_re_range_char = no_escape_re_range_char\n \n ret = []\n- s = \"\".join(sorted(set(s)))\n- if len(s) > 3:\n- for _, chars in itertools.groupby(s, key=is_consecutive):\n+\n+ # reduce input string to remove duplicates, and put in sorted order\n+ s_chars: list[str] = sorted(set(s))\n+\n+ if len(s_chars) > 2:\n+ # find groups of characters that are consecutive (can be collapsed\n+ # down to \"<first>-<last>\")\n+ for _, chars in itertools.groupby(s_chars, key=_GroupConsecutive()):\n+ # _ is unimportant, is just used to identify groups\n+ # chars is an iterator of one or more consecutive characters\n+ # that comprise the current group\n first = last = next(chars)\n- last = collections.deque(\n- itertools.chain(iter([last]), chars), maxlen=1\n- ).pop()\n+ with contextlib.suppress(ValueError):\n+ *_, last = chars\n+\n if first == last:\n+ # there was only a single char in this group\n ret.append(escape_re_range_char(first))\n+\n+ elif last == chr(ord(first) + 1):\n+ # there were only 2 characters in this group\n+ # 'a','b' -> 'ab'\n+ ret.append(f\"{escape_re_range_char(first)}{escape_re_range_char(last)}\")\n+\n else:\n- sep = \"\" if ord(last) == ord(first) + 1 else \"-\"\n+ # there were > 2 characters in this group, make into a range\n+ # 'c','d','e' -> 'c-e'\n ret.append(\n- f\"{escape_re_range_char(first)}{sep}{escape_re_range_char(last)}\"\n+ f\"{escape_re_range_char(first)}-{escape_re_range_char(last)}\"\n )\n else:\n- ret = [escape_re_range_char(c) for c in s]\n+ # only 1 or 2 chars were given to form into groups\n+ # 'a' -> ['a']\n+ # 'bc' -> ['b', 'c']\n+ # 'dg' -> ['d', 'g']\n+ # no need to list them with \"-\", just return as a list\n+ # (after escaping)\n+ ret = [escape_re_range_char(c) for c in s_chars]\n \n return \"\".join(ret)\n \n \n-def _flatten(ll: list) -> list:\n+def _flatten(ll: Iterable) -> list:\n ret = []\n- for i in ll:\n- if isinstance(i, list):\n- ret.extend(_flatten(i))\n+ to_visit = [*ll]\n+ while to_visit:\n+ i = to_visit.pop(0)\n+ if isinstance(i, Iterable) and not isinstance(i, str):\n+ to_visit[:0] = i\n else:\n ret.append(i)\n return ret\n \n \n+def make_compressed_re(\n+ word_list: Iterable[str], max_level: int = 2, _level: int = 1\n+) -> str:\n+ \"\"\"\n+ Create a regular expression string from a list of words, collapsing by common\n+ prefixes and optional suffixes.\n+\n+ Calls itself recursively to build nested sublists for each group of suffixes\n+ that have a shared prefix.\n+ \"\"\"\n+\n+ def get_suffixes_from_common_prefixes(namelist: list[str]):\n+ if len(namelist) > 1:\n+ for prefix, suffixes in itertools.groupby(namelist, key=lambda s: s[:1]):\n+ yield prefix, sorted([s[1:] for s in suffixes], key=len, reverse=True)\n+ else:\n+ yield namelist[0][0], [namelist[0][1:]]\n+\n+ if max_level == 0:\n+ return \"|\".join(sorted(word_list, key=len, reverse=True))\n+\n+ ret = []\n+ sep = \"\"\n+ for initial, suffixes in get_suffixes_from_common_prefixes(sorted(word_list)):\n+ ret.append(sep)\n+ sep = \"|\"\n+\n+ trailing = \"\"\n+ if \"\" in suffixes:\n+ trailing = \"?\"\n+ suffixes.remove(\"\")\n+\n+ if len(suffixes) > 1:\n+ if all(len(s) == 1 for s in suffixes):\n+ ret.append(f\"{initial}[{''.join(suffixes)}]{trailing}\")\n+ else:\n+ if _level < max_level:\n+ suffix_re = make_compressed_re(\n+ sorted(suffixes), max_level, _level + 1\n+ )\n+ ret.append(f\"{initial}({suffix_re}){trailing}\")\n+ else:\n+ suffixes.sort(key=len, reverse=True)\n+ ret.append(f\"{initial}({'|'.join(suffixes)}){trailing}\")\n+ else:\n+ if suffixes:\n+ suffix = suffixes[0]\n+ if len(suffix) > 1 and trailing:\n+ ret.append(f\"{initial}({suffix}){trailing}\")\n+ else:\n+ ret.append(f\"{initial}{suffix}{trailing}\")\n+ else:\n+ ret.append(initial)\n+ return \"\".join(ret)\n+\n+\n def replaced_by_pep8(compat_name: str, fn: C) -> C:\n # In a future version, uncomment the code in the internal _inner() functions\n # to begin emitting DeprecationWarnings.\n@@ -246,7 +366,7 @@ def replaced_by_pep8(compat_name: str, fn: C) -> C:\n \n # (Presence of 'self' arg in signature is used by explain_exception() methods, so we take\n # some extra steps to add it if present in decorated function.)\n- if \"self\" == list(inspect.signature(fn).parameters)[0]:\n+ if [\"self\"] == list(inspect.signature(fn).parameters)[:1]:\n \n @wraps(fn)\n def _inner(self, *args, **kwargs):\n@@ -268,10 +388,10 @@ def _inner(*args, **kwargs):\n _inner.__name__ = compat_name\n _inner.__annotations__ = fn.__annotations__\n if isinstance(fn, types.FunctionType):\n- _inner.__kwdefaults__ = fn.__kwdefaults__\n+ _inner.__kwdefaults__ = fn.__kwdefaults__ # type: ignore [attr-defined]\n elif isinstance(fn, type) and hasattr(fn, \"__init__\"):\n- _inner.__kwdefaults__ = fn.__init__.__kwdefaults__\n+ _inner.__kwdefaults__ = fn.__init__.__kwdefaults__ # type: ignore [misc,attr-defined]\n else:\n- _inner.__kwdefaults__ = None\n+ _inner.__kwdefaults__ = None # type: ignore [attr-defined]\n _inner.__qualname__ = fn.__qualname__\n return cast(C, _inner)"}, {"sha": "96409e2d0846ba7c9108386d1f2cdf1e74bee9ea", "filename": "lib/pytz/__init__.py", "status": "modified", "additions": 2, "deletions": 3, "changes": 5, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2F__init__.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2F__init__.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2F__init__.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -22,8 +22,8 @@\n \n \n # The IANA (nee Olson) database is updated several times a year.\n-OLSON_VERSION = '2024a'\n-VERSION = '2024.1' # pip compatible version number.\n+OLSON_VERSION = '2024b'\n+VERSION = '2024.2' # pip compatible version number.\n __version__ = VERSION\n \n OLSEN_VERSION = OLSON_VERSION # Old releases had this misspelling\n@@ -1340,7 +1340,6 @@ def _test():\n 'Asia/Bishkek',\n 'Asia/Brunei',\n 'Asia/Chita',\n- 'Asia/Choibalsan',\n 'Asia/Colombo',\n 'Asia/Damascus',\n 'Asia/Dhaka',"}, {"sha": "65f043f1d1833d26b5fe42a460f84b35d1dcba63", "filename": "lib/pytz/zoneinfo/Africa/Blantyre", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAfrica%2FBlantyre", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAfrica%2FBlantyre", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FAfrica%2FBlantyre?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "65f043f1d1833d26b5fe42a460f84b35d1dcba63", "filename": "lib/pytz/zoneinfo/Africa/Bujumbura", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAfrica%2FBujumbura", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAfrica%2FBujumbura", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FAfrica%2FBujumbura?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "65f043f1d1833d26b5fe42a460f84b35d1dcba63", "filename": "lib/pytz/zoneinfo/Africa/Gaborone", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAfrica%2FGaborone", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAfrica%2FGaborone", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FAfrica%2FGaborone?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "65f043f1d1833d26b5fe42a460f84b35d1dcba63", "filename": "lib/pytz/zoneinfo/Africa/Harare", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAfrica%2FHarare", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAfrica%2FHarare", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FAfrica%2FHarare?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "65f043f1d1833d26b5fe42a460f84b35d1dcba63", "filename": "lib/pytz/zoneinfo/Africa/Kigali", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAfrica%2FKigali", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAfrica%2FKigali", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FAfrica%2FKigali?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "65f043f1d1833d26b5fe42a460f84b35d1dcba63", "filename": "lib/pytz/zoneinfo/Africa/Lubumbashi", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAfrica%2FLubumbashi", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAfrica%2FLubumbashi", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FAfrica%2FLubumbashi?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "65f043f1d1833d26b5fe42a460f84b35d1dcba63", "filename": "lib/pytz/zoneinfo/Africa/Lusaka", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAfrica%2FLusaka", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAfrica%2FLusaka", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FAfrica%2FLusaka?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "65f043f1d1833d26b5fe42a460f84b35d1dcba63", "filename": "lib/pytz/zoneinfo/Africa/Maputo", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAfrica%2FMaputo", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAfrica%2FMaputo", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FAfrica%2FMaputo?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "3a493e3d5a93f08201c41cb25616015963254589", "filename": "lib/pytz/zoneinfo/America/Bahia_Banderas", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FBahia_Banderas", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FBahia_Banderas", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FBahia_Banderas?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "0f7771e851b0e4242bcc65a9b838b8a1675541fb", "filename": "lib/pytz/zoneinfo/America/Cancun", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FCancun", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FCancun", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FCancun?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "667a2191145170e34271bb2200b93a2eec41d2ba", "filename": "lib/pytz/zoneinfo/America/Chihuahua", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FChihuahua", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FChihuahua", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FChihuahua?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "29af5982ac1c04357634570f15454d6f7fc30e04", "filename": "lib/pytz/zoneinfo/America/Ciudad_Juarez", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FCiudad_Juarez", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FCiudad_Juarez", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FCiudad_Juarez?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "0fe73912cacab6c8aabef4f4264d1710825888e7", "filename": "lib/pytz/zoneinfo/America/Ensenada", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FEnsenada", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FEnsenada", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FEnsenada?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "441fe3f2dd133fd081247131d860bac327107499", "filename": "lib/pytz/zoneinfo/America/Hermosillo", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FHermosillo", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FHermosillo", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FHermosillo?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "386616a59cdb2a21f1f24a6c72526bce0360cd24", "filename": "lib/pytz/zoneinfo/America/Mazatlan", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FMazatlan", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FMazatlan", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FMazatlan?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "c4b9b4e8801cd9447d3bbb6ebff71e26df669c3c", "filename": "lib/pytz/zoneinfo/America/Merida", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FMerida", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FMerida", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FMerida?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "ad70cf3e08498845aaafc38e4eccca7461c8d337", "filename": "lib/pytz/zoneinfo/America/Mexico_City", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FMexico_City", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FMexico_City", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FMexico_City?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "2d7993a0d71720493175bd05b2b0b7409d67407a", "filename": "lib/pytz/zoneinfo/America/Monterrey", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FMonterrey", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FMonterrey", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FMonterrey?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "45118a4f4b73e55879aaa81a8536e7214538f0e8", "filename": "lib/pytz/zoneinfo/America/Ojinaga", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FOjinaga", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FOjinaga", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FOjinaga?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "0fe73912cacab6c8aabef4f4264d1710825888e7", "filename": "lib/pytz/zoneinfo/America/Santa_Isabel", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FSanta_Isabel", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FSanta_Isabel", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FSanta_Isabel?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "0fe73912cacab6c8aabef4f4264d1710825888e7", "filename": "lib/pytz/zoneinfo/America/Tijuana", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FTijuana", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FTijuana", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FAmerica%2FTijuana?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "2aa5cc4b84d369b03684c30cdb66b7abf087f467", "filename": "lib/pytz/zoneinfo/Asia/Choibalsan", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAsia%2FChoibalsan", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAsia%2FChoibalsan", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FAsia%2FChoibalsan?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "4614e4fc4353a7094ba5173a0bd09158ef49d9a9", "filename": "lib/pytz/zoneinfo/Asia/Dili", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAsia%2FDili", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAsia%2FDili", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FAsia%2FDili?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "dd2c235bf9c4cc3c2ec4c2725a7cd5deac128fea", "filename": "lib/pytz/zoneinfo/Atlantic/Azores", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAtlantic%2FAzores", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAtlantic%2FAzores", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FAtlantic%2FAzores?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "6725a0ffc1f7cfc6cad54361ca91500e087e2b64", "filename": "lib/pytz/zoneinfo/Atlantic/Madeira", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAtlantic%2FMadeira", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FAtlantic%2FMadeira", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FAtlantic%2FMadeira?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "40d7124e5346af056c75e2f7012a51d94e8154b7", "filename": "lib/pytz/zoneinfo/CET", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FCET", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FCET", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FCET?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "c6981a06b1d9c26f447518efe265a6454726eae7", "filename": "lib/pytz/zoneinfo/CST6CDT", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FCST6CDT", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FCST6CDT", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FCST6CDT?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "9f3a0678d766881389e129c93def7fffd74f14f1", "filename": "lib/pytz/zoneinfo/EET", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FEET", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FEET", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FEET?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "9964b9a33452f4b636f43703b7cdec4891cbda5f", "filename": "lib/pytz/zoneinfo/EST", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FEST", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FEST", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FEST?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "a8b9ab1992257d721ad627b14f535c3d4b020888", "filename": "lib/pytz/zoneinfo/EST5EDT", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FEST5EDT", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FEST5EDT", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FEST5EDT?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "616de167b8a1c534c1c526e7bbff82f7d5da94f2", "filename": "lib/pytz/zoneinfo/Europe/Lisbon", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FEurope%2FLisbon", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FEurope%2FLisbon", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FEurope%2FLisbon?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "c7cd060159bd22fc5e6f10ac5a2089afb2c19c6a", "filename": "lib/pytz/zoneinfo/HST", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FHST", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FHST", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FHST?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "40d7124e5346af056c75e2f7012a51d94e8154b7", "filename": "lib/pytz/zoneinfo/MET", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FMET", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FMET", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FMET?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "ab37e845566aa95659b7b85be0051d0c67a7e53a", "filename": "lib/pytz/zoneinfo/MST", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FMST", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FMST", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FMST?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "abb2b974a47eb3e5c8b4f5d4370baf4898b239ab", "filename": "lib/pytz/zoneinfo/MST7MDT", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FMST7MDT", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FMST7MDT", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FMST7MDT?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "0fe73912cacab6c8aabef4f4264d1710825888e7", "filename": "lib/pytz/zoneinfo/Mexico/BajaNorte", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FMexico%2FBajaNorte", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FMexico%2FBajaNorte", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FMexico%2FBajaNorte?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "386616a59cdb2a21f1f24a6c72526bce0360cd24", "filename": "lib/pytz/zoneinfo/Mexico/BajaSur", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FMexico%2FBajaSur", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FMexico%2FBajaSur", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FMexico%2FBajaSur?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "ad70cf3e08498845aaafc38e4eccca7461c8d337", "filename": "lib/pytz/zoneinfo/Mexico/General", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FMexico%2FGeneral", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FMexico%2FGeneral", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FMexico%2FGeneral?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "610e7af5fc13d9784de30d272c7c39d7938873a0", "filename": "lib/pytz/zoneinfo/PST8PDT", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FPST8PDT", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FPST8PDT", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FPST8PDT?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "616de167b8a1c534c1c526e7bbff82f7d5da94f2", "filename": "lib/pytz/zoneinfo/Portugal", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FPortugal", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FPortugal", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FPortugal?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "616de167b8a1c534c1c526e7bbff82f7d5da94f2", "filename": "lib/pytz/zoneinfo/WET", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FWET", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2FWET", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2FWET?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "6c715cb20b0177a89ef4a4863ef6278231212a7f", "filename": "lib/pytz/zoneinfo/leapseconds", "status": "modified", "additions": 4, "deletions": 4, "changes": 8, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2Fleapseconds", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2Fleapseconds", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2Fleapseconds?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -69,11 +69,11 @@ Leap\t2016\tDec\t31\t23:59:60\t+\tS\n # Any additional leap seconds will come after this.\n # This Expires line is commented out for now,\n # so that pre-2020a zic implementations do not reject this file.\n-#Expires 2024\tDec\t28\t00:00:00\n+#Expires 2025\tJun\t28\t00:00:00\n \n # POSIX timestamps for the data in this file:\n-#updated 1704708379 (2024-01-08 10:06:19 UTC)\n-#expires 1735344000 (2024-12-28 00:00:00 UTC)\n+#updated 1720104763 (2024-07-04 14:52:43 UTC)\n+#expires 1751068800 (2025-06-28 00:00:00 UTC)\n \n #\tUpdated through IERS Bulletin C (https://hpiers.obspm.fr/iers/bul/bulc/bulletinc.dat)\n-#\tFile expires on 28 December 2024\n+#\tFile expires on 28 June 2025"}, {"sha": "b89326a7aa31b70e9014df63b6e3bdfb6563e8ea", "filename": "lib/pytz/zoneinfo/tzdata.zi", "status": "modified", "additions": 819, "deletions": 832, "changes": 1651, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2Ftzdata.zi", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2Ftzdata.zi", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2Ftzdata.zi?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1324,14 +1324,10 @@ R O 1961 1964 - May lastSu 1s 1 S\n R O 1962 1964 - S lastSu 1s 0 -\n R p 1916 o - Jun 17 23 1 S\n R p 1916 o - N 1 1 0 -\n-R p 1917 o - F 28 23s 1 S\n-R p 1917 1921 - O 14 23s 0 -\n-R p 1918 o - Mar 1 23s 1 S\n-R p 1919 o - F 28 23s 1 S\n-R p 1920 o - F 29 23s 1 S\n-R p 1921 o - F 28 23s 1 S\n+R p 1917 1921 - Mar 1 0 1 S\n+R p 1917 1921 - O 14 24 0 -\n R p 1924 o - Ap 16 23s 1 S\n-R p 1924 o - O 14 23s 0 -\n+R p 1924 o - O 4 23s 0 -\n R p 1926 o - Ap 17 23s 1 S\n R p 1926 1929 - O Sa>=1 23s 0 -\n R p 1927 o - Ap 9 23s 1 S\n@@ -1349,8 +1345,9 @@ R p 1938 o - Mar 26 23s 1 S\n R p 1939 o - Ap 15 23s 1 S\n R p 1939 o - N 18 23s 0 -\n R p 1940 o - F 24 23s 1 S\n-R p 1940 1941 - O 5 23s 0 -\n+R p 1940 o - O 7 23s 0 -\n R p 1941 o - Ap 5 23s 1 S\n+R p 1941 o - O 5 23s 0 -\n R p 1942 1945 - Mar Sa>=8 23s 1 S\n R p 1942 o - Ap 25 22s 2 M\n R p 1942 o - Au 15 22s 1 S\n@@ -1360,16 +1357,16 @@ R p 1943 1945 - Au Sa>=25 22s 1 S\n R p 1944 1945 - Ap Sa>=21 22s 2 M\n R p 1946 o - Ap Sa>=1 23s 1 S\n R p 1946 o - O Sa>=1 23s 0 -\n-R p 1947 1965 - Ap Su>=1 2s 1 S\n+R p 1947 1966 - Ap Su>=1 2s 1 S\n R p 1947 1965 - O Su>=1 2s 0 -\n-R p 1977 o - Mar 27 0s 1 S\n-R p 1977 o - S 25 0s 0 -\n-R p 1978 1979 - Ap Su>=1 0s 1 S\n-R p 1978 o - O 1 0s 0 -\n-R p 1979 1982 - S lastSu 1s 0 -\n-R p 1980 o - Mar lastSu 0s 1 S\n-R p 1981 1982 - Mar lastSu 1s 1 S\n-R p 1983 o - Mar lastSu 2s 1 S\n+R p 1976 o - S lastSu 1 0 -\n+R p 1977 o - Mar lastSu 0s 1 S\n+R p 1977 o - S lastSu 0s 0 -\n+R p 1978 1980 - Ap Su>=1 1s 1 S\n+R p 1978 o - O 1 1s 0 -\n+R p 1979 1980 - S lastSu 1s 0 -\n+R p 1981 1986 - Mar lastSu 0s 1 S\n+R p 1981 1985 - S lastSu 0s 0 -\n R z 1932 o - May 21 0s 1 S\n R z 1932 1939 - O Su>=1 0s 0 -\n R z 1933 1939 - Ap Su>=2 0s 1 S\n@@ -1728,7 +1725,7 @@ R Y 1972 2006 - O lastSu 2 0 S\n R Y 1987 2006 - Ap Su>=1 2 1 D\n R Yu 1965 o - Ap lastSu 0 2 DD\n R Yu 1965 o - O lastSu 2 0 S\n-R m 1931 o - May 1 23 1 D\n+R m 1931 o - April 30 0 1 D\n R m 1931 o - O 1 0 0 S\n R m 1939 o - F 5 0 1 D\n R m 1939 o - Jun 25 0 0 S\n@@ -2096,15 +2093,15 @@ Z Africa/Algiers 0:12:12 - LMT 1891 Mar 16\n 0 d WE%sT 1981 May\n 1 - CET\n Z Africa/Bissau -1:2:20 - LMT 1912 Ja 1 1u\n--1 - -01 1975\n+-1 - %z 1975\n 0 - GMT\n Z Africa/Cairo 2:5:9 - LMT 1900 O\n 2 K EE%sT\n Z Africa/Casablanca -0:30:20 - LMT 1913 O 26\n-0 M +00/+01 1984 Mar 16\n-1 - +01 1986\n-0 M +00/+01 2018 O 28 3\n-1 M +01/+00\n+0 M %z 1984 Mar 16\n+1 - %z 1986\n+0 M %z 2018 O 28 3\n+1 M %z\n Z Africa/Ceuta -0:21:16 - LMT 1901 Ja 1 0u\n 0 - WET 1918 May 6 23\n 0 1 WEST 1918 O 7 23\n@@ -2115,9 +2112,9 @@ Z Africa/Ceuta -0:21:16 - LMT 1901 Ja 1 0u\n 1 - CET 1986\n 1 E CE%sT\n Z Africa/El_Aaiun -0:52:48 - LMT 1934\n--1 - -01 1976 Ap 14\n-0 M +00/+01 2018 O 28 3\n-1 M +01/+00\n+-1 - %z 1976 Ap 14\n+0 M %z 2018 O 28 3\n+1 M %z\n Z Africa/Johannesburg 1:52 - LMT 1892 F 8\n 1:30 - SAST 1903 Mar\n 2 SA SAST\n@@ -2132,19 +2129,19 @@ Z Africa/Khartoum 2:10:8 - LMT 1931\n Z Africa/Lagos 0:13:35 - LMT 1905 Jul\n 0 - GMT 1908 Jul\n 0:13:35 - LMT 1914\n-0:30 - +0030 1919 S\n+0:30 - %z 1919 S\n 1 - WAT\n-Z Africa/Maputo 2:10:20 - LMT 1903 Mar\n+Z Africa/Maputo 2:10:18 - LMT 1909\n 2 - CAT\n Z Africa/Monrovia -0:43:8 - LMT 1882\n -0:43:8 - MMT 1919 Mar\n -0:44:30 - MMT 1972 Ja 7\n 0 - GMT\n Z Africa/Nairobi 2:27:16 - LMT 1908 May\n-2:30 - +0230 1928 Jun 30 24\n+2:30 - %z 1928 Jun 30 24\n 3 - EAT 1930 Ja 4 24\n-2:30 - +0230 1936 D 31 24\n-2:45 - +0245 1942 Jul 31 24\n+2:30 - %z 1936 D 31 24\n+2:45 - %z 1942 Jul 31 24\n 3 - EAT\n Z Africa/Ndjamena 1:0:12 - LMT 1912\n 1 - WAT 1979 O 14\n@@ -2168,7 +2165,7 @@ Z Africa/Tunis 0:40:44 - LMT 1881 May 12\n 0:9:21 - PMT 1911 Mar 11\n 1 n CE%sT\n Z Africa/Windhoek 1:8:24 - LMT 1892 F 8\n-1:30 - +0130 1903 Mar\n+1:30 - %z 1903 Mar\n 2 - SAST 1942 S 20 2\n 2 1 SAST 1943 Mar 21 2\n 2 - SAST 1990 Mar 21\n@@ -2191,186 +2188,185 @@ Z America/Anchorage 14:0:24 - LMT 1867 O 19 14:31:37\n -9 u Y%sT 1983 N 30\n -9 u AK%sT\n Z America/Araguaina -3:12:48 - LMT 1914\n--3 B -03/-02 1990 S 17\n--3 - -03 1995 S 14\n--3 B -03/-02 2003 S 24\n--3 - -03 2012 O 21\n--3 B -03/-02 2013 S\n--3 - -03\n+-3 B %z 1990 S 17\n+-3 - %z 1995 S 14\n+-3 B %z 2003 S 24\n+-3 - %z 2012 O 21\n+-3 B %z 2013 S\n+-3 - %z\n Z America/Argentina/Buenos_Aires -3:53:48 - LMT 1894 O 31\n -4:16:48 - CMT 1920 May\n--4 - -04 1930 D\n--4 A -04/-03 1969 O 5\n--3 A -03/-02 1999 O 3\n--4 A -04/-03 2000 Mar 3\n--3 A -03/-02\n+-4 - %z 1930 D\n+-4 A %z 1969 O 5\n+-3 A %z 1999 O 3\n+-4 A %z 2000 Mar 3\n+-3 A %z\n Z America/Argentina/Catamarca -4:23:8 - LMT 1894 O 31\n -4:16:48 - CMT 1920 May\n--4 - -04 1930 D\n--4 A -04/-03 1969 O 5\n--3 A -03/-02 1991 Mar 3\n--4 - -04 1991 O 20\n--3 A -03/-02 1999 O 3\n--4 A -04/-03 2000 Mar 3\n--3 - -03 2004 Jun\n--4 - -04 2004 Jun 20\n--3 A -03/-02 2008 O 18\n--3 - -03\n+-4 - %z 1930 D\n+-4 A %z 1969 O 5\n+-3 A %z 1991 Mar 3\n+-4 - %z 1991 O 20\n+-3 A %z 1999 O 3\n+-4 A %z 2000 Mar 3\n+-3 - %z 2004 Jun\n+-4 - %z 2004 Jun 20\n+-3 A %z 2008 O 18\n+-3 - %z\n Z America/Argentina/Cordoba -4:16:48 - LMT 1894 O 31\n -4:16:48 - CMT 1920 May\n--4 - -04 1930 D\n--4 A -04/-03 1969 O 5\n--3 A -03/-02 1991 Mar 3\n--4 - -04 1991 O 20\n--3 A -03/-02 1999 O 3\n--4 A -04/-03 2000 Mar 3\n--3 A -03/-02\n+-4 - %z 1930 D\n+-4 A %z 1969 O 5\n+-3 A %z 1991 Mar 3\n+-4 - %z 1991 O 20\n+-3 A %z 1999 O 3\n+-4 A %z 2000 Mar 3\n+-3 A %z\n Z America/Argentina/Jujuy -4:21:12 - LMT 1894 O 31\n -4:16:48 - CMT 1920 May\n--4 - -04 1930 D\n--4 A -04/-03 1969 O 5\n--3 A -03/-02 1990 Mar 4\n--4 - -04 1990 O 28\n--4 1 -03 1991 Mar 17\n--4 - -04 1991 O 6\n--3 1 -02 1992\n--3 A -03/-02 1999 O 3\n--4 A -04/-03 2000 Mar 3\n--3 A -03/-02 2008 O 18\n--3 - -03\n+-4 - %z 1930 D\n+-4 A %z 1969 O 5\n+-3 A %z 1990 Mar 4\n+-4 - %z 1990 O 28\n+-4 1 %z 1991 Mar 17\n+-4 - %z 1991 O 6\n+-3 1 %z 1992\n+-3 A %z 1999 O 3\n+-4 A %z 2000 Mar 3\n+-3 A %z 2008 O 18\n+-3 - %z\n Z America/Argentina/La_Rioja -4:27:24 - LMT 1894 O 31\n -4:16:48 - CMT 1920 May\n--4 - -04 1930 D\n--4 A -04/-03 1969 O 5\n--3 A -03/-02 1991 Mar\n--4 - -04 1991 May 7\n--3 A -03/-02 1999 O 3\n--4 A -04/-03 2000 Mar 3\n--3 - -03 2004 Jun\n--4 - -04 2004 Jun 20\n--3 A -03/-02 2008 O 18\n--3 - -03\n+-4 - %z 1930 D\n+-4 A %z 1969 O 5\n+-3 A %z 1991 Mar\n+-4 - %z 1991 May 7\n+-3 A %z 1999 O 3\n+-4 A %z 2000 Mar 3\n+-3 - %z 2004 Jun\n+-4 - %z 2004 Jun 20\n+-3 A %z 2008 O 18\n+-3 - %z\n Z America/Argentina/Mendoza -4:35:16 - LMT 1894 O 31\n -4:16:48 - CMT 1920 May\n--4 - -04 1930 D\n--4 A -04/-03 1969 O 5\n--3 A -03/-02 1990 Mar 4\n--4 - -04 1990 O 15\n--4 1 -03 1991 Mar\n--4 - -04 1991 O 15\n--4 1 -03 1992 Mar\n--4 - -04 1992 O 18\n--3 A -03/-02 1999 O 3\n--4 A -04/-03 2000 Mar 3\n--3 - -03 2004 May 23\n--4 - -04 2004 S 26\n--3 A -03/-02 2008 O 18\n--3 - -03\n+-4 - %z 1930 D\n+-4 A %z 1969 O 5\n+-3 A %z 1990 Mar 4\n+-4 - %z 1990 O 15\n+-4 1 %z 1991 Mar\n+-4 - %z 1991 O 15\n+-4 1 %z 1992 Mar\n+-4 - %z 1992 O 18\n+-3 A %z 1999 O 3\n+-4 A %z 2000 Mar 3\n+-3 - %z 2004 May 23\n+-4 - %z 2004 S 26\n+-3 A %z 2008 O 18\n+-3 - %z\n Z America/Argentina/Rio_Gallegos -4:36:52 - LMT 1894 O 31\n -4:16:48 - CMT 1920 May\n--4 - -04 1930 D\n--4 A -04/-03 1969 O 5\n--3 A -03/-02 1999 O 3\n--4 A -04/-03 2000 Mar 3\n--3 - -03 2004 Jun\n--4 - -04 2004 Jun 20\n--3 A -03/-02 2008 O 18\n--3 - -03\n+-4 - %z 1930 D\n+-4 A %z 1969 O 5\n+-3 A %z 1999 O 3\n+-4 A %z 2000 Mar 3\n+-3 - %z 2004 Jun\n+-4 - %z 2004 Jun 20\n+-3 A %z 2008 O 18\n+-3 - %z\n Z America/Argentina/Salta -4:21:40 - LMT 1894 O 31\n -4:16:48 - CMT 1920 May\n--4 - -04 1930 D\n--4 A -04/-03 1969 O 5\n--3 A -03/-02 1991 Mar 3\n--4 - -04 1991 O 20\n--3 A -03/-02 1999 O 3\n--4 A -04/-03 2000 Mar 3\n--3 A -03/-02 2008 O 18\n--3 - -03\n+-4 - %z 1930 D\n+-4 A %z 1969 O 5\n+-3 A %z 1991 Mar 3\n+-4 - %z 1991 O 20\n+-3 A %z 1999 O 3\n+-4 A %z 2000 Mar 3\n+-3 A %z 2008 O 18\n+-3 - %z\n Z America/Argentina/San_Juan -4:34:4 - LMT 1894 O 31\n -4:16:48 - CMT 1920 May\n--4 - -04 1930 D\n--4 A -04/-03 1969 O 5\n--3 A -03/-02 1991 Mar\n--4 - -04 1991 May 7\n--3 A -03/-02 1999 O 3\n--4 A -04/-03 2000 Mar 3\n--3 - -03 2004 May 31\n--4 - -04 2004 Jul 25\n--3 A -03/-02 2008 O 18\n--3 - -03\n+-4 - %z 1930 D\n+-4 A %z 1969 O 5\n+-3 A %z 1991 Mar\n+-4 - %z 1991 May 7\n+-3 A %z 1999 O 3\n+-4 A %z 2000 Mar 3\n+-3 - %z 2004 May 31\n+-4 - %z 2004 Jul 25\n+-3 A %z 2008 O 18\n+-3 - %z\n Z America/Argentina/San_Luis -4:25:24 - LMT 1894 O 31\n -4:16:48 - CMT 1920 May\n--4 - -04 1930 D\n--4 A -04/-03 1969 O 5\n--3 A -03/-02 1990\n--3 1 -02 1990 Mar 14\n--4 - -04 1990 O 15\n--4 1 -03 1991 Mar\n--4 - -04 1991 Jun\n--3 - -03 1999 O 3\n--4 1 -03 2000 Mar 3\n--3 - -03 2004 May 31\n--4 - -04 2004 Jul 25\n--3 A -03/-02 2008 Ja 21\n--4 Sa -04/-03 2009 O 11\n--3 - -03\n+-4 - %z 1930 D\n+-4 A %z 1969 O 5\n+-3 A %z 1990\n+-3 1 %z 1990 Mar 14\n+-4 - %z 1990 O 15\n+-4 1 %z 1991 Mar\n+-4 - %z 1991 Jun\n+-3 - %z 1999 O 3\n+-4 1 %z 2000 Mar 3\n+-3 - %z 2004 May 31\n+-4 - %z 2004 Jul 25\n+-3 A %z 2008 Ja 21\n+-4 Sa %z 2009 O 11\n+-3 - %z\n Z America/Argentina/Tucuman -4:20:52 - LMT 1894 O 31\n -4:16:48 - CMT 1920 May\n--4 - -04 1930 D\n--4 A -04/-03 1969 O 5\n--3 A -03/-02 1991 Mar 3\n--4 - -04 1991 O 20\n--3 A -03/-02 1999 O 3\n--4 A -04/-03 2000 Mar 3\n--3 - -03 2004 Jun\n--4 - -04 2004 Jun 13\n--3 A -03/-02\n+-4 - %z 1930 D\n+-4 A %z 1969 O 5\n+-3 A %z 1991 Mar 3\n+-4 - %z 1991 O 20\n+-3 A %z 1999 O 3\n+-4 A %z 2000 Mar 3\n+-3 - %z 2004 Jun\n+-4 - %z 2004 Jun 13\n+-3 A %z\n Z America/Argentina/Ushuaia -4:33:12 - LMT 1894 O 31\n -4:16:48 - CMT 1920 May\n--4 - -04 1930 D\n--4 A -04/-03 1969 O 5\n--3 A -03/-02 1999 O 3\n--4 A -04/-03 2000 Mar 3\n--3 - -03 2004 May 30\n--4 - -04 2004 Jun 20\n--3 A -03/-02 2008 O 18\n--3 - -03\n+-4 - %z 1930 D\n+-4 A %z 1969 O 5\n+-3 A %z 1999 O 3\n+-4 A %z 2000 Mar 3\n+-3 - %z 2004 May 30\n+-4 - %z 2004 Jun 20\n+-3 A %z 2008 O 18\n+-3 - %z\n Z America/Asuncion -3:50:40 - LMT 1890\n -3:50:40 - AMT 1931 O 10\n--4 - -04 1972 O\n--3 - -03 1974 Ap\n--4 y -04/-03\n+-4 - %z 1972 O\n+-3 - %z 1974 Ap\n+-4 y %z\n Z America/Bahia -2:34:4 - LMT 1914\n--3 B -03/-02 2003 S 24\n--3 - -03 2011 O 16\n--3 B -03/-02 2012 O 21\n--3 - -03\n+-3 B %z 2003 S 24\n+-3 - %z 2011 O 16\n+-3 B %z 2012 O 21\n+-3 - %z\n Z America/Bahia_Banderas -7:1 - LMT 1922 Ja 1 7u\n--7 - MST 1927 Jun 10 23\n+-7 - MST 1927 Jun 10\n -6 - CST 1930 N 15\n -7 m M%sT 1932 Ap\n -6 - CST 1942 Ap 24\n--7 - MST 1949 Ja 14\n--8 - PST 1970\n+-7 - MST 1970\n -7 m M%sT 2010 Ap 4 2\n -6 m C%sT\n Z America/Barbados -3:58:29 - LMT 1911 Au 28\n -4 BB A%sT 1944\n -4 BB AST/-0330 1945\n -4 BB A%sT\n Z America/Belem -3:13:56 - LMT 1914\n--3 B -03/-02 1988 S 12\n--3 - -03\n+-3 B %z 1988 S 12\n+-3 - %z\n Z America/Belize -5:52:48 - LMT 1912 Ap\n -6 BZ %s\n Z America/Boa_Vista -4:2:40 - LMT 1914\n--4 B -04/-03 1988 S 12\n--4 - -04 1999 S 30\n--4 B -04/-03 2000 O 15\n--4 - -04\n+-4 B %z 1988 S 12\n+-4 - %z 1999 S 30\n+-4 B %z 2000 O 15\n+-4 - %z\n Z America/Bogota -4:56:16 - LMT 1884 Mar 13\n -4:56:16 - BMT 1914 N 23\n--5 CO -05/-04\n+-5 CO %z\n Z America/Boise -7:44:49 - LMT 1883 N 18 20u\n -8 u P%sT 1923 May 13 2\n -7 u M%sT 1974\n@@ -2383,21 +2379,23 @@ Z America/Cambridge_Bay 0 - -00 1920\n -6 - CST 2001 Ap 1 3\n -7 C M%sT\n Z America/Campo_Grande -3:38:28 - LMT 1914\n--4 B -04/-03\n+-4 B %z\n Z America/Cancun -5:47:4 - LMT 1922 Ja 1 6u\n--6 - CST 1981 D 23\n+-6 - CST 1981 D 26 2\n+-5 - EST 1983 Ja 4\n+-6 m C%sT 1997 O 26 2\n -5 m E%sT 1998 Au 2 2\n -6 m C%sT 2015 F 1 2\n -5 - EST\n Z America/Caracas -4:27:44 - LMT 1890\n -4:27:40 - CMT 1912 F 12\n--4:30 - -0430 1965\n--4 - -04 2007 D 9 3\n--4:30 - -0430 2016 May 1 2:30\n--4 - -04\n+-4:30 - %z 1965\n+-4 - %z 2007 D 9 3\n+-4:30 - %z 2016 May 1 2:30\n+-4 - %z\n Z America/Cayenne -3:29:20 - LMT 1911 Jul\n--4 - -04 1967 O\n--3 - -03\n+-4 - %z 1967 O\n+-3 - %z\n Z America/Chicago -5:50:36 - LMT 1883 N 18 18u\n -6 u C%sT 1920\n -6 Ch C%sT 1936 Mar 1 2\n@@ -2407,7 +2405,7 @@ Z America/Chicago -5:50:36 - LMT 1883 N 18 18u\n -6 Ch C%sT 1967\n -6 u C%sT\n Z America/Chihuahua -7:4:20 - LMT 1922 Ja 1 7u\n--7 - MST 1927 Jun 10 23\n+-7 - MST 1927 Jun 10\n -6 - CST 1930 N 15\n -7 m M%sT 1932 Ap\n -6 - CST 1996\n@@ -2416,7 +2414,7 @@ Z America/Chihuahua -7:4:20 - LMT 1922 Ja 1 7u\n -7 m M%sT 2022 O 30 2\n -6 - CST\n Z America/Ciudad_Juarez -7:5:56 - LMT 1922 Ja 1 7u\n--7 - MST 1927 Jun 10 23\n+-7 - MST 1927 Jun 10\n -6 - CST 1930 N 15\n -7 m M%sT 1932 Ap\n -6 - CST 1996\n@@ -2430,12 +2428,12 @@ Z America/Costa_Rica -5:36:13 - LMT 1890\n -5:36:13 - SJMT 1921 Ja 15\n -6 CR C%sT\n Z America/Cuiaba -3:44:20 - LMT 1914\n--4 B -04/-03 2003 S 24\n--4 - -04 2004 O\n--4 B -04/-03\n+-4 B %z 2003 S 24\n+-4 - %z 2004 O\n+-4 B %z\n Z America/Danmarkshavn -1:14:40 - LMT 1916 Jul 28\n--3 - -03 1980 Ap 6 2\n--3 E -03/-02 1996\n+-3 - %z 1980 Ap 6 2\n+-3 E %z 1996\n 0 - GMT\n Z America/Dawson -9:17:40 - LMT 1900 Au 20\n -9 Y Y%sT 1965\n@@ -2467,12 +2465,12 @@ Z America/Edmonton -7:33:52 - LMT 1906 S\n -7 Ed M%sT 1987\n -7 C M%sT\n Z America/Eirunepe -4:39:28 - LMT 1914\n--5 B -05/-04 1988 S 12\n--5 - -05 1993 S 28\n--5 B -05/-04 1994 S 22\n--5 - -05 2008 Jun 24\n--4 - -04 2013 N 10\n--5 - -05\n+-5 B %z 1988 S 12\n+-5 - %z 1993 S 28\n+-5 B %z 1994 S 22\n+-5 - %z 2008 Jun 24\n+-4 - %z 2013 N 10\n+-5 - %z\n Z America/El_Salvador -5:56:48 - LMT 1921\n -6 SV C%sT\n Z America/Fort_Nelson -8:10:47 - LMT 1884\n@@ -2482,12 +2480,12 @@ Z America/Fort_Nelson -8:10:47 - LMT 1884\n -8 C P%sT 2015 Mar 8 2\n -7 - MST\n Z America/Fortaleza -2:34 - LMT 1914\n--3 B -03/-02 1990 S 17\n--3 - -03 1999 S 30\n--3 B -03/-02 2000 O 22\n--3 - -03 2001 S 13\n--3 B -03/-02 2002 O\n--3 - -03\n+-3 B %z 1990 S 17\n+-3 - %z 1999 S 30\n+-3 B %z 2000 O 22\n+-3 - %z 2001 S 13\n+-3 B %z 2002 O\n+-3 - %z\n Z America/Glace_Bay -3:59:48 - LMT 1902 Jun 15\n -4 C A%sT 1953\n -4 H A%sT 1954\n@@ -2514,12 +2512,12 @@ Z America/Guatemala -6:2:4 - LMT 1918 O 5\n -6 GT C%sT\n Z America/Guayaquil -5:19:20 - LMT 1890\n -5:14 - QMT 1931\n--5 EC -05/-04\n+-5 EC %z\n Z America/Guyana -3:52:39 - LMT 1911 Au\n--4 - -04 1915 Mar\n--3:45 - -0345 1975 Au\n--3 - -03 1992 Mar 29 1\n--4 - -04\n+-4 - %z 1915 Mar\n+-3:45 - %z 1975 Au\n+-3 - %z 1992 Mar 29 1\n+-4 - %z\n Z America/Halifax -4:14:24 - LMT 1902 Jun 15\n -4 H A%sT 1918\n -4 C A%sT 1919\n@@ -2531,12 +2529,11 @@ Z America/Havana -5:29:28 - LMT 1890\n -5:29:36 - HMT 1925 Jul 19 12\n -5 Q C%sT\n Z America/Hermosillo -7:23:52 - LMT 1922 Ja 1 7u\n--7 - MST 1927 Jun 10 23\n+-7 - MST 1927 Jun 10\n -6 - CST 1930 N 15\n -7 m M%sT 1932 Ap\n -6 - CST 1942 Ap 24\n--7 - MST 1949 Ja 14\n--8 - PST 1970\n+-7 - MST 1996\n -7 m M%sT 1999\n -7 - MST\n Z America/Indiana/Indianapolis -5:44:38 - LMT 1883 N 18 18u\n@@ -2644,23 +2641,23 @@ Z America/Kentucky/Monticello -5:39:24 - LMT 1883 N 18 18u\n Z America/La_Paz -4:32:36 - LMT 1890\n -4:32:36 - CMT 1931 O 15\n -4:32:36 1 BST 1932 Mar 21\n--4 - -04\n+-4 - %z\n Z America/Lima -5:8:12 - LMT 1890\n -5:8:36 - LMT 1908 Jul 28\n--5 PE -05/-04\n+-5 PE %z\n Z America/Los_Angeles -7:52:58 - LMT 1883 N 18 20u\n -8 u P%sT 1946\n -8 CA P%sT 1967\n -8 u P%sT\n Z America/Maceio -2:22:52 - LMT 1914\n--3 B -03/-02 1990 S 17\n--3 - -03 1995 O 13\n--3 B -03/-02 1996 S 4\n--3 - -03 1999 S 30\n--3 B -03/-02 2000 O 22\n--3 - -03 2001 S 13\n--3 B -03/-02 2002 O\n--3 - -03\n+-3 B %z 1990 S 17\n+-3 - %z 1995 O 13\n+-3 B %z 1996 S 4\n+-3 - %z 1999 S 30\n+-3 B %z 2000 O 22\n+-3 - %z 2001 S 13\n+-3 B %z 2002 O\n+-3 - %z\n Z America/Managua -5:45:8 - LMT 1890\n -5:45:12 - MMT 1934 Jun 23\n -6 - CST 1973 May\n@@ -2671,10 +2668,10 @@ Z America/Managua -5:45:8 - LMT 1890\n -5 - EST 1997\n -6 NI C%sT\n Z America/Manaus -4:0:4 - LMT 1914\n--4 B -04/-03 1988 S 12\n--4 - -04 1993 S 28\n--4 B -04/-03 1994 S 22\n--4 - -04\n+-4 B %z 1988 S 12\n+-4 - %z 1993 S 28\n+-4 B %z 1994 S 22\n+-4 - %z\n Z America/Martinique -4:4:20 - LMT 1890\n -4:4:20 - FFMT 1911 May\n -4 - AST 1980 Ap 6\n@@ -2686,21 +2683,20 @@ Z America/Matamoros -6:30 - LMT 1922 Ja 1 6u\n -6 m C%sT 2010\n -6 u C%sT\n Z America/Mazatlan -7:5:40 - LMT 1922 Ja 1 7u\n--7 - MST 1927 Jun 10 23\n+-7 - MST 1927 Jun 10\n -6 - CST 1930 N 15\n -7 m M%sT 1932 Ap\n -6 - CST 1942 Ap 24\n--7 - MST 1949 Ja 14\n--8 - PST 1970\n+-7 - MST 1970\n -7 m M%sT\n Z America/Menominee -5:50:27 - LMT 1885 S 18 12\n -6 u C%sT 1946\n -6 Me C%sT 1969 Ap 27 2\n -5 - EST 1973 Ap 29 2\n -6 u C%sT\n Z America/Merida -5:58:28 - LMT 1922 Ja 1 6u\n--6 - CST 1981 D 23\n--5 - EST 1982 D 2\n+-6 - CST 1981 D 26 2\n+-5 - EST 1982 N 2 2\n -6 m C%sT\n Z America/Metlakatla 15:13:42 - LMT 1867 O 19 15:44:55\n -8:46:18 - LMT 1900 Au 20 12\n@@ -2713,16 +2709,16 @@ Z America/Metlakatla 15:13:42 - LMT 1867 O 19 15:44:55\n -8 - PST 2019 Ja 20 2\n -9 u AK%sT\n Z America/Mexico_City -6:36:36 - LMT 1922 Ja 1 7u\n--7 - MST 1927 Jun 10 23\n+-7 - MST 1927 Jun 10\n -6 - CST 1930 N 15\n -7 m M%sT 1932 Ap\n -6 m C%sT 2001 S 30 2\n -6 - CST 2002 F 20\n -6 m C%sT\n Z America/Miquelon -3:44:40 - LMT 1911 Jun 15\n -4 - AST 1980 May\n--3 - -03 1987\n--3 C -03/-02\n+-3 - %z 1987\n+-3 C %z\n Z America/Moncton -4:19:8 - LMT 1883 D 9\n -5 - EST 1902 Jun 15\n -4 C A%sT 1933\n@@ -2733,20 +2729,23 @@ Z America/Moncton -4:19:8 - LMT 1883 D 9\n -4 o A%sT 2007\n -4 C A%sT\n Z America/Monterrey -6:41:16 - LMT 1922 Ja 1 6u\n+-7 - MST 1927 Jun 10\n+-6 - CST 1930 N 15\n+-7 m M%sT 1932 Ap\n -6 - CST 1988\n -6 u C%sT 1989\n -6 m C%sT\n Z America/Montevideo -3:44:51 - LMT 1908 Jun 10\n -3:44:51 - MMT 1920 May\n--4 - -04 1923 O\n--3:30 U -0330/-03 1942 D 14\n--3 U -03/-0230 1960\n--3 U -03/-02 1968\n--3 U -03/-0230 1970\n--3 U -03/-02 1974\n--3 U -03/-0130 1974 Mar 10\n--3 U -03/-0230 1974 D 22\n--3 U -03/-02\n+-4 - %z 1923 O\n+-3:30 U %z 1942 D 14\n+-3 U %z 1960\n+-3 U %z 1968\n+-3 U %z 1970\n+-3 U %z 1974\n+-3 U %z 1974 Mar 10\n+-3 U %z 1974 D 22\n+-3 U %z\n Z America/New_York -4:56:2 - LMT 1883 N 18 17u\n -5 u E%sT 1920\n -5 NY E%sT 1942\n@@ -2763,12 +2762,12 @@ Z America/Nome 12:58:22 - LMT 1867 O 19 13:29:35\n -9 u Y%sT 1983 N 30\n -9 u AK%sT\n Z America/Noronha -2:9:40 - LMT 1914\n--2 B -02/-01 1990 S 17\n--2 - -02 1999 S 30\n--2 B -02/-01 2000 O 15\n--2 - -02 2001 S 13\n--2 B -02/-01 2002 O\n--2 - -02\n+-2 B %z 1990 S 17\n+-2 - %z 1999 S 30\n+-2 B %z 2000 O 15\n+-2 - %z 2001 S 13\n+-2 B %z 2002 O\n+-2 - %z\n Z America/North_Dakota/Beulah -6:47:7 - LMT 1883 N 18 19u\n -7 u M%sT 2010 N 7 2\n -6 u C%sT\n@@ -2779,12 +2778,12 @@ Z America/North_Dakota/New_Salem -6:45:39 - LMT 1883 N 18 19u\n -7 u M%sT 2003 O 26 2\n -6 u C%sT\n Z America/Nuuk -3:26:56 - LMT 1916 Jul 28\n--3 - -03 1980 Ap 6 2\n--3 E -03/-02 2023 Mar 26 1u\n--2 - -02 2023 O 29 1u\n--2 E -02/-01\n+-3 - %z 1980 Ap 6 2\n+-3 E %z 2023 Mar 26 1u\n+-2 - %z 2023 O 29 1u\n+-2 E %z\n Z America/Ojinaga -6:57:40 - LMT 1922 Ja 1 7u\n--7 - MST 1927 Jun 10 23\n+-7 - MST 1927 Jun 10\n -6 - CST 1930 N 15\n -7 m M%sT 1932 Ap\n -6 - CST 1996\n@@ -2800,8 +2799,8 @@ Z America/Panama -5:18:8 - LMT 1890\n Z America/Paramaribo -3:40:40 - LMT 1911\n -3:40:52 - PMT 1935\n -3:40:36 - PMT 1945 O\n--3:30 - -0330 1984 O\n--3 - -03\n+-3:30 - %z 1984 O\n+-3 - %z\n Z America/Phoenix -7:28:18 - LMT 1883 N 18 19u\n -7 u M%sT 1944 Ja 1 0:1\n -7 - MST 1944 Ap 1 0:1\n@@ -2813,37 +2812,37 @@ Z America/Port-au-Prince -4:49:20 - LMT 1890\n -4:49 - PPMT 1917 Ja 24 12\n -5 HT E%sT\n Z America/Porto_Velho -4:15:36 - LMT 1914\n--4 B -04/-03 1988 S 12\n--4 - -04\n+-4 B %z 1988 S 12\n+-4 - %z\n Z America/Puerto_Rico -4:24:25 - LMT 1899 Mar 28 12\n -4 - AST 1942 May 3\n -4 u A%sT 1946\n -4 - AST\n Z America/Punta_Arenas -4:43:40 - LMT 1890\n -4:42:45 - SMT 1910 Ja 10\n--5 - -05 1916 Jul\n+-5 - %z 1916 Jul\n -4:42:45 - SMT 1918 S 10\n--4 - -04 1919 Jul\n+-4 - %z 1919 Jul\n -4:42:45 - SMT 1927 S\n--5 x -05/-04 1932 S\n--4 - -04 1942 Jun\n--5 - -05 1942 Au\n--4 - -04 1946 Au 28 24\n--5 1 -04 1947 Mar 31 24\n--5 - -05 1947 May 21 23\n--4 x -04/-03 2016 D 4\n--3 - -03\n+-5 x %z 1932 S\n+-4 - %z 1942 Jun\n+-5 - %z 1942 Au\n+-4 - %z 1946 Au 28 24\n+-5 1 %z 1947 Mar 31 24\n+-5 - %z 1947 May 21 23\n+-4 x %z 2016 D 4\n+-3 - %z\n Z America/Rankin_Inlet 0 - -00 1957\n -6 Y C%sT 2000 O 29 2\n -5 - EST 2001 Ap 1 3\n -6 C C%sT\n Z America/Recife -2:19:36 - LMT 1914\n--3 B -03/-02 1990 S 17\n--3 - -03 1999 S 30\n--3 B -03/-02 2000 O 15\n--3 - -03 2001 S 13\n--3 B -03/-02 2002 O\n--3 - -03\n+-3 B %z 1990 S 17\n+-3 - %z 1999 S 30\n+-3 B %z 2000 O 15\n+-3 - %z 2001 S 13\n+-3 B %z 2002 O\n+-3 - %z\n Z America/Regina -6:58:36 - LMT 1905 S\n -7 r M%sT 1960 Ap lastSu 2\n -6 - CST\n@@ -2854,43 +2853,43 @@ Z America/Resolute 0 - -00 1947 Au 31\n -5 - EST 2007 Mar 11 3\n -6 C C%sT\n Z America/Rio_Branco -4:31:12 - LMT 1914\n--5 B -05/-04 1988 S 12\n--5 - -05 2008 Jun 24\n--4 - -04 2013 N 10\n--5 - -05\n+-5 B %z 1988 S 12\n+-5 - %z 2008 Jun 24\n+-4 - %z 2013 N 10\n+-5 - %z\n Z America/Santarem -3:38:48 - LMT 1914\n--4 B -04/-03 1988 S 12\n--4 - -04 2008 Jun 24\n--3 - -03\n+-4 B %z 1988 S 12\n+-4 - %z 2008 Jun 24\n+-3 - %z\n Z America/Santiago -4:42:45 - LMT 1890\n -4:42:45 - SMT 1910 Ja 10\n--5 - -05 1916 Jul\n+-5 - %z 1916 Jul\n -4:42:45 - SMT 1918 S 10\n--4 - -04 1919 Jul\n+-4 - %z 1919 Jul\n -4:42:45 - SMT 1927 S\n--5 x -05/-04 1932 S\n--4 - -04 1942 Jun\n--5 - -05 1942 Au\n--4 - -04 1946 Jul 14 24\n--4 1 -03 1946 Au 28 24\n--5 1 -04 1947 Mar 31 24\n--5 - -05 1947 May 21 23\n--4 x -04/-03\n+-5 x %z 1932 S\n+-4 - %z 1942 Jun\n+-5 - %z 1942 Au\n+-4 - %z 1946 Jul 14 24\n+-4 1 %z 1946 Au 28 24\n+-5 1 %z 1947 Mar 31 24\n+-5 - %z 1947 May 21 23\n+-4 x %z\n Z America/Santo_Domingo -4:39:36 - LMT 1890\n -4:40 - SDMT 1933 Ap 1 12\n -5 DO %s 1974 O 27\n -4 - AST 2000 O 29 2\n -5 u E%sT 2000 D 3 1\n -4 - AST\n Z America/Sao_Paulo -3:6:28 - LMT 1914\n--3 B -03/-02 1963 O 23\n--3 1 -02 1964\n--3 B -03/-02\n+-3 B %z 1963 O 23\n+-3 1 %z 1964\n+-3 B %z\n Z America/Scoresbysund -1:27:52 - LMT 1916 Jul 28\n--2 - -02 1980 Ap 6 2\n--2 c -02/-01 1981 Mar 29\n--1 E -01/+00 2024 Mar 31\n--2 E -02/-01\n+-2 - %z 1980 Ap 6 2\n+-2 c %z 1981 Mar 29\n+-1 E %z 2024 Mar 31\n+-2 E %z\n Z America/Sitka 14:58:47 - LMT 1867 O 19 15:30\n -9:1:13 - LMT 1900 Au 20 12\n -8 - PST 1942\n@@ -2918,15 +2917,21 @@ Z America/Thule -4:35:8 - LMT 1916 Jul 28\n -4 Th A%sT\n Z America/Tijuana -7:48:4 - LMT 1922 Ja 1 7u\n -7 - MST 1924\n--8 - PST 1927 Jun 10 23\n+-8 - PST 1927 Jun 10\n -7 - MST 1930 N 15\n -8 - PST 1931 Ap\n -8 1 PDT 1931 S 30\n -8 - PST 1942 Ap 24\n -8 1 PWT 1945 Au 14 23u\n--8 1 PPT 1945 N 12\n+-8 1 PPT 1945 N 15\n -8 - PST 1948 Ap 5\n -8 1 PDT 1949 Ja 14\n+-8 - PST 1950 May\n+-8 1 PDT 1950 S 24\n+-8 - PST 1951 Ap 29 2\n+-8 1 PDT 1951 S 30 2\n+-8 - PST 1952 Ap 27 2\n+-8 1 PDT 1952 S 28 2\n -8 - PST 1954\n -8 CA P%sT 1961\n -8 - PST 1976\n@@ -2961,31 +2966,31 @@ Z America/Yakutat 14:41:5 - LMT 1867 O 19 15:12:18\n -9 u Y%sT 1983 N 30\n -9 u AK%sT\n Z Antarctica/Casey 0 - -00 1969\n-8 - +08 2009 O 18 2\n-11 - +11 2010 Mar 5 2\n-8 - +08 2011 O 28 2\n-11 - +11 2012 F 21 17u\n-8 - +08 2016 O 22\n-11 - +11 2018 Mar 11 4\n-8 - +08 2018 O 7 4\n-11 - +11 2019 Mar 17 3\n-8 - +08 2019 O 4 3\n-11 - +11 2020 Mar 8 3\n-8 - +08 2020 O 4 0:1\n-11 - +11 2021 Mar 14\n-8 - +08 2021 O 3 0:1\n-11 - +11 2022 Mar 13\n-8 - +08 2022 O 2 0:1\n-11 - +11 2023 Mar 9 3\n-8 - +08\n+8 - %z 2009 O 18 2\n+11 - %z 2010 Mar 5 2\n+8 - %z 2011 O 28 2\n+11 - %z 2012 F 21 17u\n+8 - %z 2016 O 22\n+11 - %z 2018 Mar 11 4\n+8 - %z 2018 O 7 4\n+11 - %z 2019 Mar 17 3\n+8 - %z 2019 O 4 3\n+11 - %z 2020 Mar 8 3\n+8 - %z 2020 O 4 0:1\n+11 - %z 2021 Mar 14\n+8 - %z 2021 O 3 0:1\n+11 - %z 2022 Mar 13\n+8 - %z 2022 O 2 0:1\n+11 - %z 2023 Mar 9 3\n+8 - %z\n Z Antarctica/Davis 0 - -00 1957 Ja 13\n-7 - +07 1964 N\n+7 - %z 1964 N\n 0 - -00 1969 F\n-7 - +07 2009 O 18 2\n-5 - +05 2010 Mar 10 20u\n-7 - +07 2011 O 28 2\n-5 - +05 2012 F 21 20u\n-7 - +07\n+7 - %z 2009 O 18 2\n+5 - %z 2010 Mar 10 20u\n+7 - %z 2011 O 28 2\n+5 - %z 2012 F 21 20u\n+7 - %z\n Z Antarctica/Macquarie 0 - -00 1899 N\n 10 - AEST 1916 O 1 2\n 10 1 AEDT 1917 F\n@@ -2996,151 +3001,146 @@ Z Antarctica/Macquarie 0 - -00 1899 N\n 10 1 AEDT 2011\n 10 AT AE%sT\n Z Antarctica/Mawson 0 - -00 1954 F 13\n-6 - +06 2009 O 18 2\n-5 - +05\n+6 - %z 2009 O 18 2\n+5 - %z\n Z Antarctica/Palmer 0 - -00 1965\n--4 A -04/-03 1969 O 5\n--3 A -03/-02 1982 May\n--4 x -04/-03 2016 D 4\n--3 - -03\n+-4 A %z 1969 O 5\n+-3 A %z 1982 May\n+-4 x %z 2016 D 4\n+-3 - %z\n Z Antarctica/Rothera 0 - -00 1976 D\n--3 - -03\n+-3 - %z\n Z Antarctica/Troll 0 - -00 2005 F 12\n 0 Tr %s\n Z Antarctica/Vostok 0 - -00 1957 D 16\n-7 - +07 1994 F\n+7 - %z 1994 F\n 0 - -00 1994 N\n-7 - +07 2023 D 18 2\n-5 - +05\n+7 - %z 2023 D 18 2\n+5 - %z\n Z Asia/Almaty 5:7:48 - LMT 1924 May 2\n-5 - +05 1930 Jun 21\n-6 R +06/+07 1991 Mar 31 2s\n-5 R +05/+06 1992 Ja 19 2s\n-6 R +06/+07 2004 O 31 2s\n-6 - +06 2024 Mar\n-5 - +05\n+5 - %z 1930 Jun 21\n+6 R %z 1991 Mar 31 2s\n+5 R %z 1992 Ja 19 2s\n+6 R %z 2004 O 31 2s\n+6 - %z 2024 Mar\n+5 - %z\n Z Asia/Amman 2:23:44 - LMT 1931\n 2 J EE%sT 2022 O 28 0s\n-3 - +03\n+3 - %z\n Z Asia/Anadyr 11:49:56 - LMT 1924 May 2\n-12 - +12 1930 Jun 21\n-13 R +13/+14 1982 Ap 1 0s\n-12 R +12/+13 1991 Mar 31 2s\n-11 R +11/+12 1992 Ja 19 2s\n-12 R +12/+13 2010 Mar 28 2s\n-11 R +11/+12 2011 Mar 27 2s\n-12 - +12\n+12 - %z 1930 Jun 21\n+13 R %z 1982 Ap 1 0s\n+12 R %z 1991 Mar 31 2s\n+11 R %z 1992 Ja 19 2s\n+12 R %z 2010 Mar 28 2s\n+11 R %z 2011 Mar 27 2s\n+12 - %z\n Z Asia/Aqtau 3:21:4 - LMT 1924 May 2\n-4 - +04 1930 Jun 21\n-5 - +05 1981 O\n-6 - +06 1982 Ap\n-5 R +05/+06 1991 Mar 31 2s\n-4 R +04/+05 1992 Ja 19 2s\n-5 R +05/+06 1994 S 25 2s\n-4 R +04/+05 2004 O 31 2s\n-5 - +05\n+4 - %z 1930 Jun 21\n+5 - %z 1981 O\n+6 - %z 1982 Ap\n+5 R %z 1991 Mar 31 2s\n+4 R %z 1992 Ja 19 2s\n+5 R %z 1994 S 25 2s\n+4 R %z 2004 O 31 2s\n+5 - %z\n Z Asia/Aqtobe 3:48:40 - LMT 1924 May 2\n-4 - +04 1930 Jun 21\n-5 - +05 1981 Ap\n-5 1 +06 1981 O\n-6 - +06 1982 Ap\n-5 R +05/+06 1991 Mar 31 2s\n-4 R +04/+05 1992 Ja 19 2s\n-5 R +05/+06 2004 O 31 2s\n-5 - +05\n+4 - %z 1930 Jun 21\n+5 - %z 1981 Ap\n+5 1 %z 1981 O\n+6 - %z 1982 Ap\n+5 R %z 1991 Mar 31 2s\n+4 R %z 1992 Ja 19 2s\n+5 R %z 2004 O 31 2s\n+5 - %z\n Z Asia/Ashgabat 3:53:32 - LMT 1924 May 2\n-4 - +04 1930 Jun 21\n-5 R +05/+06 1991 Mar 31 2\n-4 R +04/+05 1992 Ja 19 2\n-5 - +05\n+4 - %z 1930 Jun 21\n+5 R %z 1991 Mar 31 2\n+4 R %z 1992 Ja 19 2\n+5 - %z\n Z Asia/Atyrau 3:27:44 - LMT 1924 May 2\n-3 - +03 1930 Jun 21\n-5 - +05 1981 O\n-6 - +06 1982 Ap\n-5 R +05/+06 1991 Mar 31 2s\n-4 R +04/+05 1992 Ja 19 2s\n-5 R +05/+06 1999 Mar 28 2s\n-4 R +04/+05 2004 O 31 2s\n-5 - +05\n+3 - %z 1930 Jun 21\n+5 - %z 1981 O\n+6 - %z 1982 Ap\n+5 R %z 1991 Mar 31 2s\n+4 R %z 1992 Ja 19 2s\n+5 R %z 1999 Mar 28 2s\n+4 R %z 2004 O 31 2s\n+5 - %z\n Z Asia/Baghdad 2:57:40 - LMT 1890\n 2:57:36 - BMT 1918\n-3 - +03 1982 May\n-3 IQ +03/+04\n+3 - %z 1982 May\n+3 IQ %z\n Z Asia/Baku 3:19:24 - LMT 1924 May 2\n-3 - +03 1957 Mar\n-4 R +04/+05 1991 Mar 31 2s\n-3 R +03/+04 1992 S lastSu 2s\n-4 - +04 1996\n-4 E +04/+05 1997\n-4 AZ +04/+05\n+3 - %z 1957 Mar\n+4 R %z 1991 Mar 31 2s\n+3 R %z 1992 S lastSu 2s\n+4 - %z 1996\n+4 E %z 1997\n+4 AZ %z\n Z Asia/Bangkok 6:42:4 - LMT 1880\n 6:42:4 - BMT 1920 Ap\n-7 - +07\n+7 - %z\n Z Asia/Barnaul 5:35 - LMT 1919 D 10\n-6 - +06 1930 Jun 21\n-7 R +07/+08 1991 Mar 31 2s\n-6 R +06/+07 1992 Ja 19 2s\n-7 R +07/+08 1995 May 28\n-6 R +06/+07 2011 Mar 27 2s\n-7 - +07 2014 O 26 2s\n-6 - +06 2016 Mar 27 2s\n-7 - +07\n+6 - %z 1930 Jun 21\n+7 R %z 1991 Mar 31 2s\n+6 R %z 1992 Ja 19 2s\n+7 R %z 1995 May 28\n+6 R %z 2011 Mar 27 2s\n+7 - %z 2014 O 26 2s\n+6 - %z 2016 Mar 27 2s\n+7 - %z\n Z Asia/Beirut 2:22 - LMT 1880\n 2 l EE%sT\n Z Asia/Bishkek 4:58:24 - LMT 1924 May 2\n-5 - +05 1930 Jun 21\n-6 R +06/+07 1991 Mar 31 2s\n-5 R +05/+06 1991 Au 31 2\n-5 KG +05/+06 2005 Au 12\n-6 - +06\n+5 - %z 1930 Jun 21\n+6 R %z 1991 Mar 31 2s\n+5 R %z 1991 Au 31 2\n+5 KG %z 2005 Au 12\n+6 - %z\n Z Asia/Chita 7:33:52 - LMT 1919 D 15\n-8 - +08 1930 Jun 21\n-9 R +09/+10 1991 Mar 31 2s\n-8 R +08/+09 1992 Ja 19 2s\n-9 R +09/+10 2011 Mar 27 2s\n-10 - +10 2014 O 26 2s\n-8 - +08 2016 Mar 27 2\n-9 - +09\n-Z Asia/Choibalsan 7:38 - LMT 1905 Au\n-7 - +07 1978\n-8 - +08 1983 Ap\n-9 X +09/+10 2008 Mar 31\n-8 X +08/+09\n+8 - %z 1930 Jun 21\n+9 R %z 1991 Mar 31 2s\n+8 R %z 1992 Ja 19 2s\n+9 R %z 2011 Mar 27 2s\n+10 - %z 2014 O 26 2s\n+8 - %z 2016 Mar 27 2\n+9 - %z\n Z Asia/Colombo 5:19:24 - LMT 1880\n 5:19:32 - MMT 1906\n-5:30 - +0530 1942 Ja 5\n-5:30 0:30 +06 1942 S\n-5:30 1 +0630 1945 O 16 2\n-5:30 - +0530 1996 May 25\n-6:30 - +0630 1996 O 26 0:30\n-6 - +06 2006 Ap 15 0:30\n-5:30 - +0530\n+5:30 - %z 1942 Ja 5\n+5:30 0:30 %z 1942 S\n+5:30 1 %z 1945 O 16 2\n+5:30 - %z 1996 May 25\n+6:30 - %z 1996 O 26 0:30\n+6 - %z 2006 Ap 15 0:30\n+5:30 - %z\n Z Asia/Damascus 2:25:12 - LMT 1920\n 2 S EE%sT 2022 O 28\n-3 - +03\n+3 - %z\n Z Asia/Dhaka 6:1:40 - LMT 1890\n 5:53:20 - HMT 1941 O\n-6:30 - +0630 1942 May 15\n-5:30 - +0530 1942 S\n-6:30 - +0630 1951 S 30\n-6 - +06 2009\n-6 BD +06/+07\n-Z Asia/Dili 8:22:20 - LMT 1912\n-8 - +08 1942 F 21 23\n-9 - +09 1976 May 3\n-8 - +08 2000 S 17\n-9 - +09\n+6:30 - %z 1942 May 15\n+5:30 - %z 1942 S\n+6:30 - %z 1951 S 30\n+6 - %z 2009\n+6 BD %z\n+Z Asia/Dili 8:22:20 - LMT 1911 D 31 16u\n+8 - %z 1942 F 21 23\n+9 - %z 1976 May 3\n+8 - %z 2000 S 17\n+9 - %z\n Z Asia/Dubai 3:41:12 - LMT 1920\n-4 - +04\n+4 - %z\n Z Asia/Dushanbe 4:35:12 - LMT 1924 May 2\n-5 - +05 1930 Jun 21\n-6 R +06/+07 1991 Mar 31 2s\n-5 1 +06 1991 S 9 2s\n-5 - +05\n+5 - %z 1930 Jun 21\n+6 R %z 1991 Mar 31 2s\n+5 1 %z 1991 S 9 2s\n+5 - %z\n Z Asia/Famagusta 2:15:48 - LMT 1921 N 14\n 2 CY EE%sT 1998 S\n 2 E EE%sT 2016 S 8\n-3 - +03 2017 O 29 1u\n+3 - %z 2017 O 29 1u\n 2 E EE%sT\n Z Asia/Gaza 2:17:52 - LMT 1900 O\n 2 Z EET/EEST 1948 May 15\n@@ -3162,111 +3162,111 @@ Z Asia/Hebron 2:20:23 - LMT 1900 O\n 2 P EE%sT\n Z Asia/Ho_Chi_Minh 7:6:30 - LMT 1906 Jul\n 7:6:30 - PLMT 1911 May\n-7 - +07 1942 D 31 23\n-8 - +08 1945 Mar 14 23\n-9 - +09 1945 S 1 24\n-7 - +07 1947 Ap\n-8 - +08 1955 Jul 1 1\n-7 - +07 1959 D 31 23\n-8 - +08 1975 Jun 13\n-7 - +07\n+7 - %z 1942 D 31 23\n+8 - %z 1945 Mar 14 23\n+9 - %z 1945 S 1 24\n+7 - %z 1947 Ap\n+8 - %z 1955 Jul 1 1\n+7 - %z 1959 D 31 23\n+8 - %z 1975 Jun 13\n+7 - %z\n Z Asia/Hong_Kong 7:36:42 - LMT 1904 O 29 17u\n 8 - HKT 1941 Jun 15 3\n 8 1 HKST 1941 O 1 4\n 8 0:30 HKWT 1941 D 25\n 9 - JST 1945 N 18 2\n 8 HK HK%sT\n Z Asia/Hovd 6:6:36 - LMT 1905 Au\n-6 - +06 1978\n-7 X +07/+08\n+6 - %z 1978\n+7 X %z\n Z Asia/Irkutsk 6:57:5 - LMT 1880\n 6:57:5 - IMT 1920 Ja 25\n-7 - +07 1930 Jun 21\n-8 R +08/+09 1991 Mar 31 2s\n-7 R +07/+08 1992 Ja 19 2s\n-8 R +08/+09 2011 Mar 27 2s\n-9 - +09 2014 O 26 2s\n-8 - +08\n+7 - %z 1930 Jun 21\n+8 R %z 1991 Mar 31 2s\n+7 R %z 1992 Ja 19 2s\n+8 R %z 2011 Mar 27 2s\n+9 - %z 2014 O 26 2s\n+8 - %z\n Z Asia/Jakarta 7:7:12 - LMT 1867 Au 10\n 7:7:12 - BMT 1923 D 31 16:40u\n-7:20 - +0720 1932 N\n-7:30 - +0730 1942 Mar 23\n-9 - +09 1945 S 23\n-7:30 - +0730 1948 May\n-8 - +08 1950 May\n-7:30 - +0730 1964\n+7:20 - %z 1932 N\n+7:30 - %z 1942 Mar 23\n+9 - %z 1945 S 23\n+7:30 - %z 1948 May\n+8 - %z 1950 May\n+7:30 - %z 1964\n 7 - WIB\n Z Asia/Jayapura 9:22:48 - LMT 1932 N\n-9 - +09 1944 S\n-9:30 - +0930 1964\n+9 - %z 1944 S\n+9:30 - %z 1964\n 9 - WIT\n Z Asia/Jerusalem 2:20:54 - LMT 1880\n 2:20:40 - JMT 1918\n 2 Z I%sT\n Z Asia/Kabul 4:36:48 - LMT 1890\n-4 - +04 1945\n-4:30 - +0430\n+4 - %z 1945\n+4:30 - %z\n Z Asia/Kamchatka 10:34:36 - LMT 1922 N 10\n-11 - +11 1930 Jun 21\n-12 R +12/+13 1991 Mar 31 2s\n-11 R +11/+12 1992 Ja 19 2s\n-12 R +12/+13 2010 Mar 28 2s\n-11 R +11/+12 2011 Mar 27 2s\n-12 - +12\n+11 - %z 1930 Jun 21\n+12 R %z 1991 Mar 31 2s\n+11 R %z 1992 Ja 19 2s\n+12 R %z 2010 Mar 28 2s\n+11 R %z 2011 Mar 27 2s\n+12 - %z\n Z Asia/Karachi 4:28:12 - LMT 1907\n-5:30 - +0530 1942 S\n-5:30 1 +0630 1945 O 15\n-5:30 - +0530 1951 S 30\n-5 - +05 1971 Mar 26\n+5:30 - %z 1942 S\n+5:30 1 %z 1945 O 15\n+5:30 - %z 1951 S 30\n+5 - %z 1971 Mar 26\n 5 PK PK%sT\n Z Asia/Kathmandu 5:41:16 - LMT 1920\n-5:30 - +0530 1986\n-5:45 - +0545\n+5:30 - %z 1986\n+5:45 - %z\n Z Asia/Khandyga 9:2:13 - LMT 1919 D 15\n-8 - +08 1930 Jun 21\n-9 R +09/+10 1991 Mar 31 2s\n-8 R +08/+09 1992 Ja 19 2s\n-9 R +09/+10 2004\n-10 R +10/+11 2011 Mar 27 2s\n-11 - +11 2011 S 13 0s\n-10 - +10 2014 O 26 2s\n-9 - +09\n+8 - %z 1930 Jun 21\n+9 R %z 1991 Mar 31 2s\n+8 R %z 1992 Ja 19 2s\n+9 R %z 2004\n+10 R %z 2011 Mar 27 2s\n+11 - %z 2011 S 13 0s\n+10 - %z 2014 O 26 2s\n+9 - %z\n Z Asia/Kolkata 5:53:28 - LMT 1854 Jun 28\n 5:53:20 - HMT 1870\n 5:21:10 - MMT 1906\n 5:30 - IST 1941 O\n-5:30 1 +0630 1942 May 15\n+5:30 1 %z 1942 May 15\n 5:30 - IST 1942 S\n-5:30 1 +0630 1945 O 15\n+5:30 1 %z 1945 O 15\n 5:30 - IST\n Z Asia/Krasnoyarsk 6:11:26 - LMT 1920 Ja 6\n-6 - +06 1930 Jun 21\n-7 R +07/+08 1991 Mar 31 2s\n-6 R +06/+07 1992 Ja 19 2s\n-7 R +07/+08 2011 Mar 27 2s\n-8 - +08 2014 O 26 2s\n-7 - +07\n+6 - %z 1930 Jun 21\n+7 R %z 1991 Mar 31 2s\n+6 R %z 1992 Ja 19 2s\n+7 R %z 2011 Mar 27 2s\n+8 - %z 2014 O 26 2s\n+7 - %z\n Z Asia/Kuching 7:21:20 - LMT 1926 Mar\n-7:30 - +0730 1933\n-8 NB +08/+0820 1942 F 16\n-9 - +09 1945 S 12\n-8 - +08\n+7:30 - %z 1933\n+8 NB %z 1942 F 16\n+9 - %z 1945 S 12\n+8 - %z\n Z Asia/Macau 7:34:10 - LMT 1904 O 30\n 8 - CST 1941 D 21 23\n-9 _ +09/+10 1945 S 30 24\n+9 _ %z 1945 S 30 24\n 8 _ C%sT\n Z Asia/Magadan 10:3:12 - LMT 1924 May 2\n-10 - +10 1930 Jun 21\n-11 R +11/+12 1991 Mar 31 2s\n-10 R +10/+11 1992 Ja 19 2s\n-11 R +11/+12 2011 Mar 27 2s\n-12 - +12 2014 O 26 2s\n-10 - +10 2016 Ap 24 2s\n-11 - +11\n+10 - %z 1930 Jun 21\n+11 R %z 1991 Mar 31 2s\n+10 R %z 1992 Ja 19 2s\n+11 R %z 2011 Mar 27 2s\n+12 - %z 2014 O 26 2s\n+10 - %z 2016 Ap 24 2s\n+11 - %z\n Z Asia/Makassar 7:57:36 - LMT 1920\n 7:57:36 - MMT 1932 N\n-8 - +08 1942 F 9\n-9 - +09 1945 S 23\n+8 - %z 1942 F 9\n+9 - %z 1945 S 23\n 8 - WITA\n Z Asia/Manila -15:56 - LMT 1844 D 31\n 8:4 - LMT 1899 May 11\n@@ -3277,45 +3277,45 @@ Z Asia/Nicosia 2:13:28 - LMT 1921 N 14\n 2 CY EE%sT 1998 S\n 2 E EE%sT\n Z Asia/Novokuznetsk 5:48:48 - LMT 1924 May\n-6 - +06 1930 Jun 21\n-7 R +07/+08 1991 Mar 31 2s\n-6 R +06/+07 1992 Ja 19 2s\n-7 R +07/+08 2010 Mar 28 2s\n-6 R +06/+07 2011 Mar 27 2s\n-7 - +07\n+6 - %z 1930 Jun 21\n+7 R %z 1991 Mar 31 2s\n+6 R %z 1992 Ja 19 2s\n+7 R %z 2010 Mar 28 2s\n+6 R %z 2011 Mar 27 2s\n+7 - %z\n Z Asia/Novosibirsk 5:31:40 - LMT 1919 D 14 6\n-6 - +06 1930 Jun 21\n-7 R +07/+08 1991 Mar 31 2s\n-6 R +06/+07 1992 Ja 19 2s\n-7 R +07/+08 1993 May 23\n-6 R +06/+07 2011 Mar 27 2s\n-7 - +07 2014 O 26 2s\n-6 - +06 2016 Jul 24 2s\n-7 - +07\n+6 - %z 1930 Jun 21\n+7 R %z 1991 Mar 31 2s\n+6 R %z 1992 Ja 19 2s\n+7 R %z 1993 May 23\n+6 R %z 2011 Mar 27 2s\n+7 - %z 2014 O 26 2s\n+6 - %z 2016 Jul 24 2s\n+7 - %z\n Z Asia/Omsk 4:53:30 - LMT 1919 N 14\n-5 - +05 1930 Jun 21\n-6 R +06/+07 1991 Mar 31 2s\n-5 R +05/+06 1992 Ja 19 2s\n-6 R +06/+07 2011 Mar 27 2s\n-7 - +07 2014 O 26 2s\n-6 - +06\n+5 - %z 1930 Jun 21\n+6 R %z 1991 Mar 31 2s\n+5 R %z 1992 Ja 19 2s\n+6 R %z 2011 Mar 27 2s\n+7 - %z 2014 O 26 2s\n+6 - %z\n Z Asia/Oral 3:25:24 - LMT 1924 May 2\n-3 - +03 1930 Jun 21\n-5 - +05 1981 Ap\n-5 1 +06 1981 O\n-6 - +06 1982 Ap\n-5 R +05/+06 1989 Mar 26 2s\n-4 R +04/+05 1992 Ja 19 2s\n-5 R +05/+06 1992 Mar 29 2s\n-4 R +04/+05 2004 O 31 2s\n-5 - +05\n+3 - %z 1930 Jun 21\n+5 - %z 1981 Ap\n+5 1 %z 1981 O\n+6 - %z 1982 Ap\n+5 R %z 1989 Mar 26 2s\n+4 R %z 1992 Ja 19 2s\n+5 R %z 1992 Mar 29 2s\n+4 R %z 2004 O 31 2s\n+5 - %z\n Z Asia/Pontianak 7:17:20 - LMT 1908 May\n 7:17:20 - PMT 1932 N\n-7:30 - +0730 1942 Ja 29\n-9 - +09 1945 S 23\n-7:30 - +0730 1948 May\n-8 - +08 1950 May\n-7:30 - +0730 1964\n+7:30 - %z 1942 Ja 29\n+9 - %z 1945 S 23\n+7:30 - %z 1948 May\n+8 - %z 1950 May\n+7:30 - %z 1964\n 8 - WITA 1988\n 7 - WIB\n Z Asia/Pyongyang 8:23 - LMT 1908 Ap\n@@ -3325,48 +3325,48 @@ Z Asia/Pyongyang 8:23 - LMT 1908 Ap\n 8:30 - KST 2018 May 4 23:30\n 9 - KST\n Z Asia/Qatar 3:26:8 - LMT 1920\n-4 - +04 1972 Jun\n-3 - +03\n+4 - %z 1972 Jun\n+3 - %z\n Z Asia/Qostanay 4:14:28 - LMT 1924 May 2\n-4 - +04 1930 Jun 21\n-5 - +05 1981 Ap\n-5 1 +06 1981 O\n-6 - +06 1982 Ap\n-5 R +05/+06 1991 Mar 31 2s\n-4 R +04/+05 1992 Ja 19 2s\n-5 R +05/+06 2004 O 31 2s\n-6 - +06 2024 Mar\n-5 - +05\n+4 - %z 1930 Jun 21\n+5 - %z 1981 Ap\n+5 1 %z 1981 O\n+6 - %z 1982 Ap\n+5 R %z 1991 Mar 31 2s\n+4 R %z 1992 Ja 19 2s\n+5 R %z 2004 O 31 2s\n+6 - %z 2024 Mar\n+5 - %z\n Z Asia/Qyzylorda 4:21:52 - LMT 1924 May 2\n-4 - +04 1930 Jun 21\n-5 - +05 1981 Ap\n-5 1 +06 1981 O\n-6 - +06 1982 Ap\n-5 R +05/+06 1991 Mar 31 2s\n-4 R +04/+05 1991 S 29 2s\n-5 R +05/+06 1992 Ja 19 2s\n-6 R +06/+07 1992 Mar 29 2s\n-5 R +05/+06 2004 O 31 2s\n-6 - +06 2018 D 21\n-5 - +05\n+4 - %z 1930 Jun 21\n+5 - %z 1981 Ap\n+5 1 %z 1981 O\n+6 - %z 1982 Ap\n+5 R %z 1991 Mar 31 2s\n+4 R %z 1991 S 29 2s\n+5 R %z 1992 Ja 19 2s\n+6 R %z 1992 Mar 29 2s\n+5 R %z 2004 O 31 2s\n+6 - %z 2018 D 21\n+5 - %z\n Z Asia/Riyadh 3:6:52 - LMT 1947 Mar 14\n-3 - +03\n+3 - %z\n Z Asia/Sakhalin 9:30:48 - LMT 1905 Au 23\n-9 - +09 1945 Au 25\n-11 R +11/+12 1991 Mar 31 2s\n-10 R +10/+11 1992 Ja 19 2s\n-11 R +11/+12 1997 Mar lastSu 2s\n-10 R +10/+11 2011 Mar 27 2s\n-11 - +11 2014 O 26 2s\n-10 - +10 2016 Mar 27 2s\n-11 - +11\n+9 - %z 1945 Au 25\n+11 R %z 1991 Mar 31 2s\n+10 R %z 1992 Ja 19 2s\n+11 R %z 1997 Mar lastSu 2s\n+10 R %z 2011 Mar 27 2s\n+11 - %z 2014 O 26 2s\n+10 - %z 2016 Mar 27 2s\n+11 - %z\n Z Asia/Samarkand 4:27:53 - LMT 1924 May 2\n-4 - +04 1930 Jun 21\n-5 - +05 1981 Ap\n-5 1 +06 1981 O\n-6 - +06 1982 Ap\n-5 R +05/+06 1992\n-5 - +05\n+4 - %z 1930 Jun 21\n+5 - %z 1981 Ap\n+5 1 %z 1981 O\n+6 - %z 1982 Ap\n+5 R %z 1992\n+5 - %z\n Z Asia/Seoul 8:27:52 - LMT 1908 Ap\n 8:30 - KST 1912\n 9 - JST 1945 S 8\n@@ -3378,161 +3378,147 @@ Z Asia/Shanghai 8:5:43 - LMT 1901\n 8 CN C%sT\n Z Asia/Singapore 6:55:25 - LMT 1901\n 6:55:25 - SMT 1905 Jun\n-7 - +07 1933\n-7 0:20 +0720 1936\n-7:20 - +0720 1941 S\n-7:30 - +0730 1942 F 16\n-9 - +09 1945 S 12\n-7:30 - +0730 1981 D 31 16u\n-8 - +08\n+7 - %z 1933\n+7 0:20 %z 1936\n+7:20 - %z 1941 S\n+7:30 - %z 1942 F 16\n+9 - %z 1945 S 12\n+7:30 - %z 1981 D 31 16u\n+8 - %z\n Z Asia/Srednekolymsk 10:14:52 - LMT 1924 May 2\n-10 - +10 1930 Jun 21\n-11 R +11/+12 1991 Mar 31 2s\n-10 R +10/+11 1992 Ja 19 2s\n-11 R +11/+12 2011 Mar 27 2s\n-12 - +12 2014 O 26 2s\n-11 - +11\n+10 - %z 1930 Jun 21\n+11 R %z 1991 Mar 31 2s\n+10 R %z 1992 Ja 19 2s\n+11 R %z 2011 Mar 27 2s\n+12 - %z 2014 O 26 2s\n+11 - %z\n Z Asia/Taipei 8:6 - LMT 1896\n 8 - CST 1937 O\n 9 - JST 1945 S 21 1\n 8 f C%sT\n Z Asia/Tashkent 4:37:11 - LMT 1924 May 2\n-5 - +05 1930 Jun 21\n-6 R +06/+07 1991 Mar 31 2\n-5 R +05/+06 1992\n-5 - +05\n+5 - %z 1930 Jun 21\n+6 R %z 1991 Mar 31 2\n+5 R %z 1992\n+5 - %z\n Z Asia/Tbilisi 2:59:11 - LMT 1880\n 2:59:11 - TBMT 1924 May 2\n-3 - +03 1957 Mar\n-4 R +04/+05 1991 Mar 31 2s\n-3 R +03/+04 1992\n-3 e +03/+04 1994 S lastSu\n-4 e +04/+05 1996 O lastSu\n-4 1 +05 1997 Mar lastSu\n-4 e +04/+05 2004 Jun 27\n-3 R +03/+04 2005 Mar lastSu 2\n-4 - +04\n+3 - %z 1957 Mar\n+4 R %z 1991 Mar 31 2s\n+3 R %z 1992\n+3 e %z 1994 S lastSu\n+4 e %z 1996 O lastSu\n+4 1 %z 1997 Mar lastSu\n+4 e %z 2004 Jun 27\n+3 R %z 2005 Mar lastSu 2\n+4 - %z\n Z Asia/Tehran 3:25:44 - LMT 1916\n 3:25:44 - TMT 1935 Jun 13\n-3:30 i +0330/+0430 1977 O 20 24\n-4 i +04/+05 1979\n-3:30 i +0330/+0430\n+3:30 i %z 1977 O 20 24\n+4 i %z 1979\n+3:30 i %z\n Z Asia/Thimphu 5:58:36 - LMT 1947 Au 15\n-5:30 - +0530 1987 O\n-6 - +06\n+5:30 - %z 1987 O\n+6 - %z\n Z Asia/Tokyo 9:18:59 - LMT 1887 D 31 15u\n 9 JP J%sT\n Z Asia/Tomsk 5:39:51 - LMT 1919 D 22\n-6 - +06 1930 Jun 21\n-7 R +07/+08 1991 Mar 31 2s\n-6 R +06/+07 1992 Ja 19 2s\n-7 R +07/+08 2002 May 1 3\n-6 R +06/+07 2011 Mar 27 2s\n-7 - +07 2014 O 26 2s\n-6 - +06 2016 May 29 2s\n-7 - +07\n+6 - %z 1930 Jun 21\n+7 R %z 1991 Mar 31 2s\n+6 R %z 1992 Ja 19 2s\n+7 R %z 2002 May 1 3\n+6 R %z 2011 Mar 27 2s\n+7 - %z 2014 O 26 2s\n+6 - %z 2016 May 29 2s\n+7 - %z\n Z Asia/Ulaanbaatar 7:7:32 - LMT 1905 Au\n-7 - +07 1978\n-8 X +08/+09\n+7 - %z 1978\n+8 X %z\n Z Asia/Urumqi 5:50:20 - LMT 1928\n-6 - +06\n+6 - %z\n Z Asia/Ust-Nera 9:32:54 - LMT 1919 D 15\n-8 - +08 1930 Jun 21\n-9 R +09/+10 1981 Ap\n-11 R +11/+12 1991 Mar 31 2s\n-10 R +10/+11 1992 Ja 19 2s\n-11 R +11/+12 2011 Mar 27 2s\n-12 - +12 2011 S 13 0s\n-11 - +11 2014 O 26 2s\n-10 - +10\n+8 - %z 1930 Jun 21\n+9 R %z 1981 Ap\n+11 R %z 1991 Mar 31 2s\n+10 R %z 1992 Ja 19 2s\n+11 R %z 2011 Mar 27 2s\n+12 - %z 2011 S 13 0s\n+11 - %z 2014 O 26 2s\n+10 - %z\n Z Asia/Vladivostok 8:47:31 - LMT 1922 N 15\n-9 - +09 1930 Jun 21\n-10 R +10/+11 1991 Mar 31 2s\n-9 R +09/+10 1992 Ja 19 2s\n-10 R +10/+11 2011 Mar 27 2s\n-11 - +11 2014 O 26 2s\n-10 - +10\n+9 - %z 1930 Jun 21\n+10 R %z 1991 Mar 31 2s\n+9 R %z 1992 Ja 19 2s\n+10 R %z 2011 Mar 27 2s\n+11 - %z 2014 O 26 2s\n+10 - %z\n Z Asia/Yakutsk 8:38:58 - LMT 1919 D 15\n-8 - +08 1930 Jun 21\n-9 R +09/+10 1991 Mar 31 2s\n-8 R +08/+09 1992 Ja 19 2s\n-9 R +09/+10 2011 Mar 27 2s\n-10 - +10 2014 O 26 2s\n-9 - +09\n+8 - %z 1930 Jun 21\n+9 R %z 1991 Mar 31 2s\n+8 R %z 1992 Ja 19 2s\n+9 R %z 2011 Mar 27 2s\n+10 - %z 2014 O 26 2s\n+9 - %z\n Z Asia/Yangon 6:24:47 - LMT 1880\n 6:24:47 - RMT 1920\n-6:30 - +0630 1942 May\n-9 - +09 1945 May 3\n-6:30 - +0630\n+6:30 - %z 1942 May\n+9 - %z 1945 May 3\n+6:30 - %z\n Z Asia/Yekaterinburg 4:2:33 - LMT 1916 Jul 3\n 3:45:5 - PMT 1919 Jul 15 4\n-4 - +04 1930 Jun 21\n-5 R +05/+06 1991 Mar 31 2s\n-4 R +04/+05 1992 Ja 19 2s\n-5 R +05/+06 2011 Mar 27 2s\n-6 - +06 2014 O 26 2s\n-5 - +05\n+4 - %z 1930 Jun 21\n+5 R %z 1991 Mar 31 2s\n+4 R %z 1992 Ja 19 2s\n+5 R %z 2011 Mar 27 2s\n+6 - %z 2014 O 26 2s\n+5 - %z\n Z Asia/Yerevan 2:58 - LMT 1924 May 2\n-3 - +03 1957 Mar\n-4 R +04/+05 1991 Mar 31 2s\n-3 R +03/+04 1995 S 24 2s\n-4 - +04 1997\n-4 R +04/+05 2011\n-4 AM +04/+05\n+3 - %z 1957 Mar\n+4 R %z 1991 Mar 31 2s\n+3 R %z 1995 S 24 2s\n+4 - %z 1997\n+4 R %z 2011\n+4 AM %z\n Z Atlantic/Azores -1:42:40 - LMT 1884\n -1:54:32 - HMT 1912 Ja 1 2u\n--2 p -02/-01 1942 Ap 25 22s\n--2 p +00 1942 Au 15 22s\n--2 p -02/-01 1943 Ap 17 22s\n--2 p +00 1943 Au 28 22s\n--2 p -02/-01 1944 Ap 22 22s\n--2 p +00 1944 Au 26 22s\n--2 p -02/-01 1945 Ap 21 22s\n--2 p +00 1945 Au 25 22s\n--2 p -02/-01 1966 Ap 3 2\n--1 p -01/+00 1983 S 25 1s\n--1 W- -01/+00 1992 S 27 1s\n-0 E WE%sT 1993 Mar 28 1u\n--1 E -01/+00\n+-2 p %z 1966 O 2 2s\n+-1 - %z 1982 Mar 28 0s\n+-1 p %z 1986\n+-1 E %z 1992 D 27 1s\n+0 E WE%sT 1993 Jun 17 1u\n+-1 E %z\n Z Atlantic/Bermuda -4:19:18 - LMT 1890\n -4:19:18 Be BMT/BST 1930 Ja 1 2\n -4 Be A%sT 1974 Ap 28 2\n -4 C A%sT 1976\n -4 u A%sT\n Z Atlantic/Canary -1:1:36 - LMT 1922 Mar\n--1 - -01 1946 S 30 1\n+-1 - %z 1946 S 30 1\n 0 - WET 1980 Ap 6 0s\n 0 1 WEST 1980 S 28 1u\n 0 E WE%sT\n Z Atlantic/Cape_Verde -1:34:4 - LMT 1912 Ja 1 2u\n--2 - -02 1942 S\n--2 1 -01 1945 O 15\n--2 - -02 1975 N 25 2\n--1 - -01\n+-2 - %z 1942 S\n+-2 1 %z 1945 O 15\n+-2 - %z 1975 N 25 2\n+-1 - %z\n Z Atlantic/Faroe -0:27:4 - LMT 1908 Ja 11\n 0 - WET 1981\n 0 E WE%sT\n Z Atlantic/Madeira -1:7:36 - LMT 1884\n -1:7:36 - FMT 1912 Ja 1 1u\n--1 p -01/+00 1942 Ap 25 22s\n--1 p +01 1942 Au 15 22s\n--1 p -01/+00 1943 Ap 17 22s\n--1 p +01 1943 Au 28 22s\n--1 p -01/+00 1944 Ap 22 22s\n--1 p +01 1944 Au 26 22s\n--1 p -01/+00 1945 Ap 21 22s\n--1 p +01 1945 Au 25 22s\n--1 p -01/+00 1966 Ap 3 2\n-0 p WE%sT 1983 S 25 1s\n+-1 p %z 1966 O 2 2s\n+0 - WET 1982 Ap 4\n+0 p WE%sT 1986 Jul 31\n 0 E WE%sT\n Z Atlantic/South_Georgia -2:26:8 - LMT 1890\n--2 - -02\n+-2 - %z\n Z Atlantic/Stanley -3:51:24 - LMT 1890\n -3:51:24 - SMT 1912 Mar 12\n--4 FK -04/-03 1983 May\n--3 FK -03/-02 1985 S 15\n--4 FK -04/-03 2010 S 5 2\n--3 - -03\n+-4 FK %z 1983 May\n+-3 FK %z 1985 S 15\n+-4 FK %z 2010 S 5 2\n+-3 - %z\n Z Australia/Adelaide 9:14:20 - LMT 1895 F\n 9 - ACST 1899 May\n 9:30 AU AC%sT 1971\n@@ -3550,8 +3536,8 @@ Z Australia/Darwin 8:43:20 - LMT 1895 F\n 9 - ACST 1899 May\n 9:30 AU AC%sT\n Z Australia/Eucla 8:35:28 - LMT 1895 D\n-8:45 AU +0845/+0945 1943 Jul\n-8:45 AW +0845/+0945\n+8:45 AU %z 1943 Jul\n+8:45 AW %z\n Z Australia/Hobart 9:49:16 - LMT 1895 S\n 10 AT AE%sT 1919 O 24\n 10 AU AE%sT 1967\n@@ -3562,8 +3548,8 @@ Z Australia/Lindeman 9:55:56 - LMT 1895\n 10 Ho AE%sT\n Z Australia/Lord_Howe 10:36:20 - LMT 1895 F\n 10 - AEST 1981 Mar\n-10:30 LH +1030/+1130 1985 Jul\n-10:30 LH +1030/+11\n+10:30 LH %z 1985 Jul\n+10:30 LH %z\n Z Australia/Melbourne 9:39:52 - LMT 1895 F\n 10 AU AE%sT 1971\n 10 AV AE%sT\n@@ -3573,52 +3559,47 @@ Z Australia/Perth 7:43:24 - LMT 1895 D\n Z Australia/Sydney 10:4:52 - LMT 1895 F\n 10 AU AE%sT 1971\n 10 AN AE%sT\n-Z CET 1 c CE%sT\n-Z CST6CDT -6 u C%sT\n-Z EET 2 E EE%sT\n-Z EST -5 - EST\n-Z EST5EDT -5 u E%sT\n Z Etc/GMT 0 - GMT\n-Z Etc/GMT+1 -1 - -01\n-Z Etc/GMT+10 -10 - -10\n-Z Etc/GMT+11 -11 - -11\n-Z Etc/GMT+12 -12 - -12\n-Z Etc/GMT+2 -2 - -02\n-Z Etc/GMT+3 -3 - -03\n-Z Etc/GMT+4 -4 - -04\n-Z Etc/GMT+5 -5 - -05\n-Z Etc/GMT+6 -6 - -06\n-Z Etc/GMT+7 -7 - -07\n-Z Etc/GMT+8 -8 - -08\n-Z Etc/GMT+9 -9 - -09\n-Z Etc/GMT-1 1 - +01\n-Z Etc/GMT-10 10 - +10\n-Z Etc/GMT-11 11 - +11\n-Z Etc/GMT-12 12 - +12\n-Z Etc/GMT-13 13 - +13\n-Z Etc/GMT-14 14 - +14\n-Z Etc/GMT-2 2 - +02\n-Z Etc/GMT-3 3 - +03\n-Z Etc/GMT-4 4 - +04\n-Z Etc/GMT-5 5 - +05\n-Z Etc/GMT-6 6 - +06\n-Z Etc/GMT-7 7 - +07\n-Z Etc/GMT-8 8 - +08\n-Z Etc/GMT-9 9 - +09\n+Z Etc/GMT+1 -1 - %z\n+Z Etc/GMT+10 -10 - %z\n+Z Etc/GMT+11 -11 - %z\n+Z Etc/GMT+12 -12 - %z\n+Z Etc/GMT+2 -2 - %z\n+Z Etc/GMT+3 -3 - %z\n+Z Etc/GMT+4 -4 - %z\n+Z Etc/GMT+5 -5 - %z\n+Z Etc/GMT+6 -6 - %z\n+Z Etc/GMT+7 -7 - %z\n+Z Etc/GMT+8 -8 - %z\n+Z Etc/GMT+9 -9 - %z\n+Z Etc/GMT-1 1 - %z\n+Z Etc/GMT-10 10 - %z\n+Z Etc/GMT-11 11 - %z\n+Z Etc/GMT-12 12 - %z\n+Z Etc/GMT-13 13 - %z\n+Z Etc/GMT-14 14 - %z\n+Z Etc/GMT-2 2 - %z\n+Z Etc/GMT-3 3 - %z\n+Z Etc/GMT-4 4 - %z\n+Z Etc/GMT-5 5 - %z\n+Z Etc/GMT-6 6 - %z\n+Z Etc/GMT-7 7 - %z\n+Z Etc/GMT-8 8 - %z\n+Z Etc/GMT-9 9 - %z\n Z Etc/UTC 0 - UTC\n Z Europe/Andorra 0:6:4 - LMT 1901\n 0 - WET 1946 S 30\n 1 - CET 1985 Mar 31 2\n 1 E CE%sT\n Z Europe/Astrakhan 3:12:12 - LMT 1924 May\n-3 - +03 1930 Jun 21\n-4 R +04/+05 1989 Mar 26 2s\n-3 R +03/+04 1991 Mar 31 2s\n-4 - +04 1992 Mar 29 2s\n-3 R +03/+04 2011 Mar 27 2s\n-4 - +04 2014 O 26 2s\n-3 - +03 2016 Mar 27 2s\n-4 - +04\n+3 - %z 1930 Jun 21\n+4 R %z 1989 Mar 26 2s\n+3 R %z 1991 Mar 31 2s\n+4 - %z 1992 Mar 29 2s\n+3 R %z 2011 Mar 27 2s\n+4 - %z 2014 O 26 2s\n+3 - %z 2016 Mar 27 2s\n+4 - %z\n Z Europe/Athens 1:34:52 - LMT 1895 S 14\n 1:34:52 - AMT 1916 Jul 28 0:1\n 2 g EE%sT 1941 Ap 30\n@@ -3691,7 +3672,7 @@ Z Europe/Helsinki 1:39:49 - LMT 1878 May 31\n Z Europe/Istanbul 1:55:52 - LMT 1880\n 1:56:56 - IMT 1910 O\n 2 T EE%sT 1978 Jun 29\n-3 T +03/+04 1984 N 1 2\n+3 T %z 1984 N 1 2\n 2 T EE%sT 2007\n 2 E EE%sT 2011 Mar 27 1u\n 2 - EET 2011 Mar 28 1u\n@@ -3700,19 +3681,19 @@ Z Europe/Istanbul 1:55:52 - LMT 1880\n 2 E EE%sT 2015 O 25 1u\n 2 1 EEST 2015 N 8 1u\n 2 E EE%sT 2016 S 7\n-3 - +03\n+3 - %z\n Z Europe/Kaliningrad 1:22 - LMT 1893 Ap\n 1 c CE%sT 1945 Ap 10\n 2 O EE%sT 1946 Ap 7\n 3 R MSK/MSD 1989 Mar 26 2s\n 2 R EE%sT 2011 Mar 27 2s\n-3 - +03 2014 O 26 2s\n+3 - %z 2014 O 26 2s\n 2 - EET\n Z Europe/Kirov 3:18:48 - LMT 1919 Jul 1 0u\n-3 - +03 1930 Jun 21\n-4 R +04/+05 1989 Mar 26 2s\n+3 - %z 1930 Jun 21\n+4 R %z 1989 Mar 26 2s\n 3 R MSK/MSD 1991 Mar 31 2s\n-4 - +04 1992 Mar 29 2s\n+4 - %z 1992 Mar 29 2s\n 3 R MSK/MSD 2011 Mar 27 2s\n 4 - MSK 2014 O 26 2s\n 3 - MSK\n@@ -3727,10 +3708,10 @@ Z Europe/Kyiv 2:2:4 - LMT 1880\n 2 E EE%sT\n Z Europe/Lisbon -0:36:45 - LMT 1884\n -0:36:45 - LMT 1912 Ja 1 0u\n-0 p WE%sT 1966 Ap 3 2\n+0 p WE%sT 1966 O 2 2s\n 1 - CET 1976 S 26 1\n-0 p WE%sT 1983 S 25 1s\n-0 W- WE%sT 1992 S 27 1s\n+0 p WE%sT 1986\n+0 E WE%sT 1992 S 27 1u\n 1 E CE%sT 1996 Mar 31 1u\n 0 E WE%sT\n Z Europe/London -0:1:15 - LMT 1847 D\n@@ -3754,7 +3735,7 @@ Z Europe/Minsk 1:50:16 - LMT 1880\n 3 R MSK/MSD 1990\n 3 - MSK 1991 Mar 31 2s\n 2 R EE%sT 2011 Mar 27 2s\n-3 - +03\n+3 - %z\n Z Europe/Moscow 2:30:17 - LMT 1880\n 2:30:17 - MMT 1916 Jul 3\n 2:31:19 R %s 1919 Jul 1 0u\n@@ -3802,24 +3783,24 @@ Z Europe/Rome 0:49:56 - LMT 1866 D 12\n 1 I CE%sT 1980\n 1 E CE%sT\n Z Europe/Samara 3:20:20 - LMT 1919 Jul 1 0u\n-3 - +03 1930 Jun 21\n-4 - +04 1935 Ja 27\n-4 R +04/+05 1989 Mar 26 2s\n-3 R +03/+04 1991 Mar 31 2s\n-2 R +02/+03 1991 S 29 2s\n-3 - +03 1991 O 20 3\n-4 R +04/+05 2010 Mar 28 2s\n-3 R +03/+04 2011 Mar 27 2s\n-4 - +04\n+3 - %z 1930 Jun 21\n+4 - %z 1935 Ja 27\n+4 R %z 1989 Mar 26 2s\n+3 R %z 1991 Mar 31 2s\n+2 R %z 1991 S 29 2s\n+3 - %z 1991 O 20 3\n+4 R %z 2010 Mar 28 2s\n+3 R %z 2011 Mar 27 2s\n+4 - %z\n Z Europe/Saratov 3:4:18 - LMT 1919 Jul 1 0u\n-3 - +03 1930 Jun 21\n-4 R +04/+05 1988 Mar 27 2s\n-3 R +03/+04 1991 Mar 31 2s\n-4 - +04 1992 Mar 29 2s\n-3 R +03/+04 2011 Mar 27 2s\n-4 - +04 2014 O 26 2s\n-3 - +03 2016 D 4 2s\n-4 - +04\n+3 - %z 1930 Jun 21\n+4 R %z 1988 Mar 27 2s\n+3 R %z 1991 Mar 31 2s\n+4 - %z 1992 Mar 29 2s\n+3 R %z 2011 Mar 27 2s\n+4 - %z 2014 O 26 2s\n+3 - %z 2016 D 4 2s\n+4 - %z\n Z Europe/Simferopol 2:16:24 - LMT 1880\n 2:16 - SMT 1924 May 2\n 2 - EET 1930 Jun 21\n@@ -3863,14 +3844,14 @@ Z Europe/Tirane 1:19:20 - LMT 1914\n 1 q CE%sT 1984 Jul\n 1 E CE%sT\n Z Europe/Ulyanovsk 3:13:36 - LMT 1919 Jul 1 0u\n-3 - +03 1930 Jun 21\n-4 R +04/+05 1989 Mar 26 2s\n-3 R +03/+04 1991 Mar 31 2s\n-2 R +02/+03 1992 Ja 19 2s\n-3 R +03/+04 2011 Mar 27 2s\n-4 - +04 2014 O 26 2s\n-3 - +03 2016 Mar 27 2s\n-4 - +04\n+3 - %z 1930 Jun 21\n+4 R %z 1989 Mar 26 2s\n+3 R %z 1991 Mar 31 2s\n+2 R %z 1992 Ja 19 2s\n+3 R %z 2011 Mar 27 2s\n+4 - %z 2014 O 26 2s\n+3 - %z 2016 Mar 27 2s\n+4 - %z\n Z Europe/Vienna 1:5:21 - LMT 1893 Ap\n 1 c CE%sT 1920\n 1 a CE%sT 1940 Ap 1 2s\n@@ -3895,15 +3876,15 @@ Z Europe/Vilnius 1:41:16 - LMT 1880\n 2 - EET 2003\n 2 E EE%sT\n Z Europe/Volgograd 2:57:40 - LMT 1920 Ja 3\n-3 - +03 1930 Jun 21\n-4 - +04 1961 N 11\n-4 R +04/+05 1988 Mar 27 2s\n+3 - %z 1930 Jun 21\n+4 - %z 1961 N 11\n+4 R %z 1988 Mar 27 2s\n 3 R MSK/MSD 1991 Mar 31 2s\n-4 - +04 1992 Mar 29 2s\n+4 - %z 1992 Mar 29 2s\n 3 R MSK/MSD 2011 Mar 27 2s\n 4 - MSK 2014 O 26 2s\n 3 - MSK 2018 O 28 2s\n-4 - +04 2020 D 27 2s\n+4 - %z 2020 D 27 2s\n 3 - MSK\n Z Europe/Warsaw 1:24 - LMT 1880\n 1:24 - WMT 1915 Au 5\n@@ -3919,58 +3900,53 @@ Z Europe/Zurich 0:34:8 - LMT 1853 Jul 16\n 1 CH CE%sT 1981\n 1 E CE%sT\n Z Factory 0 - -00\n-Z HST -10 - HST\n Z Indian/Chagos 4:49:40 - LMT 1907\n-5 - +05 1996\n-6 - +06\n+5 - %z 1996\n+6 - %z\n Z Indian/Maldives 4:54 - LMT 1880\n 4:54 - MMT 1960\n-5 - +05\n+5 - %z\n Z Indian/Mauritius 3:50 - LMT 1907\n-4 MU +04/+05\n-Z MET 1 c ME%sT\n-Z MST -7 - MST\n-Z MST7MDT -7 u M%sT\n-Z PST8PDT -8 u P%sT\n+4 MU %z\n Z Pacific/Apia 12:33:4 - LMT 1892 Jul 5\n -11:26:56 - LMT 1911\n--11:30 - -1130 1950\n--11 WS -11/-10 2011 D 29 24\n-13 WS +13/+14\n+-11:30 - %z 1950\n+-11 WS %z 2011 D 29 24\n+13 WS %z\n Z Pacific/Auckland 11:39:4 - LMT 1868 N 2\n 11:30 NZ NZ%sT 1946\n 12 NZ NZ%sT\n Z Pacific/Bougainville 10:22:16 - LMT 1880\n 9:48:32 - PMMT 1895\n-10 - +10 1942 Jul\n-9 - +09 1945 Au 21\n-10 - +10 2014 D 28 2\n-11 - +11\n+10 - %z 1942 Jul\n+9 - %z 1945 Au 21\n+10 - %z 2014 D 28 2\n+11 - %z\n Z Pacific/Chatham 12:13:48 - LMT 1868 N 2\n-12:15 - +1215 1946\n-12:45 k +1245/+1345\n+12:15 - %z 1946\n+12:45 k %z\n Z Pacific/Easter -7:17:28 - LMT 1890\n -7:17:28 - EMT 1932 S\n--7 x -07/-06 1982 Mar 14 3u\n--6 x -06/-05\n+-7 x %z 1982 Mar 14 3u\n+-6 x %z\n Z Pacific/Efate 11:13:16 - LMT 1912 Ja 13\n-11 VU +11/+12\n+11 VU %z\n Z Pacific/Fakaofo -11:24:56 - LMT 1901\n--11 - -11 2011 D 30\n-13 - +13\n+-11 - %z 2011 D 30\n+13 - %z\n Z Pacific/Fiji 11:55:44 - LMT 1915 O 26\n-12 FJ +12/+13\n+12 FJ %z\n Z Pacific/Galapagos -5:58:24 - LMT 1931\n--5 - -05 1986\n--6 EC -06/-05\n+-5 - %z 1986\n+-6 EC %z\n Z Pacific/Gambier -8:59:48 - LMT 1912 O\n--9 - -09\n+-9 - %z\n Z Pacific/Guadalcanal 10:39:48 - LMT 1912 O\n-11 - +11\n+11 - %z\n Z Pacific/Guam -14:21 - LMT 1844 D 31\n 9:39 - LMT 1901\n 10 - GST 1941 D 10\n-9 - +09 1944 Jul 31\n+9 - %z 1944 Jul 31\n 10 Gu G%sT 2000 D 23\n 10 - ChST\n Z Pacific/Honolulu -10:31:26 - LMT 1896 Ja 13 12\n@@ -3979,74 +3955,73 @@ Z Pacific/Honolulu -10:31:26 - LMT 1896 Ja 13 12\n -10:30 u H%sT 1947 Jun 8 2\n -10 - HST\n Z Pacific/Kanton 0 - -00 1937 Au 31\n--12 - -12 1979 O\n--11 - -11 1994 D 31\n-13 - +13\n+-12 - %z 1979 O\n+-11 - %z 1994 D 31\n+13 - %z\n Z Pacific/Kiritimati -10:29:20 - LMT 1901\n--10:40 - -1040 1979 O\n--10 - -10 1994 D 31\n-14 - +14\n+-10:40 - %z 1979 O\n+-10 - %z 1994 D 31\n+14 - %z\n Z Pacific/Kosrae -13:8:4 - LMT 1844 D 31\n 10:51:56 - LMT 1901\n-11 - +11 1914 O\n-9 - +09 1919 F\n-11 - +11 1937\n-10 - +10 1941 Ap\n-9 - +09 1945 Au\n-11 - +11 1969 O\n-12 - +12 1999\n-11 - +11\n+11 - %z 1914 O\n+9 - %z 1919 F\n+11 - %z 1937\n+10 - %z 1941 Ap\n+9 - %z 1945 Au\n+11 - %z 1969 O\n+12 - %z 1999\n+11 - %z\n Z Pacific/Kwajalein 11:9:20 - LMT 1901\n-11 - +11 1937\n-10 - +10 1941 Ap\n-9 - +09 1944 F 6\n-11 - +11 1969 O\n--12 - -12 1993 Au 20 24\n-12 - +12\n+11 - %z 1937\n+10 - %z 1941 Ap\n+9 - %z 1944 F 6\n+11 - %z 1969 O\n+-12 - %z 1993 Au 20 24\n+12 - %z\n Z Pacific/Marquesas -9:18 - LMT 1912 O\n--9:30 - -0930\n+-9:30 - %z\n Z Pacific/Nauru 11:7:40 - LMT 1921 Ja 15\n-11:30 - +1130 1942 Au 29\n-9 - +09 1945 S 8\n-11:30 - +1130 1979 F 10 2\n-12 - +12\n+11:30 - %z 1942 Au 29\n+9 - %z 1945 S 8\n+11:30 - %z 1979 F 10 2\n+12 - %z\n Z Pacific/Niue -11:19:40 - LMT 1952 O 16\n--11:20 - -1120 1964 Jul\n--11 - -11\n+-11:20 - %z 1964 Jul\n+-11 - %z\n Z Pacific/Norfolk 11:11:52 - LMT 1901\n-11:12 - +1112 1951\n-11:30 - +1130 1974 O 27 2s\n-11:30 1 +1230 1975 Mar 2 2s\n-11:30 - +1130 2015 O 4 2s\n-11 - +11 2019 Jul\n-11 AN +11/+12\n+11:12 - %z 1951\n+11:30 - %z 1974 O 27 2s\n+11:30 1 %z 1975 Mar 2 2s\n+11:30 - %z 2015 O 4 2s\n+11 - %z 2019 Jul\n+11 AN %z\n Z Pacific/Noumea 11:5:48 - LMT 1912 Ja 13\n-11 NC +11/+12\n+11 NC %z\n Z Pacific/Pago_Pago 12:37:12 - LMT 1892 Jul 5\n -11:22:48 - LMT 1911\n -11 - SST\n Z Pacific/Palau -15:2:4 - LMT 1844 D 31\n 8:57:56 - LMT 1901\n-9 - +09\n+9 - %z\n Z Pacific/Pitcairn -8:40:20 - LMT 1901\n--8:30 - -0830 1998 Ap 27\n--8 - -08\n+-8:30 - %z 1998 Ap 27\n+-8 - %z\n Z Pacific/Port_Moresby 9:48:40 - LMT 1880\n 9:48:32 - PMMT 1895\n-10 - +10\n+10 - %z\n Z Pacific/Rarotonga 13:20:56 - LMT 1899 D 26\n -10:39:4 - LMT 1952 O 16\n--10:30 - -1030 1978 N 12\n--10 CK -10/-0930\n+-10:30 - %z 1978 N 12\n+-10 CK %z\n Z Pacific/Tahiti -9:58:16 - LMT 1912 O\n--10 - -10\n+-10 - %z\n Z Pacific/Tarawa 11:32:4 - LMT 1901\n-12 - +12\n+12 - %z\n Z Pacific/Tongatapu 12:19:12 - LMT 1945 S 10\n-12:20 - +1220 1961\n-13 - +13 1999\n-13 TO +13/+14\n-Z WET 0 E WE%sT\n+12:20 - %z 1961\n+13 - %z 1999\n+13 TO %z\n L Etc/GMT GMT\n L Australia/Sydney Australia/ACT\n L Australia/Lord_Howe Australia/LHI\n@@ -4062,6 +4037,8 @@ L America/Rio_Branco Brazil/Acre\n L America/Noronha Brazil/DeNoronha\n L America/Sao_Paulo Brazil/East\n L America/Manaus Brazil/West\n+L Europe/Brussels CET\n+L America/Chicago CST6CDT\n L America/Halifax Canada/Atlantic\n L America/Winnipeg Canada/Central\n L America/Toronto Canada/Eastern\n@@ -4073,6 +4050,9 @@ L America/Whitehorse Canada/Yukon\n L America/Santiago Chile/Continental\n L Pacific/Easter Chile/EasterIsland\n L America/Havana Cuba\n+L Europe/Athens EET\n+L America/Panama EST\n+L America/New_York EST5EDT\n L Africa/Cairo Egypt\n L Europe/Dublin Eire\n L Etc/GMT Etc/GMT+0\n@@ -4096,6 +4076,9 @@ L America/Jamaica Jamaica\n L Asia/Tokyo Japan\n L Pacific/Kwajalein Kwajalein\n L Africa/Tripoli Libya\n+L Europe/Brussels MET\n+L America/Phoenix MST\n+L America/Denver MST7MDT\n L America/Tijuana Mexico/BajaNorte\n L America/Mazatlan Mexico/BajaSur\n L America/Mexico_City Mexico/General\n@@ -4259,6 +4242,7 @@ L America/Denver America/Shiprock\n L America/Toronto America/Thunder_Bay\n L America/Edmonton America/Yellowknife\n L Pacific/Auckland Antarctica/South_Pole\n+L Asia/Ulaanbaatar Asia/Choibalsan\n L Asia/Shanghai Asia/Chongqing\n L Asia/Shanghai Asia/Harbin\n L Asia/Urumqi Asia/Kashgar\n@@ -4273,6 +4257,7 @@ L Europe/Kyiv Europe/Zaporozhye\n L Pacific/Kanton Pacific/Enderbury\n L Pacific/Honolulu Pacific/Johnston\n L Pacific/Port_Moresby Pacific/Yap\n+L Europe/Lisbon WET\n L Africa/Nairobi Africa/Asmera\n L America/Nuuk America/Godthab\n L Asia/Ashgabat Asia/Ashkhabad\n@@ -4290,5 +4275,7 @@ L Asia/Ulaanbaatar Asia/Ulan_Bator\n L Atlantic/Faroe Atlantic/Faeroe\n L Europe/Kyiv Europe/Kiev\n L Asia/Nicosia Europe/Nicosia\n+L Pacific/Honolulu HST\n+L America/Los_Angeles PST8PDT\n L Pacific/Guadalcanal Pacific/Ponape\n L Pacific/Port_Moresby Pacific/Truk"}, {"sha": "bfc0b593304470c69ba2a56b69f593a76e47961a", "filename": "lib/pytz/zoneinfo/zone.tab", "status": "modified", "additions": 1, "deletions": 2, "changes": 3, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2Fzone.tab", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2Fzone.tab", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2Fzone.tab?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -264,8 +264,7 @@ MK\t+4159+02126\tEurope/Skopje\n ML\t+1239-00800\tAfrica/Bamako\n MM\t+1647+09610\tAsia/Yangon\n MN\t+4755+10653\tAsia/Ulaanbaatar\tmost of Mongolia\n-MN\t+4801+09139\tAsia/Hovd\tBayan-Olgiy, Govi-Altai, Hovd, Uvs, Zavkhan\n-MN\t+4804+11430\tAsia/Choibalsan\tDornod, Sukhbaatar\n+MN\t+4801+09139\tAsia/Hovd\tBayan-Olgii, Hovd, Uvs\n MO\t+221150+1133230\tAsia/Macau\n MP\t+1512+14545\tPacific/Saipan\n MQ\t+1436-06105\tAmerica/Martinique"}, {"sha": "7726f39a0922543715c7442a2dd0a128919ad43a", "filename": "lib/pytz/zoneinfo/zone1970.tab", "status": "modified", "additions": 1, "deletions": 2, "changes": 3, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2Fzone1970.tab", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2Fzone1970.tab", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2Fzone1970.tab?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -209,8 +209,7 @@ MD\t+4700+02850\tEurope/Chisinau\n MH\t+0905+16720\tPacific/Kwajalein\tKwajalein\n MM,CC\t+1647+09610\tAsia/Yangon\n MN\t+4755+10653\tAsia/Ulaanbaatar\tmost of Mongolia\n-MN\t+4801+09139\tAsia/Hovd\tBayan-\u00d6lgii, Govi-Altai, Hovd, Uvs, Zavkhan\n-MN\t+4804+11430\tAsia/Choibalsan\tDornod, S\u00fckhbaatar\n+MN\t+4801+09139\tAsia/Hovd\tBayan-\u00d6lgii, Hovd, Uvs\n MO\t+221150+1133230\tAsia/Macau\n MQ\t+1436-06105\tAmerica/Martinique\n MT\t+3554+01431\tEurope/Malta"}, {"sha": "01f536b3ba3833dcb5c4ee25d6e18bd3772e860a", "filename": "lib/pytz/zoneinfo/zonenow.tab", "status": "modified", "additions": 2, "deletions": 6, "changes": 8, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2Fzonenow.tab", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fpytz%2Fzoneinfo%2Fzonenow.tab", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fpytz%2Fzoneinfo%2Fzonenow.tab?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -5,7 +5,7 @@\n # From Paul Eggert (2023-12-18):\n # This file contains a table where each row stands for a timezone\n # where civil timestamps are predicted to agree from now on.\n-# This file is like zone1970.tab (see zone1970.tab's coments),\n+# This file is like zone1970.tab (see zone1970.tab's comments),\n # but with the following changes:\n #\n # 1. Each timezone corresponds to a set of clocks that are planned\n@@ -123,8 +123,6 @@ XX\t+1455-02331\tAtlantic/Cape_Verde\tCape Verde\n #\n # -01/+00 (EU DST)\n XX\t+3744-02540\tAtlantic/Azores\tAzores\n-# -01/+00 (EU DST) until 2024-03-31; then -02/-01 (EU DST)\n-XX\t+7029-02158\tAmerica/Scoresbysund\tIttoqqortoormiit\n #\n # +00 - GMT\n XX\t+0519-00402\tAfrica/Abidjan\tfar western Africa; Iceland (\"GMT\")\n@@ -199,7 +197,7 @@ XX\t+2518+05518\tAsia/Dubai\tRussia; Caucasus; Persian Gulf; Seychelles; R\u00e9union\n XX\t+3431+06912\tAsia/Kabul\tAfghanistan\n #\n # +05\n-XX\t+4120+06918\tAsia/Tashkent\tRussia; west Kazakhstan; Tajikistan; Turkmenistan; Uzbekistan; Maldives\n+XX\t+4120+06918\tAsia/Tashkent\tRussia; Kazakhstan; Tajikistan; Turkmenistan; Uzbekistan; Maldives\n #\n # +05 - PKT\n XX\t+2452+06703\tAsia/Karachi\tPakistan (\"PKT\")\n@@ -215,8 +213,6 @@ XX\t+2743+08519\tAsia/Kathmandu\tNepal\n #\n # +06\n XX\t+2343+09025\tAsia/Dhaka\tRussia; Kyrgyzstan; Bhutan; Bangladesh; Chagos\n-# +06 until 2024-03-01; then +05\n-XX\t+4315+07657\tAsia/Almaty\tKazakhstan (except western areas)\n #\n # +06:30\n XX\t+1647+09610\tAsia/Yangon\tMyanmar; Cocos"}, {"sha": "d3ff141d00bd36ae866f972a0cabdd87483649ed", "filename": "lib/simplejson/__init__.py", "status": "modified", "additions": 1, "deletions": 1, "changes": 2, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fsimplejson%2F__init__.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fsimplejson%2F__init__.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fsimplejson%2F__init__.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -118,7 +118,7 @@\n \n \"\"\"\n from __future__ import absolute_import\n-__version__ = '3.19.2'\n+__version__ = '3.19.3'\n __all__ = [\n 'dump', 'dumps', 'load', 'loads',\n 'JSONDecoder', 'JSONDecodeError', 'JSONEncoder',"}, {"sha": "f909ead7a289a11e5eafaf32aec18f24e28e788c", "filename": "lib/tokenize_rt.py", "status": "modified", "additions": 18, "deletions": 6, "changes": 24, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftokenize_rt.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftokenize_rt.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftokenize_rt.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -6,11 +6,11 @@\n import re\n import sys\n import tokenize\n-from typing import Generator\n-from typing import Iterable\n+from collections.abc import Generator\n+from collections.abc import Iterable\n+from collections.abc import Sequence\n+from re import Pattern\n from typing import NamedTuple\n-from typing import Pattern\n-from typing import Sequence\n \n # this is a performance hack. see https://bugs.python.org/issue43014\n if ( # pragma: no branch\n@@ -47,6 +47,16 @@ def matches(self, *, name: str, src: str) -> bool:\n _string_re = re.compile('^([^\\'\"]*)(.*)$', re.DOTALL)\n _escaped_nl_re = re.compile(r'\\\\(\\n|\\r\\n|\\r)')\n \n+NAMED_UNICODE_RE = re.compile(r'(?<!\\\\)(?:\\\\\\\\)*(\\\\N\\{[^}]+\\})')\n+\n+\n+def curly_escape(s: str) -> str:\n+ parts = NAMED_UNICODE_RE.split(s)\n+ return ''.join(\n+ part.replace('{', '{{').replace('}', '}}') if i % 2 == 0 else part\n+ for i, part in enumerate(parts)\n+ )\n+\n \n def _re_partition(regex: Pattern[str], s: str) -> tuple[str, str, str]:\n match = regex.search(s)\n@@ -101,8 +111,10 @@ def src_to_tokens(src: str) -> list[Token]:\n tok_name = tokenize.tok_name[tok_type]\n \n if tok_name == 'FSTRING_MIDDLE': # pragma: >=3.12 cover\n- ecol += tok_text.count('{') + tok_text.count('}')\n- tok_text = tok_text.replace('{', '{{').replace('}', '}}')\n+ if '{' in tok_text or '}' in tok_text:\n+ new_tok_text = curly_escape(tok_text)\n+ ecol += len(new_tok_text) - len(tok_text)\n+ tok_text = new_tok_text\n \n tokens.append(Token(tok_name, tok_text, sline, end_offset))\n last_line, last_col = eline, ecol"}, {"sha": "e558a8a536ce2d2498fdde3d7832b5168d8245bc", "filename": "lib/tzdata/__init__.py", "status": "modified", "additions": 2, "deletions": 2, "changes": 4, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2F__init__.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2F__init__.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2F__init__.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -1,6 +1,6 @@\n # IANA versions like 2020a are not valid PEP 440 identifiers; the recommended\n # way to translate the version is to use YYYY.n where `n` is a 0-based index.\n-__version__ = \"2024.1\"\n+__version__ = \"2024.2\"\n \n # This exposes the original IANA version number.\n-IANA_VERSION = \"2024a\"\n+IANA_VERSION = \"2024b\""}, {"sha": "581bb0e08b616a433d422ccb8f958cbebdae1770", "filename": "lib/tzdata/zoneinfo/Africa/Blantyre", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAfrica%2FBlantyre", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAfrica%2FBlantyre", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FAfrica%2FBlantyre?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "581bb0e08b616a433d422ccb8f958cbebdae1770", "filename": "lib/tzdata/zoneinfo/Africa/Bujumbura", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAfrica%2FBujumbura", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAfrica%2FBujumbura", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FAfrica%2FBujumbura?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "581bb0e08b616a433d422ccb8f958cbebdae1770", "filename": "lib/tzdata/zoneinfo/Africa/Gaborone", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAfrica%2FGaborone", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAfrica%2FGaborone", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FAfrica%2FGaborone?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "581bb0e08b616a433d422ccb8f958cbebdae1770", "filename": "lib/tzdata/zoneinfo/Africa/Harare", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAfrica%2FHarare", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAfrica%2FHarare", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FAfrica%2FHarare?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "581bb0e08b616a433d422ccb8f958cbebdae1770", "filename": "lib/tzdata/zoneinfo/Africa/Kigali", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAfrica%2FKigali", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAfrica%2FKigali", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FAfrica%2FKigali?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "581bb0e08b616a433d422ccb8f958cbebdae1770", "filename": "lib/tzdata/zoneinfo/Africa/Lubumbashi", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAfrica%2FLubumbashi", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAfrica%2FLubumbashi", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FAfrica%2FLubumbashi?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "581bb0e08b616a433d422ccb8f958cbebdae1770", "filename": "lib/tzdata/zoneinfo/Africa/Lusaka", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAfrica%2FLusaka", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAfrica%2FLusaka", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FAfrica%2FLusaka?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "581bb0e08b616a433d422ccb8f958cbebdae1770", "filename": "lib/tzdata/zoneinfo/Africa/Maputo", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAfrica%2FMaputo", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAfrica%2FMaputo", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FAfrica%2FMaputo?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "882400bd33bdc23fc75b092a01ea935b02431715", "filename": "lib/tzdata/zoneinfo/America/Bahia_Banderas", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FBahia_Banderas", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FBahia_Banderas", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FBahia_Banderas?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "3110cdfd6e6f4ccd889447657dff43560d5aaee0", "filename": "lib/tzdata/zoneinfo/America/Cancun", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FCancun", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FCancun", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FCancun?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "f65bb1c9310447737822ad026470d5092ce87678", "filename": "lib/tzdata/zoneinfo/America/Chihuahua", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FChihuahua", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FChihuahua", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FChihuahua?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "5f865ea808b57d97634d4331fc5fce84349ded36", "filename": "lib/tzdata/zoneinfo/America/Ciudad_Juarez", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FCiudad_Juarez", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FCiudad_Juarez", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FCiudad_Juarez?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "18d0d14afc1cdf37c8f3607181e3f72211da99e9", "filename": "lib/tzdata/zoneinfo/America/Ensenada", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FEnsenada", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FEnsenada", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FEnsenada?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "ba7b14760d47d1e10241e78e976f0093ada6551b", "filename": "lib/tzdata/zoneinfo/America/Hermosillo", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FHermosillo", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FHermosillo", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FHermosillo?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "5aa6039ea4cb36077f048782b54c6275c25e86b3", "filename": "lib/tzdata/zoneinfo/America/Mazatlan", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FMazatlan", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FMazatlan", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FMazatlan?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "e5c7d8cc2d2a986374f35561d8b629110b481a66", "filename": "lib/tzdata/zoneinfo/America/Merida", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FMerida", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FMerida", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FMerida?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "18112346129a885b5d5fa27109682b63784f72c0", "filename": "lib/tzdata/zoneinfo/America/Mexico_City", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FMexico_City", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FMexico_City", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FMexico_City?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "c1e05464513a451ab3cc49452e39da2b04e01de0", "filename": "lib/tzdata/zoneinfo/America/Monterrey", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FMonterrey", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FMonterrey", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FMonterrey?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "1dd08b1cafd4b36ce963bdc42a075665fa723b54", "filename": "lib/tzdata/zoneinfo/America/Ojinaga", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FOjinaga", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FOjinaga", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FOjinaga?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "18d0d14afc1cdf37c8f3607181e3f72211da99e9", "filename": "lib/tzdata/zoneinfo/America/Santa_Isabel", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FSanta_Isabel", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FSanta_Isabel", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FSanta_Isabel?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "18d0d14afc1cdf37c8f3607181e3f72211da99e9", "filename": "lib/tzdata/zoneinfo/America/Tijuana", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FTijuana", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FTijuana", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FAmerica%2FTijuana?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "6f5d3a15abbe48b8a4dc72aadc88c416160a56a6", "filename": "lib/tzdata/zoneinfo/Asia/Choibalsan", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAsia%2FChoibalsan", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAsia%2FChoibalsan", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FAsia%2FChoibalsan?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "22e705ca1ab1218e9f36b9f4f607258389853c8b", "filename": "lib/tzdata/zoneinfo/Asia/Dili", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAsia%2FDili", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAsia%2FDili", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FAsia%2FDili?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "cda1c1d225ae261ae433579e56d94a84ae2acd38", "filename": "lib/tzdata/zoneinfo/Atlantic/Azores", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAtlantic%2FAzores", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAtlantic%2FAzores", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FAtlantic%2FAzores?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "21e84571ee1694758dd244ed0702fbae962648e6", "filename": "lib/tzdata/zoneinfo/Atlantic/Madeira", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAtlantic%2FMadeira", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FAtlantic%2FMadeira", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FAtlantic%2FMadeira?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "31973271d2f87f7e9df4e8b0a1481f09a9e5407d", "filename": "lib/tzdata/zoneinfo/CET", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FCET", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FCET", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FCET?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "b016880653929aa40dd5ac0e82e4094a9d787cdf", "filename": "lib/tzdata/zoneinfo/CST6CDT", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FCST6CDT", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FCST6CDT", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FCST6CDT?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "231bf9c3b713e3676dbd8f3ced867973c601e104", "filename": "lib/tzdata/zoneinfo/EET", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FEET", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FEET", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FEET?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "9154643f4c9189998392afb8a93e2e2eb9eaecf5", "filename": "lib/tzdata/zoneinfo/EST", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FEST", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FEST", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FEST?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "2b6c2eea14df07392729ae9f5712a44ec4f02bae", "filename": "lib/tzdata/zoneinfo/EST5EDT", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FEST5EDT", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FEST5EDT", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FEST5EDT?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "7e9aae727b2b660e7f5e383121f445daf033a9c5", "filename": "lib/tzdata/zoneinfo/Europe/Lisbon", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FEurope%2FLisbon", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FEurope%2FLisbon", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FEurope%2FLisbon?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "40e3d492e6c22c30041c31f159d4fe0ee9451c03", "filename": "lib/tzdata/zoneinfo/HST", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FHST", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FHST", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FHST?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "31973271d2f87f7e9df4e8b0a1481f09a9e5407d", "filename": "lib/tzdata/zoneinfo/MET", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FMET", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FMET", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FMET?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "c2bd2f949b248b835c98216b4dc66f9f6eb0265e", "filename": "lib/tzdata/zoneinfo/MST", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FMST", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FMST", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FMST?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "09e54e5c7c5bb2384e37626d4b985cfad29ed29b", "filename": "lib/tzdata/zoneinfo/MST7MDT", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FMST7MDT", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FMST7MDT", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FMST7MDT?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "18d0d14afc1cdf37c8f3607181e3f72211da99e9", "filename": "lib/tzdata/zoneinfo/Mexico/BajaNorte", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FMexico%2FBajaNorte", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FMexico%2FBajaNorte", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FMexico%2FBajaNorte?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "5aa6039ea4cb36077f048782b54c6275c25e86b3", "filename": "lib/tzdata/zoneinfo/Mexico/BajaSur", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FMexico%2FBajaSur", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FMexico%2FBajaSur", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FMexico%2FBajaSur?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "18112346129a885b5d5fa27109682b63784f72c0", "filename": "lib/tzdata/zoneinfo/Mexico/General", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FMexico%2FGeneral", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FMexico%2FGeneral", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FMexico%2FGeneral?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "aaf07787ad92b65eadae63b64bba290f9f961507", "filename": "lib/tzdata/zoneinfo/PST8PDT", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FPST8PDT", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FPST8PDT", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FPST8PDT?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "7e9aae727b2b660e7f5e383121f445daf033a9c5", "filename": "lib/tzdata/zoneinfo/Portugal", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FPortugal", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FPortugal", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FPortugal?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "7e9aae727b2b660e7f5e383121f445daf033a9c5", "filename": "lib/tzdata/zoneinfo/WET", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FWET", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2FWET", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2FWET?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "6c715cb20b0177a89ef4a4863ef6278231212a7f", "filename": "lib/tzdata/zoneinfo/leapseconds", "status": "modified", "additions": 4, "deletions": 4, "changes": 8, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2Fleapseconds", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2Fleapseconds", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2Fleapseconds?ref=78864d7a97ab94d433f5eca205897f2b10be455b", "patch": "@@ -69,11 +69,11 @@ Leap\t2016\tDec\t31\t23:59:60\t+\tS\n # Any additional leap seconds will come after this.\n # This Expires line is commented out for now,\n # so that pre-2020a zic implementations do not reject this file.\n-#Expires 2024\tDec\t28\t00:00:00\n+#Expires 2025\tJun\t28\t00:00:00\n \n # POSIX timestamps for the data in this file:\n-#updated 1704708379 (2024-01-08 10:06:19 UTC)\n-#expires 1735344000 (2024-12-28 00:00:00 UTC)\n+#updated 1720104763 (2024-07-04 14:52:43 UTC)\n+#expires 1751068800 (2025-06-28 00:00:00 UTC)\n \n #\tUpdated through IERS Bulletin C (https://hpiers.obspm.fr/iers/bul/bulc/bulletinc.dat)\n-#\tFile expires on 28 December 2024\n+#\tFile expires on 28 June 2025"}, {"sha": "62e78bb826cdaa85d8683a1a78137c14bde66abb", "filename": "lib/tzdata/zoneinfo/tzdata.zi", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2Ftzdata.zi", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2Ftzdata.zi", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2Ftzdata.zi?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "bfc0b593304470c69ba2a56b69f593a76e47961a", "filename": "lib/tzdata/zoneinfo/zone.tab", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2Fzone.tab", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2Fzone.tab", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2Fzone.tab?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "7726f39a0922543715c7442a2dd0a128919ad43a", "filename": "lib/tzdata/zoneinfo/zone1970.tab", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2Fzone1970.tab", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2Fzone1970.tab", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2Fzone1970.tab?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "01f536b3ba3833dcb5c4ee25d6e18bd3772e860a", "filename": "lib/tzdata/zoneinfo/zonenow.tab", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2Fzonenow.tab", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzoneinfo%2Fzonenow.tab", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzoneinfo%2Fzonenow.tab?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "3c4a951d4990e82c545c0cc3a66fcb51d9d0b6e8", "filename": "lib/tzdata/zones", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzones", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Ftzdata%2Fzones", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Ftzdata%2Fzones?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "098f62762aede2b6eb11d2b7c11c62cf71af81f6", "filename": "lib/xmltodict.py", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fxmltodict.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fxmltodict.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fxmltodict.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "52c82a0e0444bdaf7667d1eb438d68ed1b602ee4", "filename": "lib/zipp.py", "status": "removed", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fzipp.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/940c2ae6cd064817512aa7646386a31fa9f465fa/lib%2Fzipp.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fzipp.py?ref=940c2ae6cd064817512aa7646386a31fa9f465fa"}, {"sha": "031d9d4f21aa9353b4adea8ce42038571bbd72db", "filename": "lib/zipp/__init__.py", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fzipp%2F__init__.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fzipp%2F__init__.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fzipp%2F__init__.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "f75ae2b0b439147179169dd85ab54021c2bbe084", "filename": "lib/zipp/_functools.py", "status": "added", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fzipp%2F_functools.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fzipp%2F_functools.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fzipp%2F_functools.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "5a97ee7cd8b98f3a5487c0a0b0a80ffde5ff4dfd", "filename": "lib/zipp/compat/overlay.py", "status": "added", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fzipp%2Fcompat%2Foverlay.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fzipp%2Fcompat%2Foverlay.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fzipp%2Fcompat%2Foverlay.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}, {"sha": "e1e7ec229062b8556cdd85f530d1ff301b2e6845", "filename": "lib/zipp/compat/py310.py", "status": "modified", "additions": 0, "deletions": 0, "changes": 0, "blob_url": "https://github.com/Tautulli/Tautulli/blob/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fzipp%2Fcompat%2Fpy310.py", "raw_url": "https://github.com/Tautulli/Tautulli/raw/78864d7a97ab94d433f5eca205897f2b10be455b/lib%2Fzipp%2Fcompat%2Fpy310.py", "contents_url": "https://api.github.com/repos/Tautulli/Tautulli/contents/lib%2Fzipp%2Fcompat%2Fpy310.py?ref=78864d7a97ab94d433f5eca205897f2b10be455b"}]}, "_cache_time": 1732922383, "_release_version": "v2.14.6"} |