mirror of https://github.com/zulip/zulip.git
requirements: Upgrade Python requirements.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
parent
e56863fa85
commit
93198a19ed
|
@ -60,9 +60,7 @@ def generate_time_series_data(
|
|||
f"Must be generating at least 2 data points. Currently generating {length}"
|
||||
)
|
||||
growth_base = growth ** (1.0 / (length - 1))
|
||||
values_no_noise = [
|
||||
seasonality[i % len(seasonality)] * (growth_base**i) for i in range(length)
|
||||
]
|
||||
values_no_noise = [seasonality[i % len(seasonality)] * (growth_base**i) for i in range(length)]
|
||||
|
||||
noise_scalars = [rng.gauss(0, 1)]
|
||||
for i in range(1, length):
|
||||
|
|
|
@ -266,18 +266,18 @@ def support(
|
|||
context["error_message"] = error.message
|
||||
else:
|
||||
do_change_realm_subdomain(realm, new_subdomain, acting_user=acting_user)
|
||||
request.session[
|
||||
"success_message"
|
||||
] = f"Subdomain changed from {old_subdomain} to {new_subdomain}"
|
||||
request.session["success_message"] = (
|
||||
f"Subdomain changed from {old_subdomain} to {new_subdomain}"
|
||||
)
|
||||
return HttpResponseRedirect(
|
||||
reverse("support") + "?" + urlencode({"q": new_subdomain})
|
||||
)
|
||||
elif status is not None:
|
||||
if status == "active":
|
||||
do_send_realm_reactivation_email(realm, acting_user=acting_user)
|
||||
context[
|
||||
"success_message"
|
||||
] = f"Realm reactivation email sent to admins of {realm.string_id}."
|
||||
context["success_message"] = (
|
||||
f"Realm reactivation email sent to admins of {realm.string_id}."
|
||||
)
|
||||
elif status == "deactivated":
|
||||
do_deactivate_realm(realm, acting_user=acting_user)
|
||||
context["success_message"] = f"{realm.string_id} deactivated."
|
||||
|
@ -569,9 +569,9 @@ def remote_servers_support(
|
|||
remote_server
|
||||
)
|
||||
except MissingDataError:
|
||||
remote_server_to_max_monthly_messages[
|
||||
remote_server.id
|
||||
] = "Recent analytics data missing"
|
||||
remote_server_to_max_monthly_messages[remote_server.id] = (
|
||||
"Recent analytics data missing"
|
||||
)
|
||||
|
||||
context["remote_servers"] = remote_servers
|
||||
context["remote_servers_support_data"] = server_support_data
|
||||
|
|
|
@ -449,10 +449,10 @@ def catch_stripe_errors(func: Callable[ParamT, ReturnT]) -> Callable[ParamT, Ret
|
|||
# See https://stripe.com/docs/api/python#error_handling, though
|
||||
# https://stripe.com/docs/api/ruby#error_handling suggests there are additional fields, and
|
||||
# https://stripe.com/docs/error-codes gives a more detailed set of error codes
|
||||
except stripe.error.StripeError as e:
|
||||
except stripe.StripeError as e:
|
||||
assert isinstance(e.json_body, dict)
|
||||
err = e.json_body.get("error", {})
|
||||
if isinstance(e, stripe.error.CardError):
|
||||
if isinstance(e, stripe.CardError):
|
||||
billing_logger.info(
|
||||
"Stripe card error: %s %s %s %s",
|
||||
e.http_status,
|
||||
|
@ -469,9 +469,7 @@ def catch_stripe_errors(func: Callable[ParamT, ReturnT]) -> Callable[ParamT, Ret
|
|||
err.get("code"),
|
||||
err.get("param"),
|
||||
)
|
||||
if isinstance(
|
||||
e, (stripe.error.RateLimitError, stripe.error.APIConnectionError)
|
||||
): # nocoverage TODO
|
||||
if isinstance(e, (stripe.RateLimitError, stripe.APIConnectionError)): # nocoverage TODO
|
||||
raise StripeConnectionError(
|
||||
"stripe connection error",
|
||||
_("Something went wrong. Please wait a few seconds and try again."),
|
||||
|
@ -979,7 +977,7 @@ class BillingSession(ABC):
|
|||
off_session=True,
|
||||
payment_method=stripe_customer.invoice_settings.default_payment_method,
|
||||
)
|
||||
except stripe.error.CardError as e:
|
||||
except stripe.CardError as e:
|
||||
raise StripeCardError("card error", e.user_message)
|
||||
|
||||
PaymentIntent.objects.create(
|
||||
|
@ -1398,9 +1396,9 @@ class BillingSession(ABC):
|
|||
# to worry about this plan being used for any other purpose.
|
||||
# NOTE: This is the 2nd plan for the customer.
|
||||
plan_params["status"] = CustomerPlan.NEVER_STARTED
|
||||
plan_params[
|
||||
"invoicing_status"
|
||||
] = CustomerPlan.INVOICING_STATUS_INITIAL_INVOICE_TO_BE_SENT
|
||||
plan_params["invoicing_status"] = (
|
||||
CustomerPlan.INVOICING_STATUS_INITIAL_INVOICE_TO_BE_SENT
|
||||
)
|
||||
event_time = timezone_now().replace(microsecond=0)
|
||||
|
||||
# Schedule switching to the new plan at plan end date.
|
||||
|
|
|
@ -153,9 +153,9 @@ class Session(models.Model):
|
|||
|
||||
session_dict["status"] = self.get_status_as_string()
|
||||
session_dict["type"] = self.get_type_as_string()
|
||||
session_dict[
|
||||
"is_manual_license_management_upgrade_session"
|
||||
] = self.is_manual_license_management_upgrade_session
|
||||
session_dict["is_manual_license_management_upgrade_session"] = (
|
||||
self.is_manual_license_management_upgrade_session
|
||||
)
|
||||
session_dict["tier"] = self.tier
|
||||
event = self.get_last_associated_event()
|
||||
if event is not None:
|
||||
|
|
|
@ -163,7 +163,7 @@ def generate_and_save_stripe_fixture(
|
|||
request_mock.add_passthru("https://api.stripe.com")
|
||||
# Talk to Stripe
|
||||
stripe_object = mocked_function(*args, **kwargs)
|
||||
except stripe.error.StripeError as e:
|
||||
except stripe.StripeError as e:
|
||||
with open(fixture_path, "w") as f:
|
||||
assert e.headers is not None
|
||||
error_dict = {**vars(e), "headers": dict(e.headers)}
|
||||
|
@ -193,12 +193,12 @@ def read_stripe_fixture(
|
|||
fixture = orjson.loads(f.read())
|
||||
# Check for StripeError fixtures
|
||||
if "json_body" in fixture:
|
||||
requester = stripe.api_requestor.APIRequestor()
|
||||
requester = stripe._api_requestor._APIRequestor()
|
||||
# This function will raise the relevant StripeError according to the fixture
|
||||
requester.interpret_response(
|
||||
requester._interpret_response(
|
||||
fixture["http_body"], fixture["http_status"], fixture["headers"]
|
||||
)
|
||||
return stripe.util.convert_to_stripe_object(fixture)
|
||||
return stripe.convert_to_stripe_object(fixture)
|
||||
|
||||
return _read_stripe_fixture
|
||||
|
||||
|
@ -257,9 +257,9 @@ def normalize_fixture_data(
|
|||
# why we're doing something a bit more complicated
|
||||
for i, timestamp_field in enumerate(tested_timestamp_fields):
|
||||
# Don't use (..) notation, since the matched timestamp can easily appear in other fields
|
||||
pattern_translations[
|
||||
f'"{timestamp_field}": 1[5-9][0-9]{{8}}(?![0-9-])'
|
||||
] = f'"{timestamp_field}": 1{i+1:02}%07d'
|
||||
pattern_translations[f'"{timestamp_field}": 1[5-9][0-9]{{8}}(?![0-9-])'] = (
|
||||
f'"{timestamp_field}": 1{i+1:02}%07d'
|
||||
)
|
||||
|
||||
normalized_values: Dict[str, Dict[str, str]] = {pattern: {} for pattern in pattern_translations}
|
||||
for fixture_file in fixture_files_for_function(decorated_function):
|
||||
|
@ -752,7 +752,7 @@ class StripeTest(StripeTestCase):
|
|||
def test_catch_stripe_errors(self) -> None:
|
||||
@catch_stripe_errors
|
||||
def raise_invalid_request_error() -> None:
|
||||
raise stripe.error.InvalidRequestError("message", "param", "code", json_body={})
|
||||
raise stripe.InvalidRequestError("message", "param", "code", json_body={})
|
||||
|
||||
with self.assertLogs("corporate.stripe", "ERROR") as error_log:
|
||||
with self.assertRaises(BillingError) as billing_context:
|
||||
|
@ -766,9 +766,7 @@ class StripeTest(StripeTestCase):
|
|||
def raise_card_error() -> None:
|
||||
error_message = "The card number is not a valid credit card number."
|
||||
json_body = {"error": {"message": error_message}}
|
||||
raise stripe.error.CardError(
|
||||
error_message, "number", "invalid_number", json_body=json_body
|
||||
)
|
||||
raise stripe.CardError(error_message, "number", "invalid_number", json_body=json_body)
|
||||
|
||||
with self.assertLogs("corporate.stripe", "INFO") as info_log:
|
||||
with self.assertRaises(StripeCardError) as card_context:
|
||||
|
@ -2166,7 +2164,7 @@ class StripeTest(StripeTestCase):
|
|||
"tier": None,
|
||||
},
|
||||
)
|
||||
with self.assertRaises(stripe.error.CardError):
|
||||
with self.assertRaises(stripe.CardError):
|
||||
# We don't have to handle this since the Stripe Checkout page would
|
||||
# ask Customer to enter a valid card number. trigger_stripe_checkout_session_completed_webhook
|
||||
# emulates what happens in the Stripe Checkout page. Adding this check mostly for coverage of
|
||||
|
|
|
@ -554,15 +554,15 @@ def remote_billing_legacy_server_login(
|
|||
# authenticated as a billing admin for this remote server, so we need to store
|
||||
# our usual IdentityDict structure in the session.
|
||||
request.session["remote_billing_identities"] = {}
|
||||
request.session["remote_billing_identities"][
|
||||
f"remote_server:{remote_server_uuid}"
|
||||
] = LegacyServerIdentityDict(
|
||||
remote_server_uuid=remote_server_uuid,
|
||||
authenticated_at=datetime_to_timestamp(timezone_now()),
|
||||
# The lack of remote_billing_user_id indicates the auth hasn't been completed.
|
||||
# This means access to authenticated endpoints will be denied. Only proceeding
|
||||
# to the next step in the flow is permitted with this.
|
||||
remote_billing_user_id=None,
|
||||
request.session["remote_billing_identities"][f"remote_server:{remote_server_uuid}"] = (
|
||||
LegacyServerIdentityDict(
|
||||
remote_server_uuid=remote_server_uuid,
|
||||
authenticated_at=datetime_to_timestamp(timezone_now()),
|
||||
# The lack of remote_billing_user_id indicates the auth hasn't been completed.
|
||||
# This means access to authenticated endpoints will be denied. Only proceeding
|
||||
# to the next step in the flow is permitted with this.
|
||||
remote_billing_user_id=None,
|
||||
)
|
||||
)
|
||||
|
||||
context = {
|
||||
|
@ -755,14 +755,14 @@ def remote_billing_legacy_server_from_login_confirmation_link(
|
|||
# if the user came here e.g. in a different browser than they
|
||||
# started the login flow in.)
|
||||
request.session["remote_billing_identities"] = {}
|
||||
request.session["remote_billing_identities"][
|
||||
f"remote_server:{remote_server_uuid}"
|
||||
] = LegacyServerIdentityDict(
|
||||
remote_server_uuid=remote_server_uuid,
|
||||
authenticated_at=datetime_to_timestamp(timezone_now()),
|
||||
# Having a remote_billing_user_id indicates the auth has been completed.
|
||||
# The user will now be granted access to authenticated endpoints.
|
||||
remote_billing_user_id=remote_billing_user.id,
|
||||
request.session["remote_billing_identities"][f"remote_server:{remote_server_uuid}"] = (
|
||||
LegacyServerIdentityDict(
|
||||
remote_server_uuid=remote_server_uuid,
|
||||
authenticated_at=datetime_to_timestamp(timezone_now()),
|
||||
# Having a remote_billing_user_id indicates the auth has been completed.
|
||||
# The user will now be granted access to authenticated endpoints.
|
||||
remote_billing_user_id=remote_billing_user.id,
|
||||
)
|
||||
)
|
||||
|
||||
next_page = prereg_object.next_page
|
||||
|
|
|
@ -6,7 +6,6 @@ from django.conf import settings
|
|||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from stripe.webhook import Webhook
|
||||
|
||||
from corporate.lib.stripe import STRIPE_API_VERSION
|
||||
from corporate.lib.stripe_event_handler import (
|
||||
|
@ -27,14 +26,14 @@ def stripe_webhook(request: HttpRequest) -> HttpResponse:
|
|||
): # nocoverage: We can't verify the signature in test suite since we fetch the events
|
||||
# from Stripe events API and manually post to the webhook endpoint.
|
||||
try:
|
||||
stripe_event = Webhook.construct_event(
|
||||
stripe_event = stripe.Webhook.construct_event(
|
||||
request.body,
|
||||
request.headers["Stripe-Signature"],
|
||||
stripe_webhook_endpoint_secret,
|
||||
)
|
||||
except ValueError:
|
||||
return HttpResponse(status=400)
|
||||
except stripe.error.SignatureVerificationError:
|
||||
except stripe.SignatureVerificationError:
|
||||
return HttpResponse(status=400)
|
||||
else:
|
||||
assert not settings.PRODUCTION
|
||||
|
|
|
@ -137,7 +137,7 @@ lxml
|
|||
django-two-factor-auth[call,phonenumberslite,sms]
|
||||
|
||||
# Needed for processing payments (in corporate)
|
||||
stripe<7.8.0 # https://github.com/stripe/stripe-python/issues/1158
|
||||
stripe
|
||||
|
||||
# For checking whether email of the user is from a disposable email provider.
|
||||
disposable-email-domains
|
||||
|
|
|
@ -62,7 +62,7 @@ cairosvg
|
|||
python-debian
|
||||
|
||||
# Pattern-based lint tool
|
||||
semgrep
|
||||
semgrep<1.53.0 # https://github.com/boto/botocore/issues/2926
|
||||
|
||||
# Contains Pysa, a security-focused static analyzer
|
||||
pyre-check
|
||||
|
|
2376
requirements/dev.txt
2376
requirements/dev.txt
File diff suppressed because it is too large
Load Diff
|
@ -11,9 +11,9 @@ alabaster==0.7.13 \
|
|||
--hash=sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3 \
|
||||
--hash=sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2
|
||||
# via sphinx
|
||||
babel==2.13.1 \
|
||||
--hash=sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900 \
|
||||
--hash=sha256:7077a4984b02b6727ac10f1f7294484f737443d7e2e66c5e4380e41a3ae0b4ed
|
||||
babel==2.14.0 \
|
||||
--hash=sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363 \
|
||||
--hash=sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287
|
||||
# via sphinx
|
||||
certifi==2023.11.17 \
|
||||
--hash=sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1 \
|
||||
|
@ -126,13 +126,13 @@ imagesize==1.4.1 \
|
|||
--hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \
|
||||
--hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a
|
||||
# via sphinx
|
||||
importlib-metadata==7.0.0 \
|
||||
--hash=sha256:7fc841f8b8332803464e5dc1c63a2e59121f46ca186c0e2e182e80bf8c1319f7 \
|
||||
--hash=sha256:d97503976bb81f40a193d41ee6570868479c69d5068651eb039c40d850c59d67
|
||||
importlib-metadata==7.0.1 \
|
||||
--hash=sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e \
|
||||
--hash=sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc
|
||||
# via sphinx
|
||||
jinja2==3.1.2 \
|
||||
--hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \
|
||||
--hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
|
||||
jinja2==3.1.3 \
|
||||
--hash=sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa \
|
||||
--hash=sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90
|
||||
# via
|
||||
# myst-parser
|
||||
# sphinx
|
||||
|
@ -142,67 +142,67 @@ markdown-it-py==3.0.0 \
|
|||
# via
|
||||
# mdit-py-plugins
|
||||
# myst-parser
|
||||
markupsafe==2.1.3 \
|
||||
--hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \
|
||||
--hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \
|
||||
--hash=sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431 \
|
||||
--hash=sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686 \
|
||||
--hash=sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c \
|
||||
--hash=sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559 \
|
||||
--hash=sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc \
|
||||
--hash=sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb \
|
||||
--hash=sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939 \
|
||||
--hash=sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c \
|
||||
--hash=sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0 \
|
||||
--hash=sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4 \
|
||||
--hash=sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9 \
|
||||
--hash=sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575 \
|
||||
--hash=sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba \
|
||||
--hash=sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d \
|
||||
--hash=sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd \
|
||||
--hash=sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3 \
|
||||
--hash=sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00 \
|
||||
--hash=sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155 \
|
||||
--hash=sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac \
|
||||
--hash=sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52 \
|
||||
--hash=sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f \
|
||||
--hash=sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8 \
|
||||
--hash=sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b \
|
||||
--hash=sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007 \
|
||||
--hash=sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24 \
|
||||
--hash=sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea \
|
||||
--hash=sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198 \
|
||||
--hash=sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0 \
|
||||
--hash=sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee \
|
||||
--hash=sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be \
|
||||
--hash=sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2 \
|
||||
--hash=sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1 \
|
||||
--hash=sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707 \
|
||||
--hash=sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6 \
|
||||
--hash=sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c \
|
||||
--hash=sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58 \
|
||||
--hash=sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823 \
|
||||
--hash=sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779 \
|
||||
--hash=sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636 \
|
||||
--hash=sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c \
|
||||
--hash=sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad \
|
||||
--hash=sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee \
|
||||
--hash=sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc \
|
||||
--hash=sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2 \
|
||||
--hash=sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48 \
|
||||
--hash=sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7 \
|
||||
--hash=sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e \
|
||||
--hash=sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b \
|
||||
--hash=sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa \
|
||||
--hash=sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5 \
|
||||
--hash=sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e \
|
||||
--hash=sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb \
|
||||
--hash=sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9 \
|
||||
--hash=sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57 \
|
||||
--hash=sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc \
|
||||
--hash=sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc \
|
||||
--hash=sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2 \
|
||||
--hash=sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11
|
||||
markupsafe==2.1.4 \
|
||||
--hash=sha256:0042d6a9880b38e1dd9ff83146cc3c9c18a059b9360ceae207805567aacccc69 \
|
||||
--hash=sha256:0c26f67b3fe27302d3a412b85ef696792c4a2386293c53ba683a89562f9399b0 \
|
||||
--hash=sha256:0fbad3d346df8f9d72622ac71b69565e621ada2ce6572f37c2eae8dacd60385d \
|
||||
--hash=sha256:15866d7f2dc60cfdde12ebb4e75e41be862348b4728300c36cdf405e258415ec \
|
||||
--hash=sha256:1c98c33ffe20e9a489145d97070a435ea0679fddaabcafe19982fe9c971987d5 \
|
||||
--hash=sha256:21e7af8091007bf4bebf4521184f4880a6acab8df0df52ef9e513d8e5db23411 \
|
||||
--hash=sha256:23984d1bdae01bee794267424af55eef4dfc038dc5d1272860669b2aa025c9e3 \
|
||||
--hash=sha256:31f57d64c336b8ccb1966d156932f3daa4fee74176b0fdc48ef580be774aae74 \
|
||||
--hash=sha256:3583a3a3ab7958e354dc1d25be74aee6228938312ee875a22330c4dc2e41beb0 \
|
||||
--hash=sha256:36d7626a8cca4d34216875aee5a1d3d654bb3dac201c1c003d182283e3205949 \
|
||||
--hash=sha256:396549cea79e8ca4ba65525470d534e8a41070e6b3500ce2414921099cb73e8d \
|
||||
--hash=sha256:3a66c36a3864df95e4f62f9167c734b3b1192cb0851b43d7cc08040c074c6279 \
|
||||
--hash=sha256:3aae9af4cac263007fd6309c64c6ab4506dd2b79382d9d19a1994f9240b8db4f \
|
||||
--hash=sha256:3ab3a886a237f6e9c9f4f7d272067e712cdb4efa774bef494dccad08f39d8ae6 \
|
||||
--hash=sha256:47bb5f0142b8b64ed1399b6b60f700a580335c8e1c57f2f15587bd072012decc \
|
||||
--hash=sha256:49a3b78a5af63ec10d8604180380c13dcd870aba7928c1fe04e881d5c792dc4e \
|
||||
--hash=sha256:4df98d4a9cd6a88d6a585852f56f2155c9cdb6aec78361a19f938810aa020954 \
|
||||
--hash=sha256:5045e892cfdaecc5b4c01822f353cf2c8feb88a6ec1c0adef2a2e705eef0f656 \
|
||||
--hash=sha256:5244324676254697fe5c181fc762284e2c5fceeb1c4e3e7f6aca2b6f107e60dc \
|
||||
--hash=sha256:54635102ba3cf5da26eb6f96c4b8c53af8a9c0d97b64bdcb592596a6255d8518 \
|
||||
--hash=sha256:54a7e1380dfece8847c71bf7e33da5d084e9b889c75eca19100ef98027bd9f56 \
|
||||
--hash=sha256:55d03fea4c4e9fd0ad75dc2e7e2b6757b80c152c032ea1d1de487461d8140efc \
|
||||
--hash=sha256:698e84142f3f884114ea8cf83e7a67ca8f4ace8454e78fe960646c6c91c63bfa \
|
||||
--hash=sha256:6aa5e2e7fc9bc042ae82d8b79d795b9a62bd8f15ba1e7594e3db243f158b5565 \
|
||||
--hash=sha256:7653fa39578957bc42e5ebc15cf4361d9e0ee4b702d7d5ec96cdac860953c5b4 \
|
||||
--hash=sha256:765f036a3d00395a326df2835d8f86b637dbaf9832f90f5d196c3b8a7a5080cb \
|
||||
--hash=sha256:78bc995e004681246e85e28e068111a4c3f35f34e6c62da1471e844ee1446250 \
|
||||
--hash=sha256:7a07f40ef8f0fbc5ef1000d0c78771f4d5ca03b4953fc162749772916b298fc4 \
|
||||
--hash=sha256:8b570a1537367b52396e53325769608f2a687ec9a4363647af1cded8928af959 \
|
||||
--hash=sha256:987d13fe1d23e12a66ca2073b8d2e2a75cec2ecb8eab43ff5624ba0ad42764bc \
|
||||
--hash=sha256:9896fca4a8eb246defc8b2a7ac77ef7553b638e04fbf170bff78a40fa8a91474 \
|
||||
--hash=sha256:9e9e3c4020aa2dc62d5dd6743a69e399ce3de58320522948af6140ac959ab863 \
|
||||
--hash=sha256:a0b838c37ba596fcbfca71651a104a611543077156cb0a26fe0c475e1f152ee8 \
|
||||
--hash=sha256:a4d176cfdfde84f732c4a53109b293d05883e952bbba68b857ae446fa3119b4f \
|
||||
--hash=sha256:a76055d5cb1c23485d7ddae533229039b850db711c554a12ea64a0fd8a0129e2 \
|
||||
--hash=sha256:a76cd37d229fc385738bd1ce4cba2a121cf26b53864c1772694ad0ad348e509e \
|
||||
--hash=sha256:a7cc49ef48a3c7a0005a949f3c04f8baa5409d3f663a1b36f0eba9bfe2a0396e \
|
||||
--hash=sha256:abf5ebbec056817057bfafc0445916bb688a255a5146f900445d081db08cbabb \
|
||||
--hash=sha256:b0fe73bac2fed83839dbdbe6da84ae2a31c11cfc1c777a40dbd8ac8a6ed1560f \
|
||||
--hash=sha256:b6f14a9cd50c3cb100eb94b3273131c80d102e19bb20253ac7bd7336118a673a \
|
||||
--hash=sha256:b83041cda633871572f0d3c41dddd5582ad7d22f65a72eacd8d3d6d00291df26 \
|
||||
--hash=sha256:b835aba863195269ea358cecc21b400276747cc977492319fd7682b8cd2c253d \
|
||||
--hash=sha256:bf1196dcc239e608605b716e7b166eb5faf4bc192f8a44b81e85251e62584bd2 \
|
||||
--hash=sha256:c669391319973e49a7c6230c218a1e3044710bc1ce4c8e6eb71f7e6d43a2c131 \
|
||||
--hash=sha256:c7556bafeaa0a50e2fe7dc86e0382dea349ebcad8f010d5a7dc6ba568eaaa789 \
|
||||
--hash=sha256:c8f253a84dbd2c63c19590fa86a032ef3d8cc18923b8049d91bcdeeb2581fbf6 \
|
||||
--hash=sha256:d18b66fe626ac412d96c2ab536306c736c66cf2a31c243a45025156cc190dc8a \
|
||||
--hash=sha256:d5291d98cd3ad9a562883468c690a2a238c4a6388ab3bd155b0c75dd55ece858 \
|
||||
--hash=sha256:d5c31fe855c77cad679b302aabc42d724ed87c043b1432d457f4976add1c2c3e \
|
||||
--hash=sha256:d6e427c7378c7f1b2bef6a344c925b8b63623d3321c09a237b7cc0e77dd98ceb \
|
||||
--hash=sha256:dac1ebf6983148b45b5fa48593950f90ed6d1d26300604f321c74a9ca1609f8e \
|
||||
--hash=sha256:de8153a7aae3835484ac168a9a9bdaa0c5eee4e0bc595503c95d53b942879c84 \
|
||||
--hash=sha256:e1a0d1924a5013d4f294087e00024ad25668234569289650929ab871231668e7 \
|
||||
--hash=sha256:e7902211afd0af05fbadcc9a312e4cf10f27b779cf1323e78d52377ae4b72bea \
|
||||
--hash=sha256:e888ff76ceb39601c59e219f281466c6d7e66bd375b4ec1ce83bcdc68306796b \
|
||||
--hash=sha256:f06e5a9e99b7df44640767842f414ed5d7bedaaa78cd817ce04bbd6fd86e2dd6 \
|
||||
--hash=sha256:f6be2d708a9d0e9b0054856f07ac7070fbe1754be40ca8525d5adccdbda8f475 \
|
||||
--hash=sha256:f9917691f410a2e0897d1ef99619fd3f7dd503647c8ff2475bf90c3cf222ad74 \
|
||||
--hash=sha256:fc1a75aa8f11b87910ffd98de62b29d6520b6d6e8a3de69a70ca34dea85d2a8a \
|
||||
--hash=sha256:fe8512ed897d5daf089e5bd010c3dc03bb1bdae00b35588c49b98268d4a01e00
|
||||
# via jinja2
|
||||
mdit-py-plugins==0.4.0 \
|
||||
--hash=sha256:b51b3bb70691f57f974e257e367107857a93b36f322a9e6d44ca5bf28ec2def9 \
|
||||
|
@ -224,9 +224,9 @@ pygments==2.17.2 \
|
|||
--hash=sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c \
|
||||
--hash=sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367
|
||||
# via sphinx
|
||||
pytz==2023.3.post1 \
|
||||
--hash=sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b \
|
||||
--hash=sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7
|
||||
pytz==2023.4 \
|
||||
--hash=sha256:31d4583c4ed539cd037956140d695e42c033a19e984bfce9964a3f7d59bc2b40 \
|
||||
--hash=sha256:f90ef520d95e7c46951105338d918664ebfd6f1d995bd7d153127ce90efafa6a
|
||||
# via babel
|
||||
pyyaml==6.0.1 \
|
||||
--hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \
|
||||
|
@ -258,6 +258,7 @@ pyyaml==6.0.1 \
|
|||
--hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \
|
||||
--hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \
|
||||
--hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \
|
||||
--hash=sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef \
|
||||
--hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \
|
||||
--hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \
|
||||
--hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \
|
||||
|
|
|
@ -11,7 +11,7 @@ types-boto
|
|||
types-chardet
|
||||
types-decorator
|
||||
types-jsonschema
|
||||
types-Markdown<3.5.0.3 # https://github.com/python/typeshed/pull/10972#pullrequestreview-1760743552
|
||||
types-Markdown
|
||||
types-oauthlib
|
||||
types-polib
|
||||
types-pika
|
||||
|
|
|
@ -17,7 +17,7 @@ pip==20.3.4 \
|
|||
--hash=sha256:217ae5161a0e08c0fb873858806e3478c9775caffce5168b50ec885e358c199d \
|
||||
--hash=sha256:6773934e5f5fc3eaa8c5a44949b5b924fc122daa0a8aa9f80c835b4ca2a543fc
|
||||
# via -r requirements/pip.in
|
||||
setuptools==69.0.2 \
|
||||
--hash=sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2 \
|
||||
--hash=sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6
|
||||
setuptools==69.0.3 \
|
||||
--hash=sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05 \
|
||||
--hash=sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78
|
||||
# via -r requirements/pip.in
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,7 @@
|
|||
"""
|
||||
Use libraries from a virtualenv (by modifying sys.path) in production.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
|
|
@ -186,8 +186,7 @@ class FilterType(Enum):
|
|||
|
||||
|
||||
class FilterFunc(Protocol):
|
||||
def __call__(self, m: Match[str], t: str = ...) -> bool:
|
||||
...
|
||||
def __call__(self, m: Match[str], t: str = ...) -> bool: ...
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
|
|
@ -137,10 +137,10 @@ def run() -> None:
|
|||
pass_targets=False,
|
||||
description="Static type checker for Python (config: pyproject.toml)",
|
||||
suppress_line=(
|
||||
lambda line: line.startswith("Daemon") or line == "Restarting: configuration changed"
|
||||
)
|
||||
if args.use_mypy_daemon
|
||||
else lambda _: False,
|
||||
(lambda line: line.startswith("Daemon") or line == "Restarting: configuration changed")
|
||||
if args.use_mypy_daemon
|
||||
else lambda _: False
|
||||
),
|
||||
)
|
||||
linter_config.external_linter(
|
||||
"ruff",
|
||||
|
|
|
@ -48,4 +48,4 @@ API_FEATURE_LEVEL = 237
|
|||
# historical commits sharing the same major version, in which case a
|
||||
# minor version bump suffices.
|
||||
|
||||
PROVISION_VERSION = (258, 0)
|
||||
PROVISION_VERSION = (259, 0)
|
||||
|
|
|
@ -445,9 +445,9 @@ def do_update_message(
|
|||
event["orig_rendered_content"] = target_message.rendered_content
|
||||
edit_history_event["prev_content"] = target_message.content
|
||||
edit_history_event["prev_rendered_content"] = target_message.rendered_content
|
||||
edit_history_event[
|
||||
"prev_rendered_content_version"
|
||||
] = target_message.rendered_content_version
|
||||
edit_history_event["prev_rendered_content_version"] = (
|
||||
target_message.rendered_content_version
|
||||
)
|
||||
target_message.content = content
|
||||
target_message.rendered_content = rendering_result.rendered_content
|
||||
target_message.rendered_content_version = markdown_version
|
||||
|
@ -848,16 +848,16 @@ def do_update_message(
|
|||
if new_stream is not None and user_topic.user_profile_id in losing_access_user_ids:
|
||||
stream_inaccessible_to_user_profiles.append(user_topic.user_profile)
|
||||
else:
|
||||
orig_topic_user_profile_to_visibility_policy[
|
||||
user_topic.user_profile
|
||||
] = user_topic.visibility_policy
|
||||
orig_topic_user_profile_to_visibility_policy[user_topic.user_profile] = (
|
||||
user_topic.visibility_policy
|
||||
)
|
||||
|
||||
for user_topic in get_users_with_user_topic_visibility_policy(
|
||||
target_stream.id, target_topic_name
|
||||
):
|
||||
target_topic_user_profile_to_visibility_policy[
|
||||
user_topic.user_profile
|
||||
] = user_topic.visibility_policy
|
||||
target_topic_user_profile_to_visibility_policy[user_topic.user_profile] = (
|
||||
user_topic.visibility_policy
|
||||
)
|
||||
|
||||
# User profiles having any of the visibility policies set for either the original or target topic.
|
||||
user_profiles_having_visibility_policy: Set[UserProfile] = set(
|
||||
|
@ -867,18 +867,18 @@ def do_update_message(
|
|||
)
|
||||
)
|
||||
|
||||
user_profiles_for_visibility_policy_pair: Dict[
|
||||
Tuple[int, int], List[UserProfile]
|
||||
] = defaultdict(list)
|
||||
user_profiles_for_visibility_policy_pair: Dict[Tuple[int, int], List[UserProfile]] = (
|
||||
defaultdict(list)
|
||||
)
|
||||
for user_profile_with_policy in user_profiles_having_visibility_policy:
|
||||
if user_profile_with_policy not in target_topic_user_profile_to_visibility_policy:
|
||||
target_topic_user_profile_to_visibility_policy[
|
||||
user_profile_with_policy
|
||||
] = UserTopic.VisibilityPolicy.INHERIT
|
||||
target_topic_user_profile_to_visibility_policy[user_profile_with_policy] = (
|
||||
UserTopic.VisibilityPolicy.INHERIT
|
||||
)
|
||||
elif user_profile_with_policy not in orig_topic_user_profile_to_visibility_policy:
|
||||
orig_topic_user_profile_to_visibility_policy[
|
||||
user_profile_with_policy
|
||||
] = UserTopic.VisibilityPolicy.INHERIT
|
||||
orig_topic_user_profile_to_visibility_policy[user_profile_with_policy] = (
|
||||
UserTopic.VisibilityPolicy.INHERIT
|
||||
)
|
||||
|
||||
orig_topic_visibility_policy = orig_topic_user_profile_to_visibility_policy[
|
||||
user_profile_with_policy
|
||||
|
|
|
@ -214,8 +214,7 @@ def build_subscription(recipient_id: int, user_id: int, subscription_id: int) ->
|
|||
|
||||
|
||||
class GetUsers(Protocol):
|
||||
def __call__(self, stream_id: int = ..., huddle_id: int = ...) -> Set[int]:
|
||||
...
|
||||
def __call__(self, stream_id: int = ..., huddle_id: int = ...) -> Set[int]: ...
|
||||
|
||||
|
||||
def build_stream_subscriptions(
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
spec:
|
||||
https://docs.mattermost.com/administration/bulk-export.html
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
|
|
|
@ -481,9 +481,9 @@ def do_login(request: HttpRequest, user_profile: UserProfile) -> None:
|
|||
assert isinstance(validated_user_profile, UserProfile)
|
||||
|
||||
django_login(request, validated_user_profile)
|
||||
RequestNotes.get_notes(
|
||||
request
|
||||
).requester_for_logs = validated_user_profile.format_requester_for_logs()
|
||||
RequestNotes.get_notes(request).requester_for_logs = (
|
||||
validated_user_profile.format_requester_for_logs()
|
||||
)
|
||||
process_client(request, validated_user_profile, is_browser_view=True)
|
||||
if settings.TWO_FACTOR_AUTHENTICATION_ENABLED:
|
||||
# Log in with two factor authentication as well.
|
||||
|
@ -537,15 +537,13 @@ def human_users_only(
|
|||
return _wrapped_view_func
|
||||
|
||||
|
||||
# Based on Django 1.8's @login_required
|
||||
@overload
|
||||
def zulip_login_required(
|
||||
function: Callable[Concatenate[HttpRequest, ParamT], HttpResponse],
|
||||
redirect_field_name: str = REDIRECT_FIELD_NAME,
|
||||
login_url: str = settings.HOME_NOT_LOGGED_IN,
|
||||
) -> Callable[Concatenate[HttpRequest, ParamT], HttpResponse]:
|
||||
...
|
||||
|
||||
|
||||
) -> Callable[Concatenate[HttpRequest, ParamT], HttpResponse]: ...
|
||||
@overload
|
||||
def zulip_login_required(
|
||||
function: None,
|
||||
|
@ -554,11 +552,7 @@ def zulip_login_required(
|
|||
) -> Callable[
|
||||
[Callable[Concatenate[HttpRequest, ParamT], HttpResponse]],
|
||||
Callable[Concatenate[HttpRequest, ParamT], HttpResponse],
|
||||
]:
|
||||
...
|
||||
|
||||
|
||||
# Based on Django 1.8's @login_required
|
||||
]: ...
|
||||
def zulip_login_required(
|
||||
function: Optional[Callable[Concatenate[HttpRequest, ParamT], HttpResponse]] = None,
|
||||
redirect_field_name: str = REDIRECT_FIELD_NAME,
|
||||
|
|
|
@ -132,9 +132,11 @@ def der_encode_ticket(tkt: Dict[str, Any]) -> bytes:
|
|||
der_encode_sequence( # EncryptedData
|
||||
[
|
||||
der_encode_int32(tkt["encPart"]["etype"]),
|
||||
der_encode_uint32(tkt["encPart"]["kvno"])
|
||||
if "kvno" in tkt["encPart"]
|
||||
else None,
|
||||
(
|
||||
der_encode_uint32(tkt["encPart"]["kvno"])
|
||||
if "kvno" in tkt["encPart"]
|
||||
else None
|
||||
),
|
||||
der_encode_octet_string(base64.b64decode(tkt["encPart"]["cipher"])),
|
||||
]
|
||||
),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""
|
||||
Context managers, i.e. things you can use with the 'with' statement.
|
||||
"""
|
||||
|
||||
import fcntl
|
||||
from contextlib import contextmanager
|
||||
from typing import IO, Any, Iterator, Union
|
||||
|
|
|
@ -661,9 +661,9 @@ def handle_missedmessage_emails(
|
|||
unique_messages[m.id] = dict(
|
||||
message=m,
|
||||
trigger=message_info.trigger if message_info else None,
|
||||
mentioned_user_group_id=message_info.mentioned_user_group_id
|
||||
if message_info is not None
|
||||
else None,
|
||||
mentioned_user_group_id=(
|
||||
message_info.mentioned_user_group_id if message_info is not None else None
|
||||
),
|
||||
)
|
||||
do_send_missedmessage_events_reply_in_zulip(
|
||||
user_profile,
|
||||
|
|
|
@ -350,9 +350,9 @@ def fetch_initial_state_data(
|
|||
state["server_emoji_data_url"] = emoji.data_url()
|
||||
|
||||
state["server_needs_upgrade"] = is_outdated_server(user_profile)
|
||||
state[
|
||||
"event_queue_longpoll_timeout_seconds"
|
||||
] = settings.EVENT_QUEUE_LONGPOLL_TIMEOUT_SECONDS
|
||||
state["event_queue_longpoll_timeout_seconds"] = (
|
||||
settings.EVENT_QUEUE_LONGPOLL_TIMEOUT_SECONDS
|
||||
)
|
||||
|
||||
# TODO: This probably belongs on the server object.
|
||||
state["realm_default_external_accounts"] = get_default_external_accounts()
|
||||
|
@ -393,15 +393,15 @@ def fetch_initial_state_data(
|
|||
state["server_presence_ping_interval_seconds"] = settings.PRESENCE_PING_INTERVAL_SECS
|
||||
state["server_presence_offline_threshold_seconds"] = settings.OFFLINE_THRESHOLD_SECS
|
||||
# Typing notifications protocol parameters for client behavior.
|
||||
state[
|
||||
"server_typing_started_expiry_period_milliseconds"
|
||||
] = settings.TYPING_STARTED_EXPIRY_PERIOD_MILLISECONDS
|
||||
state[
|
||||
"server_typing_stopped_wait_period_milliseconds"
|
||||
] = settings.TYPING_STOPPED_WAIT_PERIOD_MILLISECONDS
|
||||
state[
|
||||
"server_typing_started_wait_period_milliseconds"
|
||||
] = settings.TYPING_STARTED_WAIT_PERIOD_MILLISECONDS
|
||||
state["server_typing_started_expiry_period_milliseconds"] = (
|
||||
settings.TYPING_STARTED_EXPIRY_PERIOD_MILLISECONDS
|
||||
)
|
||||
state["server_typing_stopped_wait_period_milliseconds"] = (
|
||||
settings.TYPING_STOPPED_WAIT_PERIOD_MILLISECONDS
|
||||
)
|
||||
state["server_typing_started_wait_period_milliseconds"] = (
|
||||
settings.TYPING_STARTED_WAIT_PERIOD_MILLISECONDS
|
||||
)
|
||||
|
||||
state["server_supported_permission_settings"] = get_server_supported_permission_settings()
|
||||
if want("realm_user_settings_defaults"):
|
||||
|
@ -907,9 +907,9 @@ def apply_event(
|
|||
# Recompute properties based on is_admin/is_guest
|
||||
state["can_create_private_streams"] = user_profile.can_create_private_streams()
|
||||
state["can_create_public_streams"] = user_profile.can_create_public_streams()
|
||||
state[
|
||||
"can_create_web_public_streams"
|
||||
] = user_profile.can_create_web_public_streams()
|
||||
state["can_create_web_public_streams"] = (
|
||||
user_profile.can_create_web_public_streams()
|
||||
)
|
||||
state["can_create_streams"] = (
|
||||
state["can_create_private_streams"]
|
||||
or state["can_create_public_streams"]
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""
|
||||
This module stores data for "external account" custom profile field.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict
|
||||
|
||||
|
|
|
@ -45,9 +45,11 @@ def generate_topics(num_topics: int) -> List[str]:
|
|||
resolved_topic_probability = 0.05
|
||||
|
||||
return [
|
||||
RESOLVED_TOPIC_PREFIX + topic_name
|
||||
if random.random() < resolved_topic_probability
|
||||
else topic_name
|
||||
(
|
||||
RESOLVED_TOPIC_PREFIX + topic_name
|
||||
if random.random() < resolved_topic_probability
|
||||
else topic_name
|
||||
)
|
||||
for topic_name in topic_names
|
||||
]
|
||||
|
||||
|
|
|
@ -570,13 +570,14 @@ class BacktickInlineProcessor(markdown.inlinepatterns.BacktickInlineProcessor):
|
|||
@override
|
||||
def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issues/10197
|
||||
self, m: Match[str], data: str
|
||||
) -> Union[Tuple[None, None, None], Tuple[Element, int, int]]:
|
||||
) -> Tuple[Union[Element, str, None], Optional[int], Optional[int]]:
|
||||
# Let upstream's implementation do its job as it is, we'll
|
||||
# just replace the text to not strip the group because it
|
||||
# makes it impossible to put leading/trailing whitespace in
|
||||
# an inline code span.
|
||||
el, start, end = ret = super().handleMatch(m, data)
|
||||
if el is not None and m.group(3):
|
||||
assert isinstance(el, Element)
|
||||
# upstream's code here is: m.group(3).strip() rather than m.group(3).
|
||||
el.text = markdown.util.AtomicString(markdown.util.code_escape(m.group(3)))
|
||||
return ret
|
||||
|
@ -1492,7 +1493,7 @@ class UnicodeEmoji(CompiledInlineProcessor):
|
|||
@override
|
||||
def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issues/10197
|
||||
self, match: Match[str], data: str
|
||||
) -> Union[Tuple[None, None, None], Tuple[Element, int, int]]:
|
||||
) -> Tuple[Union[Element, str, None], Optional[int], Optional[int]]:
|
||||
orig_syntax = match.group("syntax")
|
||||
|
||||
# We want to avoid turning things like arrows (↔) and keycaps (numbers
|
||||
|
@ -1840,7 +1841,7 @@ class LinkifierPattern(CompiledInlineProcessor):
|
|||
@override
|
||||
def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issues/10197
|
||||
self, m: Match[str], data: str
|
||||
) -> Union[Tuple[Element, int, int], Tuple[None, None, None]]:
|
||||
) -> Tuple[Union[Element, str, None], Optional[int], Optional[int]]:
|
||||
db_data: Optional[DbData] = self.zmd.zulip_db_data
|
||||
url = url_to_a(
|
||||
db_data,
|
||||
|
@ -1861,7 +1862,7 @@ class UserMentionPattern(CompiledInlineProcessor):
|
|||
@override
|
||||
def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issues/10197
|
||||
self, m: Match[str], data: str
|
||||
) -> Union[Tuple[None, None, None], Tuple[Element, int, int]]:
|
||||
) -> Tuple[Union[Element, str, None], Optional[int], Optional[int]]:
|
||||
name = m.group("match")
|
||||
silent = m.group("silent") == "_"
|
||||
db_data: Optional[DbData] = self.zmd.zulip_db_data
|
||||
|
@ -1927,7 +1928,7 @@ class UserGroupMentionPattern(CompiledInlineProcessor):
|
|||
@override
|
||||
def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issues/10197
|
||||
self, m: Match[str], data: str
|
||||
) -> Union[Tuple[None, None, None], Tuple[Element, int, int]]:
|
||||
) -> Tuple[Union[Element, str, None], Optional[int], Optional[int]]:
|
||||
name = m.group("match")
|
||||
silent = m.group("silent") == "_"
|
||||
db_data: Optional[DbData] = self.zmd.zulip_db_data
|
||||
|
@ -1968,7 +1969,7 @@ class StreamPattern(CompiledInlineProcessor):
|
|||
@override
|
||||
def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issues/10197
|
||||
self, m: Match[str], data: str
|
||||
) -> Union[Tuple[None, None, None], Tuple[Element, int, int]]:
|
||||
) -> Tuple[Union[Element, str, None], Optional[int], Optional[int]]:
|
||||
name = m.group("stream_name")
|
||||
|
||||
stream_id = self.find_stream_id(name)
|
||||
|
@ -2000,7 +2001,7 @@ class StreamTopicPattern(CompiledInlineProcessor):
|
|||
@override
|
||||
def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issues/10197
|
||||
self, m: Match[str], data: str
|
||||
) -> Union[Tuple[None, None, None], Tuple[Element, int, int]]:
|
||||
) -> Tuple[Union[Element, str, None], Optional[int], Optional[int]]:
|
||||
stream_name = m.group("stream_name")
|
||||
topic_name = m.group("topic_name")
|
||||
|
||||
|
@ -2121,11 +2122,11 @@ class LinkInlineProcessor(markdown.inlinepatterns.LinkInlineProcessor):
|
|||
@override
|
||||
def handleMatch( # type: ignore[override] # https://github.com/python/mypy/issues/10197
|
||||
self, m: Match[str], data: str
|
||||
) -> Union[Tuple[None, None, None], Tuple[Element, int, int]]:
|
||||
) -> Tuple[Union[Element, str, None], Optional[int], Optional[int]]:
|
||||
ret = super().handleMatch(m, data)
|
||||
if ret[0] is not None:
|
||||
el: Optional[Element]
|
||||
el, match_start, index = ret
|
||||
assert isinstance(el, Element)
|
||||
el = self.zulip_specific_link_changes(el)
|
||||
if el is not None:
|
||||
return el, match_start, index
|
||||
|
|
|
@ -75,6 +75,7 @@ Dependencies:
|
|||
* [Pygments (optional)](http://pygments.org)
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Any, Callable, Dict, Iterable, List, Mapping, MutableSequence, Optional, Sequence
|
||||
|
||||
|
|
|
@ -664,9 +664,9 @@ class MessageDict:
|
|||
if rendered_content is not None:
|
||||
obj["rendered_content"] = rendered_content
|
||||
else:
|
||||
obj[
|
||||
"rendered_content"
|
||||
] = "<p>[Zulip note: Sorry, we could not understand the formatting of your message]</p>"
|
||||
obj["rendered_content"] = (
|
||||
"<p>[Zulip note: Sorry, we could not understand the formatting of your message]</p>"
|
||||
)
|
||||
|
||||
if rendered_content is not None:
|
||||
obj["is_me_message"] = Message.is_status_message(content, rendered_content)
|
||||
|
|
|
@ -130,8 +130,7 @@ def is_web_public_narrow(narrow: Optional[Iterable[Dict[str, Any]]]) -> bool:
|
|||
|
||||
|
||||
class NarrowPredicate(Protocol):
|
||||
def __call__(self, *, message: Dict[str, Any], flags: List[str]) -> bool:
|
||||
...
|
||||
def __call__(self, *, message: Dict[str, Any], flags: List[str]) -> bool: ...
|
||||
|
||||
|
||||
def build_narrow_predicate(
|
||||
|
|
|
@ -17,6 +17,7 @@ from users:
|
|||
And then on top of that, we want to represent narrow
|
||||
specification internally as dataclasses.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Collection, Sequence
|
||||
|
||||
|
|
|
@ -50,5 +50,4 @@ class BaseNotes(Generic[_KeyT, _DataT], metaclass=ABCMeta):
|
|||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def init_notes(cls) -> _DataT:
|
||||
...
|
||||
def init_notes(cls) -> _DataT: ...
|
||||
|
|
|
@ -291,9 +291,9 @@ def send_apple_push_notification(
|
|||
if have_missing_app_id:
|
||||
devices = [device for device in devices if device.ios_app_id is not None]
|
||||
|
||||
async def send_all_notifications() -> Iterable[
|
||||
Tuple[DeviceToken, Union[aioapns.common.NotificationResult, BaseException]]
|
||||
]:
|
||||
async def send_all_notifications() -> (
|
||||
Iterable[Tuple[DeviceToken, Union[aioapns.common.NotificationResult, BaseException]]]
|
||||
):
|
||||
requests = [
|
||||
aioapns.NotificationRequest(
|
||||
apns_topic=device.ios_app_id,
|
||||
|
|
|
@ -80,9 +80,9 @@ class QueueClient(Generic[ChannelT], metaclass=ABCMeta):
|
|||
if self.rabbitmq_heartbeat == 0:
|
||||
tcp_options = dict(TCP_KEEPIDLE=60 * 5)
|
||||
|
||||
ssl_options: Union[
|
||||
Type[pika.ConnectionParameters._DEFAULT], pika.SSLOptions
|
||||
] = pika.ConnectionParameters._DEFAULT
|
||||
ssl_options: Union[Type[pika.ConnectionParameters._DEFAULT], pika.SSLOptions] = (
|
||||
pika.ConnectionParameters._DEFAULT
|
||||
)
|
||||
if settings.RABBITMQ_USE_TLS:
|
||||
ssl_options = pika.SSLOptions(context=ssl.create_default_context())
|
||||
|
||||
|
@ -313,9 +313,11 @@ class TornadoQueueClient(QueueClient[Channel]):
|
|||
self._connection_failure_count += 1
|
||||
retry_secs = self.CONNECTION_RETRY_SECS
|
||||
self.log.log(
|
||||
logging.CRITICAL
|
||||
if self._connection_failure_count > self.CONNECTION_FAILURES_BEFORE_NOTIFY
|
||||
else logging.WARNING,
|
||||
(
|
||||
logging.CRITICAL
|
||||
if self._connection_failure_count > self.CONNECTION_FAILURES_BEFORE_NOTIFY
|
||||
else logging.WARNING
|
||||
),
|
||||
"TornadoQueueClient couldn't connect to RabbitMQ, retrying in %d secs...",
|
||||
retry_secs,
|
||||
)
|
||||
|
|
|
@ -222,8 +222,7 @@ def REQ(
|
|||
documentation_pending: bool = ...,
|
||||
aliases: Sequence[str] = ...,
|
||||
path_only: bool = ...,
|
||||
) -> ResultT:
|
||||
...
|
||||
) -> ResultT: ...
|
||||
|
||||
|
||||
# Overload 2: json_validator
|
||||
|
@ -238,8 +237,7 @@ def REQ(
|
|||
documentation_pending: bool = ...,
|
||||
aliases: Sequence[str] = ...,
|
||||
path_only: bool = ...,
|
||||
) -> ResultT:
|
||||
...
|
||||
) -> ResultT: ...
|
||||
|
||||
|
||||
# Overload 3: no converter/json_validator, default: str or unspecified, argument_type=None
|
||||
|
@ -253,8 +251,7 @@ def REQ(
|
|||
documentation_pending: bool = ...,
|
||||
aliases: Sequence[str] = ...,
|
||||
path_only: bool = ...,
|
||||
) -> str:
|
||||
...
|
||||
) -> str: ...
|
||||
|
||||
|
||||
# Overload 4: no converter/validator, default=None, argument_type=None
|
||||
|
@ -268,8 +265,7 @@ def REQ(
|
|||
documentation_pending: bool = ...,
|
||||
aliases: Sequence[str] = ...,
|
||||
path_only: bool = ...,
|
||||
) -> Optional[str]:
|
||||
...
|
||||
) -> Optional[str]: ...
|
||||
|
||||
|
||||
# Overload 5: argument_type="body"
|
||||
|
@ -283,8 +279,7 @@ def REQ(
|
|||
documentation_pending: bool = ...,
|
||||
aliases: Sequence[str] = ...,
|
||||
path_only: bool = ...,
|
||||
) -> ResultT:
|
||||
...
|
||||
) -> ResultT: ...
|
||||
|
||||
|
||||
# Implementation
|
||||
|
|
|
@ -37,7 +37,7 @@ from django.http.request import QueryDict
|
|||
from django.http.response import HttpResponseBase
|
||||
from django.test import override_settings
|
||||
from django.urls import URLResolver
|
||||
from moto.s3 import mock_s3
|
||||
from moto.core.decorator import mock_aws
|
||||
from mypy_boto3_s3.service_resource import Bucket
|
||||
from typing_extensions import ParamSpec, override
|
||||
|
||||
|
@ -562,7 +562,7 @@ P = ParamSpec("P")
|
|||
|
||||
|
||||
def use_s3_backend(method: Callable[P, None]) -> Callable[P, None]:
|
||||
@mock_s3
|
||||
@mock_aws
|
||||
@override_settings(LOCAL_UPLOADS_DIR=None)
|
||||
@override_settings(LOCAL_AVATARS_DIR=None)
|
||||
@override_settings(LOCAL_FILES_DIR=None)
|
||||
|
|
|
@ -10,7 +10,6 @@ import boto3
|
|||
import botocore
|
||||
from botocore.client import Config
|
||||
from django.conf import settings
|
||||
from mypy_boto3_s3 import S3Client
|
||||
from mypy_boto3_s3.service_resource import Bucket, Object
|
||||
from typing_extensions import override
|
||||
|
||||
|
@ -107,7 +106,7 @@ def upload_image_to_s3(
|
|||
|
||||
|
||||
def get_signed_upload_url(path: str, force_download: bool = False) -> str:
|
||||
client: S3Client = get_bucket(settings.S3_AUTH_UPLOADS_BUCKET).meta.client # type: ignore[assignment] # https://github.com/youtype/mypy_boto3_builder/issues/239
|
||||
client = get_bucket(settings.S3_AUTH_UPLOADS_BUCKET).meta.client
|
||||
params = {
|
||||
"Bucket": settings.S3_AUTH_UPLOADS_BUCKET,
|
||||
"Key": path,
|
||||
|
@ -164,7 +163,7 @@ class S3UploadBackend(ZulipUploadBackend):
|
|||
# We do not access self.avatar_bucket.meta.client directly,
|
||||
# since that client is auth'd, and we want only the direct
|
||||
# unauthed endpoint here.
|
||||
client: S3Client = get_bucket(self.avatar_bucket.name, authed=False).meta.client # type: ignore[assignment] # https://github.com/youtype/mypy_boto3_builder/issues/239
|
||||
client = get_bucket(self.avatar_bucket.name, authed=False).meta.client
|
||||
dummy_signed_url = client.generate_presigned_url(
|
||||
ClientMethod="get_object",
|
||||
Params={
|
||||
|
|
|
@ -27,6 +27,7 @@ To extend this concept, it's simply a matter of writing your own validator
|
|||
for any particular type of object.
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
|
@ -260,10 +261,7 @@ def check_dict(
|
|||
optional_keys: Collection[Tuple[str, Validator[object]]] = [],
|
||||
*,
|
||||
_allow_only_listed_keys: bool = False,
|
||||
) -> Validator[Dict[str, object]]:
|
||||
...
|
||||
|
||||
|
||||
) -> Validator[Dict[str, object]]: ...
|
||||
@overload
|
||||
def check_dict(
|
||||
required_keys: Collection[Tuple[str, Validator[ResultT]]] = [],
|
||||
|
@ -271,10 +269,7 @@ def check_dict(
|
|||
*,
|
||||
value_validator: Validator[ResultT],
|
||||
_allow_only_listed_keys: bool = False,
|
||||
) -> Validator[Dict[str, ResultT]]:
|
||||
...
|
||||
|
||||
|
||||
) -> Validator[Dict[str, ResultT]]: ...
|
||||
def check_dict(
|
||||
required_keys: Collection[Tuple[str, Validator[ResultT]]] = [],
|
||||
optional_keys: Collection[Tuple[str, Validator[ResultT]]] = [],
|
||||
|
|
|
@ -4,6 +4,7 @@ various things (e.g. invitation reminders and welcome emails).
|
|||
|
||||
This management command is run via supervisor.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Any
|
||||
|
|
|
@ -17,6 +17,7 @@ We extract and validate the target stream from information in the
|
|||
recipient address and retrieve, forward, and archive the message.
|
||||
|
||||
"""
|
||||
|
||||
import email
|
||||
import email.policy
|
||||
import logging
|
||||
|
|
|
@ -31,6 +31,7 @@ Credit for the approach goes to:
|
|||
https://stackoverflow.com/questions/2090717
|
||||
|
||||
"""
|
||||
|
||||
import glob
|
||||
import itertools
|
||||
import json
|
||||
|
|
|
@ -136,9 +136,11 @@ To learn more about the test itself, see zerver/openapi/test_curl_examples.py.
|
|||
file_name=file_name,
|
||||
line=line,
|
||||
curl_command=generated_curl_command,
|
||||
response=response_json
|
||||
if response is None
|
||||
else json.dumps(response, indent=4),
|
||||
response=(
|
||||
response_json
|
||||
if response is None
|
||||
else json.dumps(response, indent=4)
|
||||
),
|
||||
)
|
||||
)
|
||||
raise
|
||||
|
|
|
@ -70,9 +70,9 @@ class DecoratorTestCase(ZulipTestCase):
|
|||
self.assertEqual(parse_client(req), ("Unspecified", None))
|
||||
|
||||
req = HostRequestMock()
|
||||
req.META[
|
||||
"HTTP_USER_AGENT"
|
||||
] = "ZulipElectron/4.0.3 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Zulip/4.0.3 Chrome/66.0.3359.181 Electron/3.1.10 Safari/537.36"
|
||||
req.META["HTTP_USER_AGENT"] = (
|
||||
"ZulipElectron/4.0.3 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Zulip/4.0.3 Chrome/66.0.3359.181 Electron/3.1.10 Safari/537.36"
|
||||
)
|
||||
self.assertEqual(parse_client(req), ("ZulipElectron", "4.0.3"))
|
||||
|
||||
req = HostRequestMock()
|
||||
|
@ -89,23 +89,23 @@ class DecoratorTestCase(ZulipTestCase):
|
|||
|
||||
# TODO: This should ideally be Firefox.
|
||||
req = HostRequestMock()
|
||||
req.META[
|
||||
"HTTP_USER_AGENT"
|
||||
] = "Mozilla/5.0 (X11; Linux x86_64; rv:73.0) Gecko/20100101 Firefox/73.0"
|
||||
req.META["HTTP_USER_AGENT"] = (
|
||||
"Mozilla/5.0 (X11; Linux x86_64; rv:73.0) Gecko/20100101 Firefox/73.0"
|
||||
)
|
||||
self.assertEqual(parse_client(req), ("Mozilla", None))
|
||||
|
||||
# TODO: This should ideally be Chrome.
|
||||
req = HostRequestMock()
|
||||
req.META[
|
||||
"HTTP_USER_AGENT"
|
||||
] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.43 Safari/537.36"
|
||||
req.META["HTTP_USER_AGENT"] = (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.43 Safari/537.36"
|
||||
)
|
||||
self.assertEqual(parse_client(req), ("Mozilla", None))
|
||||
|
||||
# TODO: This should ideally be Mobile Safari if we had better user-agent parsing.
|
||||
req = HostRequestMock()
|
||||
req.META[
|
||||
"HTTP_USER_AGENT"
|
||||
] = "Mozilla/5.0 (Linux; Android 8.0.0; SM-G930F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Mobile Safari/537.36"
|
||||
req.META["HTTP_USER_AGENT"] = (
|
||||
"Mozilla/5.0 (Linux; Android 8.0.0; SM-G930F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Mobile Safari/537.36"
|
||||
)
|
||||
self.assertEqual(parse_client(req), ("Mozilla", None))
|
||||
|
||||
post_req_with_client = HostRequestMock()
|
||||
|
|
|
@ -490,9 +490,9 @@ and other things
|
|||
incoming_valid_message = EmailMessage()
|
||||
incoming_valid_message.set_content("TestStreamEmailMessages body")
|
||||
incoming_valid_message["Subject"] = "TestStreamEmailMessages subject"
|
||||
incoming_valid_message[
|
||||
"From"
|
||||
] = "Test =?utf-8?b?VXNlcsOzxIXEmQ==?= <=?utf-8?q?hamlet=5F=C4=99?=@zulip.com>"
|
||||
incoming_valid_message["From"] = (
|
||||
"Test =?utf-8?b?VXNlcsOzxIXEmQ==?= <=?utf-8?q?hamlet=5F=C4=99?=@zulip.com>"
|
||||
)
|
||||
incoming_valid_message["To"] = stream_to_address
|
||||
incoming_valid_message["Reply-to"] = self.example_email("othello")
|
||||
|
||||
|
|
|
@ -744,9 +744,11 @@ class PreviewTestCase(ZulipTestCase):
|
|||
|
||||
# HTML with a bad og:image metadata
|
||||
html = "\n".join(
|
||||
line
|
||||
if "og:image" not in line
|
||||
else '<meta property="og:image" content="http://[bad url/" />'
|
||||
(
|
||||
line
|
||||
if "og:image" not in line
|
||||
else '<meta property="og:image" content="http://[bad url/" />'
|
||||
)
|
||||
for line in self.open_graph_html.splitlines()
|
||||
)
|
||||
self.create_mock_response(url, body=html)
|
||||
|
|
|
@ -1180,7 +1180,7 @@ class TestMessageNotificationEmails(ZulipTestCase):
|
|||
f"http://zulip.testserver/user_avatars/{realm.id}/emoji/images/{realm_emoji_id}.png"
|
||||
)
|
||||
verify_body_include = [
|
||||
f'<img alt=":green_tick:" src="{realm_emoji_url}" style="height: 20px;" title="green tick">'
|
||||
f'<img alt=":green_tick:" src="{realm_emoji_url}" title="green tick" style="height: 20px;">'
|
||||
]
|
||||
email_subject = "DMs with Othello, the Moor of Venice"
|
||||
self._test_cases(
|
||||
|
@ -1200,7 +1200,7 @@ class TestMessageNotificationEmails(ZulipTestCase):
|
|||
"Extremely personal message with a hamburger :hamburger:!",
|
||||
)
|
||||
verify_body_include = [
|
||||
'<img alt=":hamburger:" src="http://testserver/static/generated/emoji/images-twitter-64/1f354.png" style="height: 20px;" title="hamburger">'
|
||||
'<img alt=":hamburger:" src="http://testserver/static/generated/emoji/images-twitter-64/1f354.png" title="hamburger" style="height: 20px;">'
|
||||
]
|
||||
email_subject = "DMs with Othello, the Moor of Venice"
|
||||
self._test_cases(
|
||||
|
@ -1219,7 +1219,7 @@ class TestMessageNotificationEmails(ZulipTestCase):
|
|||
stream_id = get_stream("Verona", get_realm("zulip")).id
|
||||
href = f"http://zulip.testserver/#narrow/stream/{stream_id}-Verona"
|
||||
verify_body_include = [
|
||||
f'<a class="stream" data-stream-id="{stream_id}" href="{href}">#Verona</a'
|
||||
f'<a class="stream" href="{href}" data-stream-id="{stream_id}">#Verona</a'
|
||||
]
|
||||
email_subject = "DMs with Othello, the Moor of Venice"
|
||||
self._test_cases(
|
||||
|
|
|
@ -78,9 +78,9 @@ class RealmFilterTest(ZulipTestCase):
|
|||
self.assertIsNotNone(re.match(data["pattern"], "_code=123abcdZ"))
|
||||
|
||||
data["pattern"] = r"PR (?P<id>[0-9]+)"
|
||||
data[
|
||||
"url_template"
|
||||
] = "https://example.com/~user/web#view_type=type&model=model&action=12345&id={id}"
|
||||
data["url_template"] = (
|
||||
"https://example.com/~user/web#view_type=type&model=model&action=12345&id={id}"
|
||||
)
|
||||
result = self.client_post("/json/realm/filters", info=data)
|
||||
self.assert_json_success(result)
|
||||
self.assertIsNotNone(re.match(data["pattern"], "PR 123"))
|
||||
|
@ -141,12 +141,12 @@ class RealmFilterTest(ZulipTestCase):
|
|||
self.assert_json_success(result)
|
||||
self.assertIsNotNone(re.match(data["pattern"], "zulip/zulip#123"))
|
||||
|
||||
data[
|
||||
"pattern"
|
||||
] = r"FOO_(?P<id>[a-f]{5});(?P<zone>[a-f]);(?P<domain>[a-z]+);(?P<location>[a-z]+);(?P<name>[a-z]{2,8});(?P<chapter>[0-9]{2,3});(?P<fragment>[a-z]{2,8})"
|
||||
data[
|
||||
"url_template"
|
||||
] = "https://zone_{zone}{.domain}.net/ticket{/location}{/id}{?name,chapter}{#fragment:5}"
|
||||
data["pattern"] = (
|
||||
r"FOO_(?P<id>[a-f]{5});(?P<zone>[a-f]);(?P<domain>[a-z]+);(?P<location>[a-z]+);(?P<name>[a-z]{2,8});(?P<chapter>[0-9]{2,3});(?P<fragment>[a-z]{2,8})"
|
||||
)
|
||||
data["url_template"] = (
|
||||
"https://zone_{zone}{.domain}.net/ticket{/location}{/id}{?name,chapter}{#fragment:5}"
|
||||
)
|
||||
result = self.client_post("/json/realm/filters", info=data)
|
||||
self.assert_json_success(result)
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import os
|
|||
from unittest.mock import Mock, patch
|
||||
|
||||
from django.conf import settings
|
||||
from moto.s3 import mock_s3
|
||||
from moto.core.decorator import mock_aws
|
||||
|
||||
from zerver.actions.realm_emoji import check_add_realm_emoji
|
||||
from zerver.lib.avatar_hash import user_avatar_path
|
||||
|
@ -35,7 +35,7 @@ class TransferUploadsToS3Test(ZulipTestCase):
|
|||
m2.assert_called_with(4)
|
||||
m3.assert_called_with(4)
|
||||
|
||||
@mock_s3
|
||||
@mock_aws
|
||||
def test_transfer_avatars_to_s3(self) -> None:
|
||||
bucket = create_s3_buckets(settings.S3_AVATAR_BUCKET)[0]
|
||||
|
||||
|
@ -61,7 +61,7 @@ class TransferUploadsToS3Test(ZulipTestCase):
|
|||
with open(avatar_disk_path(user, medium=True), "rb") as f:
|
||||
self.assertEqual(medium_image_key.get()["Body"].read(), f.read())
|
||||
|
||||
@mock_s3
|
||||
@mock_aws
|
||||
def test_transfer_message_files(self) -> None:
|
||||
bucket = create_s3_buckets(settings.S3_AUTH_UPLOADS_BUCKET)[0]
|
||||
hamlet = self.example_user("hamlet")
|
||||
|
@ -79,7 +79,7 @@ class TransferUploadsToS3Test(ZulipTestCase):
|
|||
self.assertEqual(bucket.Object(attachments[0].path_id).get()["Body"].read(), b"zulip1!")
|
||||
self.assertEqual(bucket.Object(attachments[1].path_id).get()["Body"].read(), b"zulip2!")
|
||||
|
||||
@mock_s3
|
||||
@mock_aws
|
||||
def test_transfer_emoji_to_s3(self) -> None:
|
||||
bucket = create_s3_buckets(settings.S3_AVATAR_BUCKET)[0]
|
||||
othello = self.example_user("othello")
|
||||
|
|
|
@ -50,8 +50,7 @@ class TestEndpoint(ZulipTestCase):
|
|||
|
||||
def test_coerce(self) -> None:
|
||||
@typed_endpoint
|
||||
def view(request: HttpRequest, *, strict_int: int) -> None:
|
||||
...
|
||||
def view(request: HttpRequest, *, strict_int: int) -> None: ...
|
||||
|
||||
with self.assertRaisesMessage(JsonableError, "strict_int is not an integer"):
|
||||
call_endpoint(view, HostRequestMock({"strict_int": orjson.dumps("10").decode()}))
|
||||
|
@ -322,8 +321,7 @@ class TestEndpoint(ZulipTestCase):
|
|||
request: HttpRequest,
|
||||
*,
|
||||
path_var_default: PathOnly[str] = "test",
|
||||
) -> None:
|
||||
...
|
||||
) -> None: ...
|
||||
|
||||
with self.assertRaisesMessage(
|
||||
AssertionError, "Path-only parameter path_var_default should not have a default value"
|
||||
|
@ -346,8 +344,7 @@ class TestEndpoint(ZulipTestCase):
|
|||
],
|
||||
paz: PathOnly[int],
|
||||
other: str,
|
||||
) -> None:
|
||||
...
|
||||
) -> None: ...
|
||||
|
||||
from zerver.lib.request import arguments_map
|
||||
|
||||
|
@ -365,16 +362,14 @@ class TestEndpoint(ZulipTestCase):
|
|||
Json[int],
|
||||
ApiParamConfig(path_only=True),
|
||||
],
|
||||
) -> None:
|
||||
...
|
||||
) -> None: ...
|
||||
|
||||
def annotated_with_repeated_api_param_config(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
*,
|
||||
foo: Annotated[Json[int], ApiParamConfig(), ApiParamConfig()],
|
||||
) -> None:
|
||||
...
|
||||
) -> None: ...
|
||||
|
||||
with self.assertRaisesMessage(
|
||||
AssertionError, "ApiParamConfig can only be defined once per parameter"
|
||||
|
@ -408,8 +403,7 @@ class TestEndpoint(ZulipTestCase):
|
|||
StringConstraints(strip_whitespace=True, max_length=3),
|
||||
ApiParamConfig("test"),
|
||||
] = None,
|
||||
) -> None:
|
||||
...
|
||||
) -> None: ...
|
||||
|
||||
with self.assertRaisesMessage(ApiParamValidationError, "test is too long"):
|
||||
call_endpoint(no_nesting, HostRequestMock({"test": "long"}))
|
||||
|
@ -510,11 +504,9 @@ class TestEndpoint(ZulipTestCase):
|
|||
)
|
||||
|
||||
def test_expect_no_parameters(self) -> None:
|
||||
def no_parameter(request: HttpRequest) -> None:
|
||||
...
|
||||
def no_parameter(request: HttpRequest) -> None: ...
|
||||
|
||||
def has_parameters(request: HttpRequest, *, foo: int, bar: str) -> None:
|
||||
...
|
||||
def has_parameters(request: HttpRequest, *, foo: int, bar: str) -> None: ...
|
||||
|
||||
with self.assertRaisesRegex(AssertionError, "there is no keyword-only parameter found"):
|
||||
typed_endpoint(no_parameter)
|
||||
|
@ -551,13 +543,11 @@ class TestEndpoint(ZulipTestCase):
|
|||
# all. The only possible way for val to be None is through the default
|
||||
# value (if it has one).
|
||||
@typed_endpoint
|
||||
def foo(request: HttpRequest, *, val: Optional[Json[int]]) -> None:
|
||||
...
|
||||
def foo(request: HttpRequest, *, val: Optional[Json[int]]) -> None: ...
|
||||
|
||||
# Json[Optional[int]] however, allows client specified None value.
|
||||
@typed_endpoint
|
||||
def bar(request: HttpRequest, *, val: Json[Optional[int]]) -> None:
|
||||
...
|
||||
def bar(request: HttpRequest, *, val: Json[Optional[int]]) -> None: ...
|
||||
|
||||
with self.assertRaisesMessage(ApiParamValidationError, "val is not an integer"):
|
||||
call_endpoint(foo, HostRequestMock({"val": orjson.dumps(None).decode()}))
|
||||
|
@ -640,8 +630,7 @@ class ValidationErrorHandlingTest(ZulipTestCase):
|
|||
input_type: Any = subtest.param_type
|
||||
|
||||
@typed_endpoint
|
||||
def func(request: HttpRequest, *, input: input_type) -> None:
|
||||
...
|
||||
def func(request: HttpRequest, *, input: input_type) -> None: ...
|
||||
|
||||
with self.assertRaises(ApiParamValidationError) as m:
|
||||
call_endpoint(func, HostRequestMock({"input": subtest.input_data}))
|
||||
|
|
|
@ -90,9 +90,9 @@ def accounts_accept_terms(request: HttpRequest) -> HttpResponse:
|
|||
request.user.tos_version == UserProfile.TOS_VERSION_BEFORE_FIRST_LOGIN
|
||||
and settings.FIRST_TIME_TERMS_OF_SERVICE_TEMPLATE is not None
|
||||
):
|
||||
context[
|
||||
"first_time_terms_of_service_message_template"
|
||||
] = settings.FIRST_TIME_TERMS_OF_SERVICE_TEMPLATE
|
||||
context["first_time_terms_of_service_message_template"] = (
|
||||
settings.FIRST_TIME_TERMS_OF_SERVICE_TEMPLATE
|
||||
)
|
||||
|
||||
return render(
|
||||
request,
|
||||
|
|
|
@ -247,9 +247,9 @@ def send_message_backend(
|
|||
)
|
||||
data["id"] = sent_message_result.message_id
|
||||
if sent_message_result.automatic_new_visibility_policy:
|
||||
data[
|
||||
"automatic_new_visibility_policy"
|
||||
] = sent_message_result.automatic_new_visibility_policy
|
||||
data["automatic_new_visibility_policy"] = (
|
||||
sent_message_result.automatic_new_visibility_policy
|
||||
)
|
||||
return json_success(request, data=data)
|
||||
|
||||
|
||||
|
|
|
@ -288,9 +288,9 @@ def update_realm(
|
|||
)
|
||||
|
||||
if setting_value_changed:
|
||||
data[
|
||||
"move_messages_within_stream_limit_seconds"
|
||||
] = move_messages_within_stream_limit_seconds
|
||||
data["move_messages_within_stream_limit_seconds"] = (
|
||||
move_messages_within_stream_limit_seconds
|
||||
)
|
||||
|
||||
move_messages_between_streams_limit_seconds: Optional[int] = None
|
||||
if move_messages_between_streams_limit_seconds_raw is not None:
|
||||
|
@ -305,9 +305,9 @@ def update_realm(
|
|||
)
|
||||
|
||||
if setting_value_changed:
|
||||
data[
|
||||
"move_messages_between_streams_limit_seconds"
|
||||
] = move_messages_between_streams_limit_seconds
|
||||
data["move_messages_between_streams_limit_seconds"] = (
|
||||
move_messages_between_streams_limit_seconds
|
||||
)
|
||||
|
||||
jitsi_server_url: Optional[str] = None
|
||||
if jitsi_server_url_raw is not None:
|
||||
|
|
|
@ -188,20 +188,20 @@ def reactivate_user_backend(
|
|||
return json_success(request)
|
||||
|
||||
|
||||
check_profile_data: Validator[
|
||||
List[Dict[str, Optional[Union[int, ProfileDataElementValue]]]]
|
||||
] = check_list(
|
||||
check_dict_only(
|
||||
[
|
||||
("id", check_int),
|
||||
(
|
||||
"value",
|
||||
check_none_or(
|
||||
check_union([check_string, check_list(check_int)]),
|
||||
check_profile_data: Validator[List[Dict[str, Optional[Union[int, ProfileDataElementValue]]]]] = (
|
||||
check_list(
|
||||
check_dict_only(
|
||||
[
|
||||
("id", check_int),
|
||||
(
|
||||
"value",
|
||||
check_none_or(
|
||||
check_union([check_string, check_list(check_int)]),
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -24,9 +24,11 @@ def api_bitbucket_webhook(
|
|||
|
||||
commits = [
|
||||
{
|
||||
"name": commit["author"].tame(check_string)
|
||||
if "author" in commit
|
||||
else payload.get("user", "Someone").tame(check_string),
|
||||
"name": (
|
||||
commit["author"].tame(check_string)
|
||||
if "author" in commit
|
||||
else payload.get("user", "Someone").tame(check_string)
|
||||
),
|
||||
"sha": commit["raw_node"].tame(check_string),
|
||||
"message": commit["message"].tame(check_string),
|
||||
"url": "{}{}commits/{}".format(
|
||||
|
|
|
@ -201,8 +201,7 @@ def get_type(request: HttpRequest, payload: WildValue) -> str:
|
|||
|
||||
|
||||
class BodyGetter(Protocol):
|
||||
def __call__(self, request: HttpRequest, payload: WildValue, include_title: bool) -> str:
|
||||
...
|
||||
def __call__(self, request: HttpRequest, payload: WildValue, include_title: bool) -> str: ...
|
||||
|
||||
|
||||
def get_body_based_on_type(
|
||||
|
@ -372,12 +371,16 @@ def get_pull_request_created_or_updated_body(
|
|||
action=action,
|
||||
url=get_pull_request_url(pull_request),
|
||||
number=pull_request["id"].tame(check_int),
|
||||
target_branch=pull_request["source"]["branch"]["name"].tame(check_string)
|
||||
if action == "created"
|
||||
else None,
|
||||
base_branch=pull_request["destination"]["branch"]["name"].tame(check_string)
|
||||
if action == "created"
|
||||
else None,
|
||||
target_branch=(
|
||||
pull_request["source"]["branch"]["name"].tame(check_string)
|
||||
if action == "created"
|
||||
else None
|
||||
),
|
||||
base_branch=(
|
||||
pull_request["destination"]["branch"]["name"].tame(check_string)
|
||||
if action == "created"
|
||||
else None
|
||||
),
|
||||
message=pull_request["description"].tame(check_string),
|
||||
assignee=assignee,
|
||||
title=pull_request["title"].tame(check_string) if include_title else None,
|
||||
|
|
|
@ -393,8 +393,7 @@ def pr_comment_handler(
|
|||
class EventHandler(Protocol):
|
||||
def __call__(
|
||||
self, payload: WildValue, branches: Optional[str], include_title: Optional[str]
|
||||
) -> List[Dict[str, str]]:
|
||||
...
|
||||
) -> List[Dict[str, str]]: ...
|
||||
|
||||
|
||||
EVENT_HANDLER_MAP: Dict[str, EventHandler] = {
|
||||
|
|
|
@ -398,9 +398,11 @@ def get_story_create_github_entity_body(payload: WildValue, action: WildValue, e
|
|||
name=action["name"].tame(check_string),
|
||||
app_url=action["app_url"].tame(check_string),
|
||||
),
|
||||
"name": pull_request_action["number"].tame(check_int)
|
||||
if entity in ("pull-request", "pull-request-comment")
|
||||
else pull_request_action["name"].tame(check_string),
|
||||
"name": (
|
||||
pull_request_action["number"].tame(check_int)
|
||||
if entity in ("pull-request", "pull-request-comment")
|
||||
else pull_request_action["name"].tame(check_string)
|
||||
),
|
||||
"url": pull_request_action["url"].tame(check_string),
|
||||
"workflow_state_template": "",
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Webhooks for external integrations."""
|
||||
|
||||
from typing import List
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
|
|
|
@ -160,9 +160,11 @@ def get_issue_body(helper: Helper) -> str:
|
|||
action=action,
|
||||
url=issue["html_url"].tame(check_string),
|
||||
number=issue["number"].tame(check_int),
|
||||
message=None
|
||||
if action in ("assigned", "unassigned")
|
||||
else issue["body"].tame(check_none_or(check_string)),
|
||||
message=(
|
||||
None
|
||||
if action in ("assigned", "unassigned")
|
||||
else issue["body"].tame(check_none_or(check_string))
|
||||
),
|
||||
title=issue["title"].tame(check_string) if include_title else None,
|
||||
)
|
||||
|
||||
|
|
|
@ -163,12 +163,12 @@ def get_merge_request_open_or_updated_body(
|
|||
action=action,
|
||||
url=pull_request["url"].tame(check_string),
|
||||
number=pull_request["iid"].tame(check_int),
|
||||
target_branch=pull_request["source_branch"].tame(check_string)
|
||||
if action == "created"
|
||||
else None,
|
||||
base_branch=pull_request["target_branch"].tame(check_string)
|
||||
if action == "created"
|
||||
else None,
|
||||
target_branch=(
|
||||
pull_request["source_branch"].tame(check_string) if action == "created" else None
|
||||
),
|
||||
base_branch=(
|
||||
pull_request["target_branch"].tame(check_string) if action == "created" else None
|
||||
),
|
||||
message=pull_request["description"].tame(check_none_or(check_string)),
|
||||
assignees=replace_assignees_username_with_name(get_assignees(payload)),
|
||||
type="MR",
|
||||
|
@ -379,8 +379,7 @@ def get_object_url(payload: WildValue) -> str:
|
|||
|
||||
|
||||
class EventFunction(Protocol):
|
||||
def __call__(self, payload: WildValue, include_title: bool) -> str:
|
||||
...
|
||||
def __call__(self, payload: WildValue, include_title: bool) -> str: ...
|
||||
|
||||
|
||||
EVENT_FUNCTION_MAPPER: Dict[str, EventFunction] = {
|
||||
|
@ -467,9 +466,11 @@ def get_topic_based_on_event(event: str, payload: WildValue, use_merge_request_t
|
|||
repo=get_repo_name(payload),
|
||||
type="MR",
|
||||
id=payload["object_attributes"]["iid"].tame(check_int),
|
||||
title=payload["object_attributes"]["title"].tame(check_string)
|
||||
if use_merge_request_title
|
||||
else "",
|
||||
title=(
|
||||
payload["object_attributes"]["title"].tame(check_string)
|
||||
if use_merge_request_title
|
||||
else ""
|
||||
),
|
||||
)
|
||||
elif event.startswith(("Issue Hook", "Confidential Issue Hook")):
|
||||
return TOPIC_WITH_PR_OR_ISSUE_INFO_TEMPLATE.format(
|
||||
|
@ -490,9 +491,11 @@ def get_topic_based_on_event(event: str, payload: WildValue, use_merge_request_t
|
|||
repo=get_repo_name(payload),
|
||||
type="MR",
|
||||
id=payload["merge_request"]["iid"].tame(check_int),
|
||||
title=payload["merge_request"]["title"].tame(check_string)
|
||||
if use_merge_request_title
|
||||
else "",
|
||||
title=(
|
||||
payload["merge_request"]["title"].tame(check_string)
|
||||
if use_merge_request_title
|
||||
else ""
|
||||
),
|
||||
)
|
||||
|
||||
elif event == "Note Hook Snippet":
|
||||
|
|
|
@ -173,8 +173,7 @@ def api_gogs_webhook(
|
|||
|
||||
|
||||
class FormatPullRequestEvent(Protocol):
|
||||
def __call__(self, payload: WildValue, include_title: bool) -> str:
|
||||
...
|
||||
def __call__(self, payload: WildValue, include_title: bool) -> str: ...
|
||||
|
||||
|
||||
def gogs_webhook_main(
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Webhooks for external integrations."""
|
||||
|
||||
import re
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ Tips for notification output:
|
|||
value should always be in bold; otherwise the subject of US/task
|
||||
should be in bold.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
|
|
|
@ -966,7 +966,9 @@ class Command(BaseCommand):
|
|||
zulip_stream_dict: Dict[str, Dict[str, Any]] = {
|
||||
"devel": {"description": "For developing"},
|
||||
# ビデオゲーム - VideoGames (japanese)
|
||||
"ビデオゲーム": {"description": f"Share your favorite video games! {raw_emojis[2]}"},
|
||||
"ビデオゲーム": {
|
||||
"description": f"Share your favorite video games! {raw_emojis[2]}"
|
||||
},
|
||||
"announce": {
|
||||
"description": "For announcements",
|
||||
"stream_post_policy": Stream.STREAM_POST_POLICY_ADMINS,
|
||||
|
|
|
@ -23,17 +23,11 @@ else:
|
|||
|
||||
|
||||
@overload
|
||||
def get_secret(key: str, default_value: str, development_only: bool = False) -> str:
|
||||
...
|
||||
|
||||
|
||||
def get_secret(key: str, default_value: str, development_only: bool = False) -> str: ...
|
||||
@overload
|
||||
def get_secret(
|
||||
key: str, default_value: Optional[str] = None, development_only: bool = False
|
||||
) -> Optional[str]:
|
||||
...
|
||||
|
||||
|
||||
) -> Optional[str]: ...
|
||||
def get_secret(
|
||||
key: str, default_value: Optional[str] = None, development_only: bool = False
|
||||
) -> Optional[str]:
|
||||
|
@ -52,15 +46,9 @@ def get_mandatory_secret(key: str) -> str:
|
|||
|
||||
|
||||
@overload
|
||||
def get_config(section: str, key: str, default_value: str) -> str:
|
||||
...
|
||||
|
||||
|
||||
def get_config(section: str, key: str, default_value: str) -> str: ...
|
||||
@overload
|
||||
def get_config(section: str, key: str, default_value: Optional[str] = None) -> Optional[str]:
|
||||
...
|
||||
|
||||
|
||||
def get_config(section: str, key: str, default_value: Optional[str] = None) -> Optional[str]: ...
|
||||
def get_config(section: str, key: str, default_value: Optional[str] = None) -> Optional[str]:
|
||||
return config_file.get(section, key, fallback=default_value)
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ middleware here, or combine a Django application with an application of another
|
|||
framework.
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
|
Loading…
Reference in New Issue