We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Pege not found when configuring DSN keys for project name "onboarding"
### Environment

SaaS (https://sentry.io/)

### Version

_No response_

### Link

_No response_

### DSN

_No response_

### Steps to Reproduce

1. Create or rename a project to "onboarding"
2. Go to Client Keys -> Configure

### Expected Result

Open the Client key configuration page

### Actual Result

Page not found error, as in the screenshot below.

Current workaround: rename the project.

URL: https://dev-curumas.sentry.io/settings/projects/onboarding/a9217e8f8e01407a9b6df81101793546/

![Screenshot 2023-03-24 at 07 59 44](https://user-images.githubusercontent.com/64906376/227448378-f017bf85-b321-4803-9006-7bc9e13d7048.png)


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/sentry/api/utils.py`
Content:
```
1 from __future__ import annotations
2 
3 import datetime
4 import logging
5 import re
6 from datetime import timedelta
7 from typing import Any, List, Literal, Tuple, overload
8 from urllib.parse import urlparse
9 
10 from django.http import HttpResponseNotAllowed
11 from django.utils import timezone
12 from django.views.decorators.csrf import csrf_exempt
13 from rest_framework.request import Request
14 
15 from sentry import options
16 
17 # Unfortunately, this function is imported as an export of this module in several places, keep it.
18 from sentry.auth.access import get_cached_organization_member  # noqa
19 from sentry.auth.superuser import is_active_superuser
20 from sentry.models.organization import Organization
21 from sentry.search.utils import InvalidQuery, parse_datetime_string
22 from sentry.services.hybrid_cloud import extract_id_from
23 from sentry.services.hybrid_cloud.organization import (
24     RpcOrganization,
25     RpcOrganizationMember,
26     RpcUserOrganizationContext,
27     organization_service,
28 )
29 from sentry.utils.dates import parse_stats_period
30 
31 logger = logging.getLogger(__name__)
32 
33 MAX_STATS_PERIOD = timedelta(days=90)
34 
35 
36 class InvalidParams(Exception):
37     pass
38 
39 
40 def get_datetime_from_stats_period(
41     stats_period: str, now: datetime.datetime | None = None
42 ) -> datetime.datetime:
43     if now is None:
44         now = timezone.now()
45     parsed_stats_period = parse_stats_period(stats_period)
46     if parsed_stats_period is None:
47         raise InvalidParams(f"Invalid statsPeriod: {stats_period!r}")
48     try:
49         return now - parsed_stats_period
50     except OverflowError:
51         raise InvalidParams(f"Invalid statsPeriod: {stats_period!r}")
52 
53 
54 def default_start_end_dates(
55     now: datetime.datetime | None = None,
56     default_stats_period: datetime.timedelta = MAX_STATS_PERIOD,
57 ) -> tuple[datetime.datetime, datetime.datetime]:
58     if now is None:
59         now = timezone.now()
60     return now - default_stats_period, now
61 
62 
63 @overload
64 def get_date_range_from_params(
65     params: dict[str, Any],
66     optional: Literal[False] = ...,
67     default_stats_period: datetime.timedelta = ...,
68 ) -> tuple[datetime.datetime, datetime.datetime]:
69     ...
70 
71 
72 @overload
73 def get_date_range_from_params(
74     params: dict[str, Any],
75     optional: bool = ...,
76     default_stats_period: datetime.timedelta = ...,
77 ) -> tuple[None, None] | tuple[datetime.datetime, datetime.datetime]:
78     ...
79 
80 
81 def get_date_range_from_params(
82     params: dict[str, Any],
83     optional: bool = False,
84     default_stats_period: datetime.timedelta = MAX_STATS_PERIOD,
85 ) -> tuple[None, None] | tuple[datetime.datetime, datetime.datetime]:
86     """
87     A wrapper function for `get_date_range_from_stats_period` that allows us
88     to alias `statsPeriod` to ensure backward compatibility.
89 
90     If `timeframe` is passed then convert to a time delta and make sure it
91     fits within our min/max period length. Values are in the format
92     <number><period_type>, where period type is one of `s` (seconds),
93     `m` (minutes), `h` (hours) or `d` (days).
94 
95     Similarly, `timeframeStart` and `timeframeEnd` allow for selecting a
96     relative range, for example: 15 days ago through 8 days ago. This uses the same
97     format as `statsPeriod`.
98 
99     :param params:
100     If `start` end `end` are passed, validate them, convert to `datetime` and
101     returns them if valid.
102     :param optional: When True, if no params passed then return `(None, None)`.
103     :param default_stats_period: When set, this becomes the interval upon which default start
104     and end dates are defined
105     :return: A length 2 tuple containing start/end or raises an `InvalidParams`
106     exception
107     """
108     timeframe = params.get("timeframe")
109     timeframe_start = params.get("timeframeStart")
110     timeframe_end = params.get("timeframeEnd")
111 
112     if timeframe is not None:
113         params["statsPeriod"] = timeframe
114 
115     elif timeframe_start or timeframe_end:
116         if not all([timeframe_start, timeframe_end]):
117             raise InvalidParams("timeframeStart and timeframeEnd are both required")
118         else:
119             params["statsPeriodStart"] = timeframe_start
120             params["statsPeriodEnd"] = timeframe_end
121 
122     return get_date_range_from_stats_period(
123         params, optional=optional, default_stats_period=default_stats_period
124     )
125 
126 
127 @overload
128 def get_date_range_from_stats_period(
129     params: dict[str, Any],
130     optional: Literal[False] = ...,
131     default_stats_period: datetime.timedelta = ...,
132 ) -> tuple[datetime.datetime, datetime.datetime]:
133     ...
134 
135 
136 @overload
137 def get_date_range_from_stats_period(
138     params: dict[str, Any],
139     optional: bool = ...,
140     default_stats_period: datetime.timedelta = ...,
141 ) -> tuple[None, None] | tuple[datetime.datetime, datetime.datetime]:
142     ...
143 
144 
145 def get_date_range_from_stats_period(
146     params: dict[str, Any],
147     optional: bool = False,
148     default_stats_period: datetime.timedelta = MAX_STATS_PERIOD,
149 ) -> tuple[None, None] | tuple[datetime.datetime, datetime.datetime]:
150     """
151     Gets a date range from standard date range params we pass to the api.
152 
153     If `statsPeriod` is passed then convert to a time delta and make sure it
154     fits within our min/max period length. Values are in the format
155     <number><period_type>, where period type is one of `s` (seconds),
156     `m` (minutes), `h` (hours) or `d` (days).
157 
158     Similarly, `statsPeriodStart` and `statsPeriodEnd` allow for selecting a
159     relative range, for example: 15 days ago through 8 days ago. This uses the same
160     format as `statsPeriod`
161 
162     :param params:
163     If `start` end `end` are passed, validate them, convert to `datetime` and
164     returns them if valid.
165     :param optional: When True, if no params passed then return `(None, None)`.
166     :param default_stats_period: When set, this becomes the interval upon which default start
167     and end dates are defined
168     :return: A length 2 tuple containing start/end or raises an `InvalidParams`
169     exception
170     """
171     now = timezone.now()
172 
173     start, end = default_start_end_dates(now, default_stats_period)
174 
175     stats_period = params.get("statsPeriod")
176     stats_period_start = params.get("statsPeriodStart")
177     stats_period_end = params.get("statsPeriodEnd")
178 
179     if stats_period is not None:
180         start = get_datetime_from_stats_period(stats_period, now)
181 
182     elif stats_period_start or stats_period_end:
183         if not stats_period_start or not stats_period_end:
184             raise InvalidParams("statsPeriodStart and statsPeriodEnd are both required")
185         start = get_datetime_from_stats_period(stats_period_start, now)
186         end = get_datetime_from_stats_period(stats_period_end, now)
187 
188     elif params.get("start") or params.get("end"):
189         if not all([params.get("start"), params.get("end")]):
190             raise InvalidParams("start and end are both required")
191         try:
192             start = parse_datetime_string(params["start"])
193             end = parse_datetime_string(params["end"])
194         except InvalidQuery as e:
195             raise InvalidParams(str(e))
196     elif optional:
197         return None, None
198 
199     if start >= end:
200         raise InvalidParams("start must be before end")
201 
202     return start, end
203 
204 
205 # The wide typing allows us to move towards RpcUserOrganizationContext in the future to save RPC calls.
206 # If you can use the wider more correct type, please do.
207 def is_member_disabled_from_limit(
208     request: Request,
209     organization: RpcUserOrganizationContext | RpcOrganization | Organization | int,
210 ) -> bool:
211     user = request.user
212 
213     # never limit sentry apps
214     if getattr(user, "is_sentry_app", False):
215         return False
216 
217     # don't limit super users
218     if is_active_superuser(request):
219         return False
220 
221     # must be a simple user at this point
222 
223     member: RpcOrganizationMember | None
224     if isinstance(organization, RpcUserOrganizationContext):
225         member = organization.member
226     else:
227         member = organization_service.check_membership_by_id(
228             organization_id=extract_id_from(organization), user_id=user.id
229         )
230     if member is None:
231         return False
232     else:
233         return member.flags.member_limit__restricted
234 
235 
236 def generate_organization_hostname(org_slug: str) -> str:
237     url_prefix_hostname: str = urlparse(options.get("system.url-prefix")).netloc
238     org_base_hostname_template: str = options.get("system.organization-base-hostname")
239     if not org_base_hostname_template:
240         return url_prefix_hostname
241     has_org_slug_placeholder = "{slug}" in org_base_hostname_template
242     if not has_org_slug_placeholder:
243         return url_prefix_hostname
244     org_hostname = org_base_hostname_template.replace("{slug}", org_slug)
245     return org_hostname
246 
247 
248 def generate_organization_url(org_slug: str) -> str:
249     org_url_template: str = options.get("system.organization-url-template")
250     if not org_url_template:
251         return options.get("system.url-prefix")  # type: ignore[no-any-return]
252     return org_url_template.replace("{hostname}", generate_organization_hostname(org_slug))
253 
254 
255 def generate_region_url(region_name: str | None = None) -> str:
256     region_url_template: str | None = options.get("system.region-api-url-template")
257     if region_name is None:
258         region_name = options.get("system.region") or None
259     if not region_url_template or not region_name:
260         return options.get("system.url-prefix")  # type: ignore[no-any-return]
261     return region_url_template.replace("{region}", region_name)
262 
263 
264 _path_patterns: List[Tuple[re.Pattern[str], str]] = [
265     # /organizations/slug/section, but not /organizations/new
266     (re.compile(r"\/?organizations\/(?!new)[^\/]+\/(.*)"), r"/\1"),
267     # For /settings/:orgId/ -> /settings/organization/
268     (
269         re.compile(r"\/settings\/(?!account)(?!projects)(?!teams)[^\/]+\/?$"),
270         "/settings/organization/",
271     ),
272     # Move /settings/:orgId/:section -> /settings/:section
273     # but not /settings/organization or /settings/projects which is a new URL
274     (re.compile(r"\/?settings\/(?!account)(?!projects)(?!teams)[^\/]+\/(.*)"), r"/settings/\1"),
275     (re.compile(r"\/?join-request\/[^\/]+\/?.*"), r"/join-request/"),
276     (re.compile(r"\/?onboarding\/[^\/]+\/(.*)"), r"/onboarding/\1"),
277     (re.compile(r"\/?[^\/]+\/([^\/]+)\/getting-started\/(.*)"), r"/getting-started/\1/\2"),
278 ]
279 
280 
281 def customer_domain_path(path: str) -> str:
282     """
283     Server side companion to path normalizations found in withDomainRequired
284     """
285     for pattern, replacement in _path_patterns:
286         updated = pattern.sub(replacement, path)
287         if updated != path:
288             return updated
289     return path
290 
291 
292 def method_dispatch(**dispatch_mapping):  # type: ignore[no-untyped-def]
293     """
294     Dispatches a incoming request to a different handler based on the HTTP method
295 
296     >>> url('^foo$', method_dispatch(POST = post_handler, GET = get_handler)))
297     """
298 
299     def invalid_method(request, *args, **kwargs):  # type: ignore[no-untyped-def]
300         return HttpResponseNotAllowed(dispatch_mapping.keys())
301 
302     def dispatcher(request, *args, **kwargs):  # type: ignore[no-untyped-def]
303         handler = dispatch_mapping.get(request.method, invalid_method)
304         return handler(request, *args, **kwargs)
305 
306     if dispatch_mapping.get("csrf_exempt"):
307         return csrf_exempt(dispatcher)
308 
309     return dispatcher
310
```
--- END FILES ---

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

Here is an example:

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

```

diff --git a/src/sentry/api/utils.py b/src/sentry/api/utils.py
--- a/src/sentry/api/utils.py
+++ b/src/sentry/api/utils.py
@@ -271,10 +271,13 @@
     ),
     # Move /settings/:orgId/:section -> /settings/:section
     # but not /settings/organization or /settings/projects which is a new URL
-    (re.compile(r"\/?settings\/(?!account)(?!projects)(?!teams)[^\/]+\/(.*)"), r"/settings/\1"),
-    (re.compile(r"\/?join-request\/[^\/]+\/?.*"), r"/join-request/"),
-    (re.compile(r"\/?onboarding\/[^\/]+\/(.*)"), r"/onboarding/\1"),
-    (re.compile(r"\/?[^\/]+\/([^\/]+)\/getting-started\/(.*)"), r"/getting-started/\1/\2"),
+    (re.compile(r"^\/?settings\/(?!account)(?!projects)(?!teams)[^\/]+\/(.*)"), r"/settings/\1"),
+    (re.compile(r"^\/?join-request\/[^\/]+\/?.*"), r"/join-request/"),
+    (re.compile(r"^\/?onboarding\/[^\/]+\/(.*)"), r"/onboarding/\1"),
+    (
+        re.compile(r"\/?(?:[^\/]+(?<!settings))\/([^\/]+)\/getting-started\/(.*)"),
+        r"/getting-started/\1/\2",
+    ),
 ]

