Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ known_first_party = ['warehouse', 'tests']
python_version = "3.11"
namespace_packages = true
warn_unused_configs = true
plugins = ["mypy_zope:plugin"]
# Plugin order matters. `mypy_zope` must come before `sqlalchemy` to prevent
# false-positive signature mismatch errors for `zope.interface` instances.
plugins = ["mypy_zope:plugin", "sqlalchemy.ext.mypy.plugin"]
exclude = ["warehouse/locale/.*", "warehouse/migrations/versions.*"]

[[tool.mypy.overrides]]
Expand All @@ -24,7 +26,6 @@ module = [
"b2sdk.*", # https://github.com/Backblaze/b2-sdk-python/issues/148
"celery.app.backends.*",
"celery.backends.redis.*",
"citext.*",
"disposable_email_domains.*",
"elasticsearch_dsl.*", # https://github.com/elastic/elasticsearch-dsl-py/issues/1533
"github_reserved_names.*",
Expand All @@ -46,7 +47,6 @@ module = [
"pyqrcode.*",
"requests_aws4auth.*", # https://github.com/tedder/requests-aws4auth/issues/53
"rfc3986.*",
"sqlalchemy.*", # https://docs.sqlalchemy.org/en/14/orm/extensions/mypy.html
"transaction.*",
"ua_parser.*", # https://github.com/ua-parser/uap-python/issues/110
"venusian.*",
Expand All @@ -70,5 +70,4 @@ filterwarnings = [
'ignore::warehouse.admin.services.InsecureStorageWarning',
'ignore::warehouse.utils.exceptions.InsecureOIDCPublisherWarning',
'ignore::warehouse.packaging.services.InsecureStorageWarning',
'ignore:UserDefinedType CIText.*:sqlalchemy.exc.SAWarning' # See https://github.com/mahmoudimus/sqlalchemy-citext/issues/25
]
3 changes: 1 addition & 2 deletions requirements/main.in
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,7 @@ redis>=2.8.0,<5.0.0
rfc3986
sentry-sdk
setuptools
sqlalchemy[asyncio]>=0.9,<1.5.0 # https://github.com/pypi/warehouse/pull/9228
sqlalchemy-citext
sqlalchemy[asyncio]>=2.0,<3.0
stdlib-list
stripe
structlog
Expand Down
89 changes: 43 additions & 46 deletions requirements/main.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1411,57 +1411,53 @@ six==1.16.0 \
# pymacaroons
# python-dateutil
# requests-aws4auth
sqlalchemy[asyncio]==1.4.48 \
--hash=sha256:005e942b451cad5285015481ae4e557ff4154dde327840ba91b9ac379be3b6ce \
--hash=sha256:066c2b0413e8cb980e6d46bf9d35ca83be81c20af688fedaef01450b06e4aa5e \
--hash=sha256:0817c181271b0ce5df1aa20949f0a9e2426830fed5ecdcc8db449618f12c2730 \
--hash=sha256:11c6b1de720f816c22d6ad3bbfa2f026f89c7b78a5c4ffafb220e0183956a92a \
--hash=sha256:1fd8b5ee5a3acc4371f820934b36f8109ce604ee73cc668c724abb054cebcb6e \
--hash=sha256:25887b4f716e085a1c5162f130b852f84e18d2633942c8ca40dfb8519367c14f \
--hash=sha256:2b562e9d1e59be7833edf28b0968f156683d57cabd2137d8121806f38a9d58f4 \
--hash=sha256:2b9af65cc58726129d8414fc1a1a650dcdd594ba12e9c97909f1f57d48e393d3 \
--hash=sha256:2ee26276f12614d47cc07bc85490a70f559cba965fb178b1c45d46ffa8d73fda \
--hash=sha256:2fc2ab4d9f6d9218a5caa4121bdcf1125303482a1cdcfcdbd8567be8518969c0 \
--hash=sha256:3509159e050bd6d24189ec7af373359f07aed690db91909c131e5068176c5a5d \
--hash=sha256:4355e5915844afdc5cf22ec29fba1010166e35dd94a21305f49020022167556b \
--hash=sha256:44d29a3fc6d9c45962476b470a81983dd8add6ad26fdbfae6d463b509d5adcda \
--hash=sha256:49c312bcff4728bffc6fb5e5318b8020ed5c8b958a06800f91859fe9633ca20e \
--hash=sha256:4bac3aa3c3d8bc7408097e6fe8bf983caa6e9491c5d2e2488cfcfd8106f13b6a \
--hash=sha256:5381ddd09a99638f429f4cbe1b71b025bed318f6a7b23e11d65f3eed5e181c33 \
--hash=sha256:627e04a5d54bd50628fc8734d5fc6df2a1aa5962f219c44aad50b00a6cdcf965 \
--hash=sha256:68413aead943883b341b2b77acd7a7fe2377c34d82e64d1840860247cec7ff7c \
--hash=sha256:6dab89874e72a9ab5462997846d4c760cdb957958be27b03b49cf0de5e5c327c \
--hash=sha256:6f82d8efea1ca92b24f51d3aea1a82897ed2409868a0af04247c8c1e4fef5890 \
--hash=sha256:7ad2b0f6520ed5038e795cc2852eb5c1f20fa6831d73301ced4aafbe3a10e1f6 \
--hash=sha256:87609f6d4e81a941a17e61a4c19fee57f795e96f834c4f0a30cee725fc3f81d9 \
--hash=sha256:92e6133cf337c42bfee03ca08c62ba0f2d9695618c8abc14a564f47503157be9 \
--hash=sha256:9af1db7a287ef86e0f5cd990b38da6bd9328de739d17e8864f1817710da2d217 \
--hash=sha256:9c8cfe951ed074ba5e708ed29c45397a95c4143255b0d022c7c8331a75ae61f3 \
--hash=sha256:9d9b55252d2ca42a09bcd10a697fa041e696def9dfab0b78c0aaea1485551a08 \
--hash=sha256:a1fc046756cf2a37d7277c93278566ddf8be135c6a58397b4c940abf837011f4 \
--hash=sha256:b47bc287096d989a0838ce96f7d8e966914a24da877ed41a7531d44b55cdb8df \
--hash=sha256:c99bf13e07140601d111a7c6f1fc1519914dd4e5228315bbda255e08412f61a4 \
--hash=sha256:cbbe8b8bffb199b225d2fe3804421b7b43a0d49983f81dc654d0431d2f855543 \
--hash=sha256:ce7915eecc9c14a93b73f4e1c9d779ca43e955b43ddf1e21df154184f39748e5 \
--hash=sha256:cef2e2abc06eab187a533ec3e1067a71d7bbec69e582401afdf6d8cad4ba3515 \
--hash=sha256:d53cd8bc582da5c1c8c86b6acc4ef42e20985c57d0ebc906445989df566c5603 \
--hash=sha256:dbcae0e528d755f4522cad5842f0942e54b578d79f21a692c44d91352ea6d64e \
--hash=sha256:e1ddbbcef9bcedaa370c03771ebec7e39e3944782bef49e69430383c376a250b \
--hash=sha256:e3e98d4907805b07743b583a99ecc58bf8807ecb6985576d82d5e8ae103b5272 \
--hash=sha256:eb5464ee8d4bb6549d368b578e9529d3c43265007193597ddca71c1bae6174e6 \
--hash=sha256:eee09350fd538e29cfe3a496ec6f148504d2da40dbf52adefb0d2f8e4d38ccc4 \
--hash=sha256:fb0808ad34167f394fea21bd4587fc62f3bd81bba232a1e7fbdfa17e6cfa7cd7 \
--hash=sha256:fbde5642104ac6e95f96e8ad6d18d9382aa20672008cf26068fe36f3004491df \
--hash=sha256:fe1dd2562313dd9fe1778ed56739ad5d9aae10f9f43d9f4cf81d65b0c85168bb
sqlalchemy[asyncio]==2.0.16 \
--hash=sha256:0db6734cb5644c55d0262a813b764c6e2cda1e66e939a488b3d6298cdc7344c2 \
--hash=sha256:0e4645b260cfe375a0603aa117f0a47680864cf37833129da870919e88b08d8f \
--hash=sha256:131f0c894c6572cb1bdcf97c92d999d3128c4ff1ca13061296057072f61afe13 \
--hash=sha256:1e2caba78e7d1f5003e88817b7a1754d4e58f4a8f956dc423bf8e304c568ab09 \
--hash=sha256:2de1477af7f48c633b8ecb88245aedd811dca88e88aee9e9d787b388abe74c44 \
--hash=sha256:2f3b6c31b915159b96b68372212fa77f69230b0a32acab40cf539d2823954f5a \
--hash=sha256:3ef876615ff4b53e2033022195830ec4941a6e21068611f8d77de60203b90a98 \
--hash=sha256:43e69c8c1cea0188b7094e22fb93ae1a1890aac748628b7e925024a206f75368 \
--hash=sha256:53081c6fce0d49bb36d05f12dc87e008c9b0df58a163b792c5fc4ac638925f98 \
--hash=sha256:5a934eff1a2882137be3384826f997db8441d43b61fda3094923e69fffe474be \
--hash=sha256:5e8522b49e0e640287308b68f71cc338446bbe1c226c8f81743baa91b0246e92 \
--hash=sha256:61f2035dea56ff1a429077e481496f813378beb02b823d2e3e7eb05bc1a7a8ca \
--hash=sha256:63ea36c08792a7a8a08958bc806ecff6b491386feeaf14607c3d9d2d9325e67f \
--hash=sha256:6e85e315725807c127ad8ba3d628fdb861cf9ebfb0e10c39a97c01e257cdd71b \
--hash=sha256:7641f6ed2682de84d77c4894cf2e43700f3cf7a729361d7f9cac98febf3d8614 \
--hash=sha256:7be04dbe3470fe8dd332fdb48c979887c381ef6c635eddf2dec43d2766111be4 \
--hash=sha256:81d867c1be5abd49f7e547c108391f371a9d980ba7ec34666c50d683f782b754 \
--hash=sha256:8544c6e62eacb77d5106e2055ef10f2407fc0dbd547e879f8745b2032eefd2bc \
--hash=sha256:8d3cbdb2f07fb0e4b897dc1df39166735e194fb946f28f26f4c9f9801c8b24f7 \
--hash=sha256:8d6ef848e5afcd1bda3e9a843751f845c0ca888b61e669237680e913d84ec206 \
--hash=sha256:8e2569dac4e3cb85365b91ab569d06a221e0e17e65ce59949d00c3958946282b \
--hash=sha256:90d320fde566b864adbc19abb40ecb80f4e25d6f084639969bb972d5cca16858 \
--hash=sha256:91eb8f89fcce8f709f8a4d65d265bc48a80264ee14c7c9e955f3222f19b4b39c \
--hash=sha256:a08a791c75d6154d46914d1e23bd81d9455f2950ec1de81f2723848c593d2c8b \
--hash=sha256:a2e9f50a906d0b81292576a9fb458f8cace904c81a67088f4a2ca9ff2856f55d \
--hash=sha256:a5a2856e12cf5f54301ddf043bcbf0552561d61555e1bcf348b63f42b8e1eec2 \
--hash=sha256:b2801f85c5c0293aa710f8aa5262c707a83c1c203962ae5a22b4d9095e71aa9d \
--hash=sha256:b72f4e4def50414164a1d899f2ce4e782a029fad0ed5585981d1611e8ae29a74 \
--hash=sha256:bdaf89dd82f4a0e1b8b5ffc9cdc0c9551be6175f7eee5af6a838e92ba2e57100 \
--hash=sha256:c5e333b81fe10d14efebd4e9429b7bb865ed9463ca8bef07a7136dfa1fd4a37b \
--hash=sha256:ce1fc3f64fd42d5f763d6b83651471f32920338a1ba107a3186211474861af57 \
--hash=sha256:d0c96592f54edd571e00ba6b1ed5df8263328ca1da9e78088c0ebc93c2e6562c \
--hash=sha256:dc97238fa44be86971270943a0c21c19ce18b8d1596919048e57912e8abc02cc \
--hash=sha256:e19546924f0cf2ec930d1faf318b7365e5827276410a513340f31a2b423e96a4 \
--hash=sha256:f2938edc512dd1fa48653e14c1655ab46144d4450f0e6b33da7acd8ba77fbfd7 \
--hash=sha256:f387b496a4c9474d8580195bb2660264a3f295a04d3a9d00f4fa15e9e597427e \
--hash=sha256:f409f35a0330ab0cb18ece736b86d8b8233c64f4461fcb10993f67afc0ac7e5a \
--hash=sha256:f662cf69484c59f8a3435902c40dfc34d86050bdb15e23d437074ce9f153306b \
--hash=sha256:fbcc51fdbc89fafe4f4fe66f59372a8be88ded04de34ef438ab04f980beb12d4 \
--hash=sha256:fc1dae11bd5167f9eb53b3ccad24a79813004612141e76de21cf4c028dc30b34 \
--hash=sha256:ff6496ad5e9dc8baeb93a151cc2f599d01e5f8928a2aaf0b09a06428fdbaf553
# via
# -r requirements/main.in
# alembic
# paginate-sqlalchemy
# sqlalchemy-citext
# zope-sqlalchemy
sqlalchemy-citext==1.8.0 \
--hash=sha256:a1740e693a9a334e7c8f60ae731083fe75ce6c1605bb9ca6644a6f1f63b15b77
# via -r requirements/main.in
stdlib-list==0.8.0 \
--hash=sha256:2ae0712a55b68f3fbbc9e58d6fa1b646a062188f49745b495f94d3310a9fdd3e \
--hash=sha256:a1e503719720d71e2ed70ed809b385c60cd3fb555ba7ec046b96360d30b16d9f
Expand Down Expand Up @@ -1514,6 +1510,7 @@ typing-extensions==4.6.3 \
# alembic
# limits
# pydantic
# sqlalchemy
ua-parser==0.16.1 \
--hash=sha256:ed3efc695f475ffe56248c9789b3016247e9c20e3556cfa4d5aadc78ab4b26c6 \
--hash=sha256:f97126300df8ac0f8f2c9d8559669532d626a1af529265fd253cba56e73ab36e
Expand Down
2 changes: 2 additions & 0 deletions tests/unit/accounts/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,7 @@ def test_is_disabled(self, user_service, disabled, reason):
remote_addr="127.0.0.1",
ip_address=IpAddressFactory.create(),
headers=dict(),
db=pretend.stub(add=lambda *a: None),
)
user = UserFactory.create()
user_service.update_user(user.id, password="foo")
Expand All @@ -496,6 +497,7 @@ def test_updating_password_undisables(self, user_service):
remote_addr="127.0.0.1",
ip_address=IpAddressFactory.create(),
headers=dict(),
db=pretend.stub(add=lambda *a: None),
)
user = UserFactory.create()
user_service.disable_password(
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/cli/test_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,20 +364,20 @@ def test_dbml_command(monkeypatch, cli):


EXPECTED_DBML = """Table _clan {
id varchar [pk, not null, default: `gen_random_uuid()`]
name text [unique, not null]
fetched text [default: `FetchedValue()`, Note: "fetched value"]
for_the_children boolean [default: `True`]
nice varchar
id varchar [pk, not null, default: `gen_random_uuid()`]
Note: "various clans"
}

Table _clan_member {
id varchar [pk, not null, default: `gen_random_uuid()`]
name text [not null]
clan_id varchar
joined datetime [not null, default: `now()`]
departed datetime
id varchar [pk, not null, default: `gen_random_uuid()`]
}

Ref: _clan_member.clan_id > _clan.id
Expand Down
9 changes: 4 additions & 5 deletions warehouse/accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import datetime
import enum

from citext import CIText
from pyramid.authorization import Allow, Authenticated
from sqlalchemy import (
Boolean,
Expand All @@ -32,7 +31,7 @@
select,
sql,
)
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.dialects.postgresql import CITEXT, UUID
from sqlalchemy.exc import NoResultFound
from sqlalchemy.ext.hybrid import hybrid_property

Expand Down Expand Up @@ -71,7 +70,7 @@ class User(SitemapMixin, HasEvents, db.Model):

__repr__ = make_repr("username")

username = Column(CIText, nullable=False, unique=True)
username = Column(CITEXT, nullable=False, unique=True)
name = Column(String(length=100), nullable=False)
password = Column(String(length=128), nullable=False)
password_date = Column(TZDateTime, nullable=True, server_default=sql.func.now())
Expand All @@ -86,7 +85,7 @@ class User(SitemapMixin, HasEvents, db.Model):
hide_avatar = Column(Boolean, nullable=False, server_default=sql.false())
date_joined = Column(DateTime, server_default=sql.func.now())
last_login = Column(TZDateTime, nullable=False, server_default=sql.func.now())
disabled_for = Column(
disabled_for = Column( # type: ignore[var-annotated]
Enum(DisableReason, values_callable=lambda x: [e.value for e in x]),
nullable=True,
)
Expand Down Expand Up @@ -271,7 +270,7 @@ class Email(db.ModelBase):
public = Column(Boolean, nullable=False, server_default=sql.false())

# Deliverability information
unverify_reason = Column(
unverify_reason = Column( # type: ignore[var-annotated]
Enum(UnverifyReasons, values_callable=lambda x: [e.value for e in x]),
nullable=True,
)
Expand Down
13 changes: 6 additions & 7 deletions warehouse/cli/db/dbml.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@

import click

from citext import CIText
from sqlalchemy.dialects.postgresql.array import ARRAY
from sqlalchemy.dialects.postgresql.base import INET, TIMESTAMP, UUID
from sqlalchemy.dialects.postgresql.base import CITEXT, INET, TIMESTAMP, UUID
from sqlalchemy.dialects.postgresql.json import JSONB
from sqlalchemy.sql.schema import ForeignKey, Table
from sqlalchemy.sql.sqltypes import (
Expand Down Expand Up @@ -66,7 +65,7 @@
UUID: "varchar",
INET: "varchar",
JSONB: "text",
CIText: "text",
CITEXT: "text",
TZDateTime: "datetime",
ARRAY: '"string[]"',
TIMESTAMP: "datetime",
Expand Down Expand Up @@ -158,11 +157,11 @@ def extract_table_info(table: Table) -> TableInfo:
raise TypeError(type(column.type))

if column.default is not None:
default = column.default.arg
default = column.default.arg # type: ignore[attr-defined]
elif column.server_default is not None:
match str(type(column.server_default)):
case "<class 'sqlalchemy.sql.schema.DefaultClause'>":
default = column.server_default.arg
default = column.server_default.arg # type: ignore[attr-defined]
case _:
default = column.server_default
else:
Expand All @@ -172,9 +171,9 @@ def extract_table_info(table: Table) -> TableInfo:
"type": SQLALCHEMY_TO_DBML[column_type],
"pk": column.primary_key,
"unique": column.unique,
"nullable": column.nullable,
"nullable": column.nullable, # type: ignore[typeddict-item]
"default": default,
"comment": column.comment,
"comment": column.comment, # type: ignore[typeddict-item]
}
for foreign_key in column.foreign_keys:
table_info["relationships"].append(
Expand Down
8 changes: 4 additions & 4 deletions warehouse/email/ses/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ class EmailMessage(db.Model):
__tablename__ = "ses_emails"

created = Column(DateTime, nullable=False, server_default=sql.func.now())
status = Column(
status = Column( # type: ignore[var-annotated]
Enum(EmailStatuses, values_callable=lambda x: [e.value for e in x]),
nullable=False,
server_default=EmailStatuses.Accepted.value,
Expand Down Expand Up @@ -273,10 +273,10 @@ class Event(db.Model):
)

event_id = Column(Text, nullable=False, unique=True, index=True)
event_type = Column(
event_type = Column( # type: ignore[var-annotated]
Enum(EventTypes, values_callable=lambda x: [e.value for e in x]), nullable=False
)

data = Column(
MutableDict.as_mutable(JSONB), nullable=False, server_default=sql.text("'{}'")
data = Column( # type: ignore[var-annotated]
MutableDict.as_mutable(JSONB), nullable=False, server_default=sql.text("'{}'") # type: ignore[arg-type] # noqa: E501
)
8 changes: 3 additions & 5 deletions warehouse/events/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ def location_info(cls) -> str: # noqa: N805
Otherwise, return the `ip_address` and let its repr decide.
"""
if cls.additional is not None and "geoip_info" in cls.additional:
g = GeoIPInfo(**cls.additional["geoip_info"])
g = GeoIPInfo(**cls.additional["geoip_info"]) # type: ignore[arg-type]
if g.display():
return g.display()

Expand All @@ -190,7 +190,7 @@ def user_agent_info(cls) -> str: # noqa: N805
Dig into `.additional` for `user_agent_info` and return that if it exists.
"""
if cls.additional is not None and "user_agent_info" in cls.additional:
return UserAgentInfo(**cls.additional["user_agent_info"]).display()
return UserAgentInfo(**cls.additional["user_agent_info"]).display() # type: ignore[arg-type] # noqa: E501

return "No User-Agent"

Expand Down Expand Up @@ -221,8 +221,6 @@ def events(cls): # noqa: N805

def record_event(self, *, tag, request: Request, additional=None):
"""Records an Event record on the associated model."""
session = orm.object_session(self)
assert session is not None

# Get-or-create a new IpAddress object
ip_address = request.ip_address
Expand Down Expand Up @@ -270,6 +268,6 @@ def record_event(self, *, tag, request: Request, additional=None):
additional=additional,
)

session.add(event)
request.db.add(event)

return event
6 changes: 3 additions & 3 deletions warehouse/integrations/vulnerabilities/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
)


class VulnerabilityRecord(db.Model):
class VulnerabilityRecord(db.ModelBase):
__tablename__ = "vulnerabilities"

source = Column(String, primary_key=True)
Expand All @@ -56,7 +56,7 @@ class VulnerabilityRecord(db.Model):

# Alternative IDs for this vulnerability
# e.g. "CVE-2021-12345"
aliases = Column(ARRAY(String))
aliases = Column(ARRAY(String)) # type: ignore[var-annotated]

# Details about the vulnerability
details = Column(String)
Expand All @@ -65,7 +65,7 @@ class VulnerabilityRecord(db.Model):
summary = Column(String)

# Events of introduced/fixed versions
fixed_in = Column(ARRAY(String))
fixed_in = Column(ARRAY(String)) # type: ignore[var-annotated]

# When the vulnerability was withdrawn, if it has been withdrawn.
withdrawn = Column(TIMESTAMP, nullable=True)
Expand Down
2 changes: 1 addition & 1 deletion warehouse/ip_addresses/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def __lt__(self, other):
server_default=sql.false(),
comment="If True, this IP Address will be marked as banned",
)
ban_reason = Column(
ban_reason = Column( # type: ignore[var-annotated]
Enum(BanReason, values_callable=lambda x: [e.value for e in x]),
nullable=True,
comment="Reason for banning, must be in the BanReason enumeration",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
Revises: b5bb5d08543d
Create Date: 2019-12-18 17:27:00.183542
"""
import citext
import sqlalchemy as sa

from alembic import op
Expand Down Expand Up @@ -50,7 +49,7 @@ def upgrade():
server_default=sa.text("gen_random_uuid()"),
nullable=False,
),
sa.Column("name", citext.CIText(), nullable=False),
sa.Column("name", postgresql.CITEXT(), nullable=False),
sa.Column("version", sa.Integer(), default=1, nullable=False),
sa.Column("short_description", sa.String(length=128), nullable=False),
sa.Column("long_description", sa.Text(), nullable=False),
Expand Down
Loading