{"golden_diff": "diff --git a/src/sentry/api/utils.py b/src/sentry/api/utils.py\n--- a/src/sentry/api/utils.py\n+++ b/src/sentry/api/utils.py\n@@ -271,10 +271,13 @@\n     ),\n     # Move /settings/:orgId/:section -> /settings/:section\n     # but not /settings/organization or /settings/projects which is a new URL\n-    (re.compile(r\"\\/?settings\\/(?!account)(?!projects)(?!teams)[^\\/]+\\/(.*)\"), r\"/settings/\\1\"),\n-    (re.compile(r\"\\/?join-request\\/[^\\/]+\\/?.*\"), r\"/join-request/\"),\n-    (re.compile(r\"\\/?onboarding\\/[^\\/]+\\/(.*)\"), r\"/onboarding/\\1\"),\n-    (re.compile(r\"\\/?[^\\/]+\\/([^\\/]+)\\/getting-started\\/(.*)\"), r\"/getting-started/\\1/\\2\"),\n+    (re.compile(r\"^\\/?settings\\/(?!account)(?!projects)(?!teams)[^\\/]+\\/(.*)\"), r\"/settings/\\1\"),\n+    (re.compile(r\"^\\/?join-request\\/[^\\/]+\\/?.*\"), r\"/join-request/\"),\n+    (re.compile(r\"^\\/?onboarding\\/[^\\/]+\\/(.*)\"), r\"/onboarding/\\1\"),\n+    (\n+        re.compile(r\"\\/?(?:[^\\/]+(?<!settings))\\/([^\\/]+)\\/getting-started\\/(.*)\"),\n+        r\"/getting-started/\\1/\\2\",\n+    ),\n ]\n", "issue": "Pege not found when configuring DSN keys for project name \"onboarding\"\n### Environment\n\nSaaS (https://sentry.io/)\n\n### Version\n\n_No response_\n\n### Link\n\n_No response_\n\n### DSN\n\n_No response_\n\n### Steps to Reproduce\n\n1. Create or rename a project to \"onboarding\"\r\n2. Go to Client Keys -> Configure\n\n### Expected Result\n\nOpen the Client key configuration page\n\n### Actual Result\n\nPage not found error, as in the screenshot below.\r\n\r\nCurrent workaround: rename the project.\r\n\r\nURL: https://dev-curumas.sentry.io/settings/projects/onboarding/a9217e8f8e01407a9b6df81101793546/\r\n\r\n![Screenshot 2023-03-24 at 07 59 44](https://user-images.githubusercontent.com/64906376/227448378-f017bf85-b321-4803-9006-7bc9e13d7048.png)\r\n\n", "before_files": [{"content": "from __future__ import annotations\n\nimport datetime\nimport logging\nimport re\nfrom datetime import timedelta\nfrom typing import Any, List, Literal, Tuple, overload\nfrom urllib.parse import urlparse\n\nfrom django.http import HttpResponseNotAllowed\nfrom django.utils import timezone\nfrom django.views.decorators.csrf import csrf_exempt\nfrom rest_framework.request import Request\n\nfrom sentry import options\n\n# Unfortunately, this function is imported as an export of this module in several places, keep it.\nfrom sentry.auth.access import get_cached_organization_member  # noqa\nfrom sentry.auth.superuser import is_active_superuser\nfrom sentry.models.organization import Organization\nfrom sentry.search.utils import InvalidQuery, parse_datetime_string\nfrom sentry.services.hybrid_cloud import extract_id_from\nfrom sentry.services.hybrid_cloud.organization import (\n    RpcOrganization,\n    RpcOrganizationMember,\n    RpcUserOrganizationContext,\n    organization_service,\n)\nfrom sentry.utils.dates import parse_stats_period\n\nlogger = logging.getLogger(__name__)\n\nMAX_STATS_PERIOD = timedelta(days=90)\n\n\nclass InvalidParams(Exception):\n    pass\n\n\ndef get_datetime_from_stats_period(\n    stats_period: str, now: datetime.datetime | None = None\n) -> datetime.datetime:\n    if now is None:\n        now = timezone.now()\n    parsed_stats_period = parse_stats_period(stats_period)\n    if parsed_stats_period is None:\n        raise InvalidParams(f\"Invalid statsPeriod: {stats_period!r}\")\n    try:\n        return now - parsed_stats_period\n    except OverflowError:\n        raise InvalidParams(f\"Invalid statsPeriod: {stats_period!r}\")\n\n\ndef default_start_end_dates(\n    now: datetime.datetime | None = None,\n    default_stats_period: datetime.timedelta = MAX_STATS_PERIOD,\n) -> tuple[datetime.datetime, datetime.datetime]:\n    if now is None:\n        now = timezone.now()\n    return now - default_stats_period, now\n\n\n@overload\ndef get_date_range_from_params(\n    params: dict[str, Any],\n    optional: Literal[False] = ...,\n    default_stats_period: datetime.timedelta = ...,\n) -> tuple[datetime.datetime, datetime.datetime]:\n    ...\n\n\n@overload\ndef get_date_range_from_params(\n    params: dict[str, Any],\n    optional: bool = ...,\n    default_stats_period: datetime.timedelta = ...,\n) -> tuple[None, None] | tuple[datetime.datetime, datetime.datetime]:\n    ...\n\n\ndef get_date_range_from_params(\n    params: dict[str, Any],\n    optional: bool = False,\n    default_stats_period: datetime.timedelta = MAX_STATS_PERIOD,\n) -> tuple[None, None] | tuple[datetime.datetime, datetime.datetime]:\n    \"\"\"\n    A wrapper function for `get_date_range_from_stats_period` that allows us\n    to alias `statsPeriod` to ensure backward compatibility.\n\n    If `timeframe` is passed then convert to a time delta and make sure it\n    fits within our min/max period length. Values are in the format\n    <number><period_type>, where period type is one of `s` (seconds),\n    `m` (minutes), `h` (hours) or `d` (days).\n\n    Similarly, `timeframeStart` and `timeframeEnd` allow for selecting a\n    relative range, for example: 15 days ago through 8 days ago. This uses the same\n    format as `statsPeriod`.\n\n    :param params:\n    If `start` end `end` are passed, validate them, convert to `datetime` and\n    returns them if valid.\n    :param optional: When True, if no params passed then return `(None, None)`.\n    :param default_stats_period: When set, this becomes the interval upon which default start\n    and end dates are defined\n    :return: A length 2 tuple containing start/end or raises an `InvalidParams`\n    exception\n    \"\"\"\n    timeframe = params.get(\"timeframe\")\n    timeframe_start = params.get(\"timeframeStart\")\n    timeframe_end = params.get(\"timeframeEnd\")\n\n    if timeframe is not None:\n        params[\"statsPeriod\"] = timeframe\n\n    elif timeframe_start or timeframe_end:\n        if not all([timeframe_start, timeframe_end]):\n            raise InvalidParams(\"timeframeStart and timeframeEnd are both required\")\n        else:\n            params[\"statsPeriodStart\"] = timeframe_start\n            params[\"statsPeriodEnd\"] = timeframe_end\n\n    return get_date_range_from_stats_period(\n        params, optional=optional, default_stats_period=default_stats_period\n    )\n\n\n@overload\ndef get_date_range_from_stats_period(\n    params: dict[str, Any],\n    optional: Literal[False] = ...,\n    default_stats_period: datetime.timedelta = ...,\n) -> tuple[datetime.datetime, datetime.datetime]:\n    ...\n\n\n@overload\ndef get_date_range_from_stats_period(\n    params: dict[str, Any],\n    optional: bool = ...,\n    default_stats_period: datetime.timedelta = ...,\n) -> tuple[None, None] | tuple[datetime.datetime, datetime.datetime]:\n    ...\n\n\ndef get_date_range_from_stats_period(\n    params: dict[str, Any],\n    optional: bool = False,\n    default_stats_period: datetime.timedelta = MAX_STATS_PERIOD,\n) -> tuple[None, None] | tuple[datetime.datetime, datetime.datetime]:\n    \"\"\"\n    Gets a date range from standard date range params we pass to the api.\n\n    If `statsPeriod` is passed then convert to a time delta and make sure it\n    fits within our min/max period length. Values are in the format\n    <number><period_type>, where period type is one of `s` (seconds),\n    `m` (minutes), `h` (hours) or `d` (days).\n\n    Similarly, `statsPeriodStart` and `statsPeriodEnd` allow for selecting a\n    relative range, for example: 15 days ago through 8 days ago. This uses the same\n    format as `statsPeriod`\n\n    :param params:\n    If `start` end `end` are passed, validate them, convert to `datetime` and\n    returns them if valid.\n    :param optional: When True, if no params passed then return `(None, None)`.\n    :param default_stats_period: When set, this becomes the interval upon which default start\n    and end dates are defined\n    :return: A length 2 tuple containing start/end or raises an `InvalidParams`\n    exception\n    \"\"\"\n    now = timezone.now()\n\n    start, end = default_start_end_dates(now, default_stats_period)\n\n    stats_period = params.get(\"statsPeriod\")\n    stats_period_start = params.get(\"statsPeriodStart\")\n    stats_period_end = params.get(\"statsPeriodEnd\")\n\n    if stats_period is not None:\n        start = get_datetime_from_stats_period(stats_period, now)\n\n    elif stats_period_start or stats_period_end:\n        if not stats_period_start or not stats_period_end:\n            raise InvalidParams(\"statsPeriodStart and statsPeriodEnd are both required\")\n        start = get_datetime_from_stats_period(stats_period_start, now)\n        end = get_datetime_from_stats_period(stats_period_end, now)\n\n    elif params.get(\"start\") or params.get(\"end\"):\n        if not all([params.get(\"start\"), params.get(\"end\")]):\n            raise InvalidParams(\"start and end are both required\")\n        try:\n            start = parse_datetime_string(params[\"start\"])\n            end = parse_datetime_string(params[\"end\"])\n        except InvalidQuery as e:\n            raise InvalidParams(str(e))\n    elif optional:\n        return None, None\n\n    if start >= end:\n        raise InvalidParams(\"start must be before end\")\n\n    return start, end\n\n\n# The wide typing allows us to move towards RpcUserOrganizationContext in the future to save RPC calls.\n# If you can use the wider more correct type, please do.\ndef is_member_disabled_from_limit(\n    request: Request,\n    organization: RpcUserOrganizationContext | RpcOrganization | Organization | int,\n) -> bool:\n    user = request.user\n\n    # never limit sentry apps\n    if getattr(user, \"is_sentry_app\", False):\n        return False\n\n    # don't limit super users\n    if is_active_superuser(request):\n        return False\n\n    # must be a simple user at this point\n\n    member: RpcOrganizationMember | None\n    if isinstance(organization, RpcUserOrganizationContext):\n        member = organization.member\n    else:\n        member = organization_service.check_membership_by_id(\n            organization_id=extract_id_from(organization), user_id=user.id\n        )\n    if member is None:\n        return False\n    else:\n        return member.flags.member_limit__restricted\n\n\ndef generate_organization_hostname(org_slug: str) -> str:\n    url_prefix_hostname: str = urlparse(options.get(\"system.url-prefix\")).netloc\n    org_base_hostname_template: str = options.get(\"system.organization-base-hostname\")\n    if not org_base_hostname_template:\n        return url_prefix_hostname\n    has_org_slug_placeholder = \"{slug}\" in org_base_hostname_template\n    if not has_org_slug_placeholder:\n        return url_prefix_hostname\n    org_hostname = org_base_hostname_template.replace(\"{slug}\", org_slug)\n    return org_hostname\n\n\ndef generate_organization_url(org_slug: str) -> str:\n    org_url_template: str = options.get(\"system.organization-url-template\")\n    if not org_url_template:\n        return options.get(\"system.url-prefix\")  # type: ignore[no-any-return]\n    return org_url_template.replace(\"{hostname}\", generate_organization_hostname(org_slug))\n\n\ndef generate_region_url(region_name: str | None = None) -> str:\n    region_url_template: str | None = options.get(\"system.region-api-url-template\")\n    if region_name is None:\n        region_name = options.get(\"system.region\") or None\n    if not region_url_template or not region_name:\n        return options.get(\"system.url-prefix\")  # type: ignore[no-any-return]\n    return region_url_template.replace(\"{region}\", region_name)\n\n\n_path_patterns: List[Tuple[re.Pattern[str], str]] = [\n    # /organizations/slug/section, but not /organizations/new\n    (re.compile(r\"\\/?organizations\\/(?!new)[^\\/]+\\/(.*)\"), r\"/\\1\"),\n    # For /settings/:orgId/ -> /settings/organization/\n    (\n        re.compile(r\"\\/settings\\/(?!account)(?!projects)(?!teams)[^\\/]+\\/?$\"),\n        \"/settings/organization/\",\n    ),\n    # Move /settings/:orgId/:section -> /settings/:section\n    # but not /settings/organization or /settings/projects which is a new URL\n    (re.compile(r\"\\/?settings\\/(?!account)(?!projects)(?!teams)[^\\/]+\\/(.*)\"), r\"/settings/\\1\"),\n    (re.compile(r\"\\/?join-request\\/[^\\/]+\\/?.*\"), r\"/join-request/\"),\n    (re.compile(r\"\\/?onboarding\\/[^\\/]+\\/(.*)\"), r\"/onboarding/\\1\"),\n    (re.compile(r\"\\/?[^\\/]+\\/([^\\/]+)\\/getting-started\\/(.*)\"), r\"/getting-started/\\1/\\2\"),\n]\n\n\ndef customer_domain_path(path: str) -> str:\n    \"\"\"\n    Server side companion to path normalizations found in withDomainRequired\n    \"\"\"\n    for pattern, replacement in _path_patterns:\n        updated = pattern.sub(replacement, path)\n        if updated != path:\n            return updated\n    return path\n\n\ndef method_dispatch(**dispatch_mapping):  # type: ignore[no-untyped-def]\n    \"\"\"\n    Dispatches a incoming request to a different handler based on the HTTP method\n\n    >>> url('^foo$', method_dispatch(POST = post_handler, GET = get_handler)))\n    \"\"\"\n\n    def invalid_method(request, *args, **kwargs):  # type: ignore[no-untyped-def]\n        return HttpResponseNotAllowed(dispatch_mapping.keys())\n\n    def dispatcher(request, *args, **kwargs):  # type: ignore[no-untyped-def]\n        handler = dispatch_mapping.get(request.method, invalid_method)\n        return handler(request, *args, **kwargs)\n\n    if dispatch_mapping.get(\"csrf_exempt\"):\n        return csrf_exempt(dispatcher)\n\n    return dispatcher\n", "path": "src/sentry/api/utils.py"}], "after_files": [{"content": "from __future__ import annotations\n\nimport datetime\nimport logging\nimport re\nfrom datetime import timedelta\nfrom typing import Any, List, Literal, Tuple, overload\nfrom urllib.parse import urlparse\n\nfrom django.http import HttpResponseNotAllowed\nfrom django.utils import timezone\nfrom django.views.decorators.csrf import csrf_exempt\nfrom rest_framework.request import Request\n\nfrom sentry import options\n\n# Unfortunately, this function is imported as an export of this module in several places, keep it.\nfrom sentry.auth.access import get_cached_organization_member  # noqa\nfrom sentry.auth.superuser import is_active_superuser\nfrom sentry.models.organization import Organization\nfrom sentry.search.utils import InvalidQuery, parse_datetime_string\nfrom sentry.services.hybrid_cloud import extract_id_from\nfrom sentry.services.hybrid_cloud.organization import (\n    RpcOrganization,\n    RpcOrganizationMember,\n    RpcUserOrganizationContext,\n    organization_service,\n)\nfrom sentry.utils.dates import parse_stats_period\n\nlogger = logging.getLogger(__name__)\n\nMAX_STATS_PERIOD = timedelta(days=90)\n\n\nclass InvalidParams(Exception):\n    pass\n\n\ndef get_datetime_from_stats_period(\n    stats_period: str, now: datetime.datetime | None = None\n) -> datetime.datetime:\n    if now is None:\n        now = timezone.now()\n    parsed_stats_period = parse_stats_period(stats_period)\n    if parsed_stats_period is None:\n        raise InvalidParams(f\"Invalid statsPeriod: {stats_period!r}\")\n    try:\n        return now - parsed_stats_period\n    except OverflowError:\n        raise InvalidParams(f\"Invalid statsPeriod: {stats_period!r}\")\n\n\ndef default_start_end_dates(\n    now: datetime.datetime | None = None,\n    default_stats_period: datetime.timedelta = MAX_STATS_PERIOD,\n) -> tuple[datetime.datetime, datetime.datetime]:\n    if now is None:\n        now = timezone.now()\n    return now - default_stats_period, now\n\n\n@overload\ndef get_date_range_from_params(\n    params: dict[str, Any],\n    optional: Literal[False] = ...,\n    default_stats_period: datetime.timedelta = ...,\n) -> tuple[datetime.datetime, datetime.datetime]:\n    ...\n\n\n@overload\ndef get_date_range_from_params(\n    params: dict[str, Any],\n    optional: bool = ...,\n    default_stats_period: datetime.timedelta = ...,\n) -> tuple[None, None] | tuple[datetime.datetime, datetime.datetime]:\n    ...\n\n\ndef get_date_range_from_params(\n    params: dict[str, Any],\n    optional: bool = False,\n    default_stats_period: datetime.timedelta = MAX_STATS_PERIOD,\n) -> tuple[None, None] | tuple[datetime.datetime, datetime.datetime]:\n    \"\"\"\n    A wrapper function for `get_date_range_from_stats_period` that allows us\n    to alias `statsPeriod` to ensure backward compatibility.\n\n    If `timeframe` is passed then convert to a time delta and make sure it\n    fits within our min/max period length. Values are in the format\n    <number><period_type>, where period type is one of `s` (seconds),\n    `m` (minutes), `h` (hours) or `d` (days).\n\n    Similarly, `timeframeStart` and `timeframeEnd` allow for selecting a\n    relative range, for example: 15 days ago through 8 days ago. This uses the same\n    format as `statsPeriod`.\n\n    :param params:\n    If `start` end `end` are passed, validate them, convert to `datetime` and\n    returns them if valid.\n    :param optional: When True, if no params passed then return `(None, None)`.\n    :param default_stats_period: When set, this becomes the interval upon which default start\n    and end dates are defined\n    :return: A length 2 tuple containing start/end or raises an `InvalidParams`\n    exception\n    \"\"\"\n    timeframe = params.get(\"timeframe\")\n    timeframe_start = params.get(\"timeframeStart\")\n    timeframe_end = params.get(\"timeframeEnd\")\n\n    if timeframe is not None:\n        params[\"statsPeriod\"] = timeframe\n\n    elif timeframe_start or timeframe_end:\n        if not all([timeframe_start, timeframe_end]):\n            raise InvalidParams(\"timeframeStart and timeframeEnd are both required\")\n        else:\n            params[\"statsPeriodStart\"] = timeframe_start\n            params[\"statsPeriodEnd\"] = timeframe_end\n\n    return get_date_range_from_stats_period(\n        params, optional=optional, default_stats_period=default_stats_period\n    )\n\n\n@overload\ndef get_date_range_from_stats_period(\n    params: dict[str, Any],\n    optional: Literal[False] = ...,\n    default_stats_period: datetime.timedelta = ...,\n) -> tuple[datetime.datetime, datetime.datetime]:\n    ...\n\n\n@overload\ndef get_date_range_from_stats_period(\n    params: dict[str, Any],\n    optional: bool = ...,\n    default_stats_period: datetime.timedelta = ...,\n) -> tuple[None, None] | tuple[datetime.datetime, datetime.datetime]:\n    ...\n\n\ndef get_date_range_from_stats_period(\n    params: dict[str, Any],\n    optional: bool = False,\n    default_stats_period: datetime.timedelta = MAX_STATS_PERIOD,\n) -> tuple[None, None] | tuple[datetime.datetime, datetime.datetime]:\n    \"\"\"\n    Gets a date range from standard date range params we pass to the api.\n\n    If `statsPeriod` is passed then convert to a time delta and make sure it\n    fits within our min/max period length. Values are in the format\n    <number><period_type>, where period type is one of `s` (seconds),\n    `m` (minutes), `h` (hours) or `d` (days).\n\n    Similarly, `statsPeriodStart` and `statsPeriodEnd` allow for selecting a\n    relative range, for example: 15 days ago through 8 days ago. This uses the same\n    format as `statsPeriod`\n\n    :param params:\n    If `start` end `end` are passed, validate them, convert to `datetime` and\n    returns them if valid.\n    :param optional: When True, if no params passed then return `(None, None)`.\n    :param default_stats_period: When set, this becomes the interval upon which default start\n    and end dates are defined\n    :return: A length 2 tuple containing start/end or raises an `InvalidParams`\n    exception\n    \"\"\"\n    now = timezone.now()\n\n    start, end = default_start_end_dates(now, default_stats_period)\n\n    stats_period = params.get(\"statsPeriod\")\n    stats_period_start = params.get(\"statsPeriodStart\")\n    stats_period_end = params.get(\"statsPeriodEnd\")\n\n    if stats_period is not None:\n        start = get_datetime_from_stats_period(stats_period, now)\n\n    elif stats_period_start or stats_period_end:\n        if not stats_period_start or not stats_period_end:\n            raise InvalidParams(\"statsPeriodStart and statsPeriodEnd are both required\")\n        start = get_datetime_from_stats_period(stats_period_start, now)\n        end = get_datetime_from_stats_period(stats_period_end, now)\n\n    elif params.get(\"start\") or params.get(\"end\"):\n        if not all([params.get(\"start\"), params.get(\"end\")]):\n            raise InvalidParams(\"start and end are both required\")\n        try:\n            start = parse_datetime_string(params[\"start\"])\n            end = parse_datetime_string(params[\"end\"])\n        except InvalidQuery as e:\n            raise InvalidParams(str(e))\n    elif optional:\n        return None, None\n\n    if start >= end:\n        raise InvalidParams(\"start must be before end\")\n\n    return start, end\n\n\n# The wide typing allows us to move towards RpcUserOrganizationContext in the future to save RPC calls.\n# If you can use the wider more correct type, please do.\ndef is_member_disabled_from_limit(\n    request: Request,\n    organization: RpcUserOrganizationContext | RpcOrganization | Organization | int,\n) -> bool:\n    user = request.user\n\n    # never limit sentry apps\n    if getattr(user, \"is_sentry_app\", False):\n        return False\n\n    # don't limit super users\n    if is_active_superuser(request):\n        return False\n\n    # must be a simple user at this point\n\n    member: RpcOrganizationMember | None\n    if isinstance(organization, RpcUserOrganizationContext):\n        member = organization.member\n    else:\n        member = organization_service.check_membership_by_id(\n            organization_id=extract_id_from(organization), user_id=user.id\n        )\n    if member is None:\n        return False\n    else:\n        return member.flags.member_limit__restricted\n\n\ndef generate_organization_hostname(org_slug: str) -> str:\n    url_prefix_hostname: str = urlparse(options.get(\"system.url-prefix\")).netloc\n    org_base_hostname_template: str = options.get(\"system.organization-base-hostname\")\n    if not org_base_hostname_template:\n        return url_prefix_hostname\n    has_org_slug_placeholder = \"{slug}\" in org_base_hostname_template\n    if not has_org_slug_placeholder:\n        return url_prefix_hostname\n    org_hostname = org_base_hostname_template.replace(\"{slug}\", org_slug)\n    return org_hostname\n\n\ndef generate_organization_url(org_slug: str) -> str:\n    org_url_template: str = options.get(\"system.organization-url-template\")\n    if not org_url_template:\n        return options.get(\"system.url-prefix\")  # type: ignore[no-any-return]\n    return org_url_template.replace(\"{hostname}\", generate_organization_hostname(org_slug))\n\n\ndef generate_region_url(region_name: str | None = None) -> str:\n    region_url_template: str | None = options.get(\"system.region-api-url-template\")\n    if region_name is None:\n        region_name = options.get(\"system.region\") or None\n    if not region_url_template or not region_name:\n        return options.get(\"system.url-prefix\")  # type: ignore[no-any-return]\n    return region_url_template.replace(\"{region}\", region_name)\n\n\n_path_patterns: List[Tuple[re.Pattern[str], str]] = [\n    # /organizations/slug/section, but not /organizations/new\n    (re.compile(r\"\\/?organizations\\/(?!new)[^\\/]+\\/(.*)\"), r\"/\\1\"),\n    # For /settings/:orgId/ -> /settings/organization/\n    (\n        re.compile(r\"\\/settings\\/(?!account)(?!projects)(?!teams)[^\\/]+\\/?$\"),\n        \"/settings/organization/\",\n    ),\n    # Move /settings/:orgId/:section -> /settings/:section\n    # but not /settings/organization or /settings/projects which is a new URL\n    (re.compile(r\"^\\/?settings\\/(?!account)(?!projects)(?!teams)[^\\/]+\\/(.*)\"), r\"/settings/\\1\"),\n    (re.compile(r\"^\\/?join-request\\/[^\\/]+\\/?.*\"), r\"/join-request/\"),\n    (re.compile(r\"^\\/?onboarding\\/[^\\/]+\\/(.*)\"), r\"/onboarding/\\1\"),\n    (\n        re.compile(r\"\\/?(?:[^\\/]+(?<!settings))\\/([^\\/]+)\\/getting-started\\/(.*)\"),\n        r\"/getting-started/\\1/\\2\",\n    ),\n]\n\n\ndef customer_domain_path(path: str) -> str:\n    \"\"\"\n    Server side companion to path normalizations found in withDomainRequired\n    \"\"\"\n    for pattern, replacement in _path_patterns:\n        updated = pattern.sub(replacement, path)\n        if updated != path:\n            return updated\n    return path\n\n\ndef method_dispatch(**dispatch_mapping):  # type: ignore[no-untyped-def]\n    \"\"\"\n    Dispatches a incoming request to a different handler based on the HTTP method\n\n    >>> url('^foo$', method_dispatch(POST = post_handler, GET = get_handler)))\n    \"\"\"\n\n    def invalid_method(request, *args, **kwargs):  # type: ignore[no-untyped-def]\n        return HttpResponseNotAllowed(dispatch_mapping.keys())\n\n    def dispatcher(request, *args, **kwargs):  # type: ignore[no-untyped-def]\n        handler = dispatch_mapping.get(request.method, invalid_method)\n        return handler(request, *args, **kwargs)\n\n    if dispatch_mapping.get(\"csrf_exempt\"):\n        return csrf_exempt(dispatcher)\n\n    return dispatcher\n", "path": "src/sentry/api/utils.py"}]}