diff --git a/AUTHORS.md b/AUTHORS.md index 130d68055..ae2be97a5 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -246,6 +246,7 @@ Authors * [Sagi Kedmi](https://github.com/sagi) * [Sam Lanning](https://github.com/samlanning) * [sapics](https://github.com/sapics) +* [SATOH Fumiyasu](https://github.com/fumiyas) * [Scott Barr](https://github.com/scottjbarr) * [Scott Merrill](https://github.com/skpy) * [Sebastian Bögl](https://github.com/TheBoegl) diff --git a/acme/setup.py b/acme/setup.py index e219d640c..c3117c612 100644 --- a/acme/setup.py +++ b/acme/setup.py @@ -10,7 +10,6 @@ install_requires = [ # relaxed to >=24.0.0 if needed. 'PyOpenSSL>=25.0.0', 'pyrfc3339', - 'pytz>=2019.3', 'requests>=2.20.0', ] diff --git a/acme/src/acme/_internal/tests/fields_test.py b/acme/src/acme/_internal/tests/fields_test.py index 0c2b8c4a0..c3a476d1f 100644 --- a/acme/src/acme/_internal/tests/fields_test.py +++ b/acme/src/acme/_internal/tests/fields_test.py @@ -6,7 +6,6 @@ import warnings import josepy as jose import pytest -import pytz class FixedTest(unittest.TestCase): @@ -34,7 +33,7 @@ class RFC3339FieldTest(unittest.TestCase): """Tests for acme.fields.RFC3339Field.""" def setUp(self): - self.decoded = datetime.datetime(2015, 3, 27, tzinfo=pytz.UTC) + self.decoded = datetime.datetime(2015, 3, 27, tzinfo=datetime.timezone.utc) self.encoded = '2015-03-27T00:00:00Z' def test_default_encoder(self): diff --git a/acme/src/acme/fields.py b/acme/src/acme/fields.py index 2ff5da419..b6d76a52c 100644 --- a/acme/src/acme/fields.py +++ b/acme/src/acme/fields.py @@ -34,7 +34,7 @@ class RFC3339Field(jose.Field): Handles decoding/encoding between RFC3339 strings and aware (not naive) `datetime.datetime` objects - (e.g. ``datetime.datetime.now(pytz.UTC)``). + (e.g. ``datetime.datetime.now(datetime.timezone.utc)``). """ diff --git a/certbot-ci/setup.py b/certbot-ci/setup.py index c38c4100c..0e2aa359e 100644 --- a/certbot-ci/setup.py +++ b/certbot-ci/setup.py @@ -17,7 +17,6 @@ install_requires = [ # installation on Linux. 'pywin32>=300 ; sys_platform == "win32"', 'pyyaml', - 'pytz>=2019.3', # requests unvendored its dependencies in version 2.16.0 and this code relies on that for # calling `urllib3.disable_warnings`. 'requests>=2.16.0', diff --git a/certbot-ci/src/certbot_integration_tests/utils/pebble_ocsp_server.py b/certbot-ci/src/certbot_integration_tests/utils/pebble_ocsp_server.py index f01332fed..83dc96e19 100755 --- a/certbot-ci/src/certbot_integration_tests/utils/pebble_ocsp_server.py +++ b/certbot-ci/src/certbot_integration_tests/utils/pebble_ocsp_server.py @@ -5,7 +5,6 @@ to serve a mock OCSP responder during integration tests against Pebble. """ import datetime import http.server as BaseHTTPServer -import pytz import re from typing import cast from typing import Union @@ -55,7 +54,7 @@ class _ProxyHandler(BaseHTTPServer.BaseHTTPRequestHandler): else: data = response.json() - now = datetime.datetime.now(pytz.UTC) + now = datetime.datetime.now(datetime.timezone.utc) cert = x509.load_pem_x509_certificate(data['Certificate'].encode(), default_backend()) if data['Status'] != 'Revoked': ocsp_status = ocsp.OCSPCertStatus.GOOD diff --git a/certbot/CHANGELOG.md b/certbot/CHANGELOG.md index 99f4f4312..37b357d24 100644 --- a/certbot/CHANGELOG.md +++ b/certbot/CHANGELOG.md @@ -12,6 +12,7 @@ Certbot adheres to [Semantic Versioning](https://semver.org/). * Catches and ignores errors during the directory fetch for ARI checking so that these errors do not hinder the actual certificate issuance +* Removed the dependency on `pytz`. ### Fixed diff --git a/certbot/setup.py b/certbot/setup.py index f253dc1d7..bdb46d0e7 100644 --- a/certbot/setup.py +++ b/certbot/setup.py @@ -37,7 +37,6 @@ install_requires = [ 'josepy>=2.0.0', 'parsedatetime>=2.4', 'pyrfc3339', - 'pytz>=2019.3', # This dependency needs to be added using environment markers to avoid its # installation on Linux. 'pywin32>=300 ; sys_platform == "win32"', @@ -75,7 +74,6 @@ test_extras = [ 'tox', 'types-httplib2', 'types-pyRFC3339', - 'types-pytz', 'types-pywin32', 'types-requests', 'types-setuptools', diff --git a/certbot/src/certbot/_internal/account.py b/certbot/src/certbot/_internal/account.py index 645f12a9d..1c15a5ae6 100644 --- a/certbot/src/certbot/_internal/account.py +++ b/certbot/src/certbot/_internal/account.py @@ -16,7 +16,6 @@ from typing import Optional from cryptography.hazmat.primitives import serialization import josepy as jose import pyrfc3339 -import pytz from acme import fields as acme_fields from acme import messages @@ -64,7 +63,7 @@ class Account: self.regr = regr self.meta = self.Meta( # pyrfc3339 drops microseconds, make sure __eq__ is sane - creation_dt=datetime.datetime.now(tz=pytz.UTC).replace(microsecond=0), + creation_dt=datetime.datetime.now(tz=datetime.timezone.utc).replace(microsecond=0), creation_host=socket.getfqdn(), register_to_eff=None) if meta is None else meta diff --git a/certbot/src/certbot/_internal/cert_manager.py b/certbot/src/certbot/_internal/cert_manager.py index 033cacfea..1510db21f 100644 --- a/certbot/src/certbot/_internal/cert_manager.py +++ b/certbot/src/certbot/_internal/cert_manager.py @@ -12,8 +12,6 @@ from typing import Tuple from typing import TypeVar from typing import Union -import pytz - from certbot import configuration from certbot import crypto_util from certbot import errors @@ -285,7 +283,7 @@ def human_readable_cert_info(config: configuration.NamespaceConfig, cert: storag return None if config.domains and not set(config.domains).issubset(cert.names()): return None - now = datetime.datetime.now(pytz.UTC) + now = datetime.datetime.now(datetime.timezone.utc) reasons = [] if cert.is_test_cert: diff --git a/certbot/src/certbot/_internal/storage.py b/certbot/src/certbot/_internal/storage.py index 16df4bf96..111c10362 100644 --- a/certbot/src/certbot/_internal/storage.py +++ b/certbot/src/certbot/_internal/storage.py @@ -22,7 +22,6 @@ from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey from cryptography.hazmat.primitives.serialization import load_pem_private_key import parsedatetime -import pytz import certbot from certbot import configuration @@ -104,7 +103,7 @@ def add_time_interval(base_time: datetime.datetime, interval: str, interval += " days" # try to use the same timezone, but fallback to UTC - tzinfo = base_time.tzinfo or pytz.UTC + tzinfo = base_time.tzinfo or datetime.timezone.utc return textparser.parseDT(interval, base_time, tzinfo=tzinfo)[0] diff --git a/certbot/src/certbot/_internal/tests/account_test.py b/certbot/src/certbot/_internal/tests/account_test.py index c9b7b7622..c36b10969 100644 --- a/certbot/src/certbot/_internal/tests/account_test.py +++ b/certbot/src/certbot/_internal/tests/account_test.py @@ -6,7 +6,6 @@ from unittest import mock import josepy as jose import pytest -import pytz from acme import messages from certbot import errors @@ -27,7 +26,7 @@ class AccountTest(unittest.TestCase): self.meta = Account.Meta( creation_host="test.certbot.org", creation_dt=datetime.datetime( - 2015, 7, 4, 14, 4, 10, tzinfo=pytz.UTC)) + 2015, 7, 4, 14, 4, 10, tzinfo=datetime.timezone.utc)) self.acc = Account(self.regr, KEY, self.meta) self.regr.__repr__ = mock.MagicMock(return_value="i_am_a_regr") @@ -111,7 +110,7 @@ class AccountFileStorageTest(test_util.ConfigTestCase): meta = Account.Meta( creation_host="test.example.org", creation_dt=datetime.datetime( - 2021, 1, 5, 14, 4, 10, tzinfo=pytz.UTC)) + 2021, 1, 5, 14, 4, 10, tzinfo=datetime.timezone.utc)) self.acc = Account( regr=messages.RegistrationResource( uri=None, body=messages.Registration()), diff --git a/certbot/src/certbot/_internal/tests/cert_manager_test.py b/certbot/src/certbot/_internal/tests/cert_manager_test.py index d2a29d3d5..fae9de8d8 100644 --- a/certbot/src/certbot/_internal/tests/cert_manager_test.py +++ b/certbot/src/certbot/_internal/tests/cert_manager_test.py @@ -213,10 +213,8 @@ class CertificatesTest(BaseCertManagerTest): mock_serial.return_value = 1234567890 import datetime - import pytz - from certbot._internal import cert_manager - expiry = datetime.datetime.now(pytz.UTC) + expiry = datetime.datetime.now(datetime.timezone.utc) cert = mock.MagicMock(lineagename="nameone") cert.target_expiry = expiry diff --git a/certbot/src/certbot/_internal/tests/eff_test.py b/certbot/src/certbot/_internal/tests/eff_test.py index 33694e17e..4402be803 100644 --- a/certbot/src/certbot/_internal/tests/eff_test.py +++ b/certbot/src/certbot/_internal/tests/eff_test.py @@ -6,7 +6,6 @@ from unittest import mock import josepy import pytest -import pytz import requests from acme import messages @@ -29,7 +28,7 @@ class SubscriptionTest(test_util.ConfigTestCase): meta=account.Account.Meta( creation_host='test.certbot.org', creation_dt=datetime.datetime( - 2015, 7, 4, 14, 4, 10, tzinfo=pytz.UTC))) + 2015, 7, 4, 14, 4, 10, tzinfo=datetime.timezone.utc))) self.config.email = 'certbot@example.org' self.config.eff_email = None diff --git a/certbot/src/certbot/_internal/tests/main_test.py b/certbot/src/certbot/_internal/tests/main_test.py index 18c44db96..32146ce59 100644 --- a/certbot/src/certbot/_internal/tests/main_test.py +++ b/certbot/src/certbot/_internal/tests/main_test.py @@ -19,7 +19,6 @@ import configobj from cryptography import x509 import josepy as jose import pytest -import pytz from acme.messages import Error as acme_error from certbot import errors @@ -423,7 +422,7 @@ class RevokeTest(test_util.TempDirTestCase): self.meta = Account.Meta( creation_host="test.certbot.org", creation_dt=datetime.datetime( - 2015, 7, 4, 14, 4, 10, tzinfo=pytz.UTC)) + 2015, 7, 4, 14, 4, 10, tzinfo=datetime.timezone.utc)) self.acc = Account(self.regr, JWK, self.meta) self.mock_determine_account.return_value = (self.acc, None) diff --git a/certbot/src/certbot/_internal/tests/ocsp_test.py b/certbot/src/certbot/_internal/tests/ocsp_test.py index 8429afac7..f0736638e 100644 --- a/certbot/src/certbot/_internal/tests/ocsp_test.py +++ b/certbot/src/certbot/_internal/tests/ocsp_test.py @@ -3,6 +3,7 @@ import contextlib from datetime import datetime from datetime import timedelta +from datetime import timezone import sys import unittest from unittest import mock @@ -14,7 +15,6 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.x509 import ocsp as ocsp_lib import pytest -import pytz from certbot import errors from certbot.tests import util as test_util @@ -65,7 +65,7 @@ class OCSPTestOpenSSL(unittest.TestCase): @mock.patch('certbot.ocsp.crypto_util.notAfter') @mock.patch('certbot.util.run_script') def test_ocsp_revoked(self, mock_run, mock_na, mock_determine): - now = datetime.now(pytz.UTC) + now = datetime.now(timezone.utc) cert_obj = mock.MagicMock() cert_obj.cert_path = "x" cert_obj.chain_path = "y" @@ -138,7 +138,7 @@ class OSCPTestCryptography(unittest.TestCase): self.cert_obj = mock.MagicMock() self.cert_obj.cert_path = self.cert_path self.cert_obj.chain_path = self.chain_path - now = datetime.now(pytz.UTC) + now = datetime.now(timezone.utc) self.mock_notAfter = mock.patch('certbot.ocsp.crypto_util.notAfter', return_value=now + timedelta(hours=2)) self.mock_notAfter.start() @@ -324,8 +324,8 @@ def _construct_mock_ocsp_response(certificate_status, response_status): responder_name=responder.subject, certificates=[responder], hash_algorithm=hashes.SHA1(), - next_update_utc=datetime.now(pytz.UTC) + timedelta(days=1), - this_update_utc=datetime.now(pytz.UTC) - timedelta(days=1), + next_update_utc=datetime.now(timezone.utc) + timedelta(days=1), + this_update_utc=datetime.now(timezone.utc) - timedelta(days=1), signature_algorithm_oid=x509.oid.SignatureAlgorithmOID.RSA_WITH_SHA1, ) diff --git a/certbot/src/certbot/_internal/tests/renewal_test.py b/certbot/src/certbot/_internal/tests/renewal_test.py index 9ba3a35d4..561984e43 100644 --- a/certbot/src/certbot/_internal/tests/renewal_test.py +++ b/certbot/src/certbot/_internal/tests/renewal_test.py @@ -7,7 +7,6 @@ import unittest from unittest import mock import pytest -import pytz from acme import challenges from certbot import configuration @@ -225,9 +224,9 @@ class RenewalTest(test_util.ConfigTestCase): from certbot._internal import renewal acme_client = mock.MagicMock() mock_acme_from_config.return_value = acme_client - past = datetime.datetime(2025, 3, 19, 0, 0, 0, tzinfo=pytz.UTC) - now = datetime.datetime(2025, 4, 19, 0, 0, 0, tzinfo=pytz.UTC) - future = datetime.datetime(2025, 4, 19, 12, 0, 0, tzinfo=pytz.UTC) + past = datetime.datetime(2025, 3, 19, 0, 0, 0, tzinfo=datetime.timezone.utc) + now = datetime.datetime(2025, 4, 19, 0, 0, 0, tzinfo=datetime.timezone.utc) + future = datetime.datetime(2025, 4, 19, 12, 0, 0, tzinfo=datetime.timezone.utc) mock_datetime.datetime.now.return_value = now acme_client.renewal_time.return_value = past, future @@ -296,7 +295,7 @@ class RenewalTest(test_util.ConfigTestCase): ari_server = "http://ari" mock_acme = mock.MagicMock() - future = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=100000) + future = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=100000) mock_acme.renewal_time.return_value = (future, future) acme_clients = {} acme_clients[ari_server] = mock_acme @@ -346,7 +345,7 @@ class RenewalTest(test_util.ConfigTestCase): (1420070400, "10 weeks", True), (1420070400, "10 months", True), (1420070400, "10 years", True), (1420070400, "99 months", True), ]: - sometime = datetime.datetime.fromtimestamp(current_time, pytz.UTC) + sometime = datetime.datetime.fromtimestamp(current_time, datetime.timezone.utc) mock_datetime.datetime.now.return_value = sometime mock_renewable_cert.configuration = {"renew_before_expiry": interval} assert renewal.should_autorenew(self.config, mock_renewable_cert, acme_clients) == result, f"at {current_time}, with config '{interval}', ari response in future, expected {result}" @@ -384,7 +383,7 @@ class RenewalTest(test_util.ConfigTestCase): (1420070400, "10 weeks", True), (1420070400, "10 months", True), (1420070400, "10 years", True), (1420070400, "99 months", True), ]: - sometime = datetime.datetime.fromtimestamp(current_time, pytz.UTC) + sometime = datetime.datetime.fromtimestamp(current_time, datetime.timezone.utc) mock_datetime.datetime.now.return_value = sometime mock_renewable_cert.configuration = {"renew_before_expiry": interval} mock_renewable_cert.server = ari_server @@ -398,7 +397,7 @@ class RenewalTest(test_util.ConfigTestCase): mock_acme = mock.MagicMock() ari_server = "http://ari" - future = datetime.datetime.now(pytz.UTC) + datetime.timedelta(seconds=1000) + future = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=1000) mock_acme.renewal_time.return_value = (future, future) acme_clients = {} acme_clients[ari_server] = mock_acme diff --git a/certbot/src/certbot/_internal/tests/storage_test.py b/certbot/src/certbot/_internal/tests/storage_test.py index e9359cb8e..0f961625a 100644 --- a/certbot/src/certbot/_internal/tests/storage_test.py +++ b/certbot/src/certbot/_internal/tests/storage_test.py @@ -6,10 +6,10 @@ import stat import sys import unittest from unittest import mock +import zoneinfo import configobj import pytest -import pytz import certbot from certbot import configuration @@ -19,7 +19,6 @@ from certbot.compat import filesystem from certbot.compat import os import certbot.tests.util as test_util -import datetime from typing import Optional, Any def unlink_all(rc_object): @@ -683,14 +682,13 @@ class RenewableCertTests(BaseRenewableCertTest): from certbot._internal import storage # this month has 30 days, and the next year is a leap year - time_1 = datetime.datetime(2003, 11, 20, 11, 59, 21, tzinfo=pytz.UTC) + time_1 = datetime.datetime(2003, 11, 20, 11, 59, 21, tzinfo=datetime.timezone.utc) # this month has 31 days, and the next year is not a leap year - time_2 = datetime.datetime(2012, 10, 18, 21, 31, 16, tzinfo=pytz.UTC) + time_2 = datetime.datetime(2012, 10, 18, 21, 31, 16, tzinfo=datetime.timezone.utc) # in different time zone (GMT+8) - time_3 = pytz.timezone('Asia/Shanghai').fromutc( - datetime.datetime(2015, 10, 26, 22, 25, 41)) + time_3 = datetime.datetime(2015, 10, 26, 22, 25, 41, tzinfo=zoneinfo.ZoneInfo('Asia/Shanghai')) intended = { (time_1, ""): time_1, diff --git a/certbot/src/certbot/ocsp.py b/certbot/src/certbot/ocsp.py index 3546f3dc8..a3841183e 100644 --- a/certbot/src/certbot/ocsp.py +++ b/certbot/src/certbot/ocsp.py @@ -1,6 +1,7 @@ """Tools for checking certificate revocation.""" from datetime import datetime from datetime import timedelta +from datetime import timezone import logging import re import subprocess @@ -16,7 +17,6 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import serialization from cryptography.x509 import ocsp -import pytz import requests from certbot import crypto_util @@ -83,7 +83,7 @@ class RevocationChecker: # Let's Encrypt doesn't update OCSP for expired certificates, # so don't check OCSP if the cert is expired. # https://github.com/certbot/certbot/issues/7152 - now = datetime.now(pytz.UTC) + now = datetime.now(timezone.utc) if crypto_util.notAfter(cert_path) <= now: return False @@ -239,7 +239,7 @@ def _check_ocsp_response(response_ocsp: 'ocsp.OCSPResponse', request_ocsp: 'ocsp # See OpenSSL implementation as a reference: # https://github.com/openssl/openssl/blob/ef45aa14c5af024fcb8bef1c9007f3d1c115bd85/crypto/ocsp/ocsp_cl.c#L338-L391 # thisUpdate/nextUpdate are expressed in UTC/GMT time zone - now = datetime.now(pytz.UTC) + now = datetime.now(timezone.utc) if not response_ocsp.this_update_utc: raise AssertionError('param thisUpdate is not set.') if response_ocsp.this_update_utc > now + timedelta(minutes=5): diff --git a/tools/oldest_constraints.txt b/tools/oldest_constraints.txt index 09f2676e9..12066cbe3 100644 --- a/tools/oldest_constraints.txt +++ b/tools/oldest_constraints.txt @@ -66,7 +66,6 @@ pytest==8.3.5 ; python_full_version >= "3.9.2" and python_version < "3.10" python-augeas==0.5.0 ; python_full_version >= "3.9.2" and python_version < "3.10" python-dateutil==2.9.0.post0 ; python_full_version >= "3.9.2" and python_version < "3.10" python-digitalocean==1.11 ; python_full_version >= "3.9.2" and python_version < "3.10" -pytz==2019.3 ; python_full_version >= "3.9.2" and python_version < "3.10" pywin32==310 ; python_full_version >= "3.9.2" and python_version < "3.10" and sys_platform == "win32" pyyaml==6.0.2 ; python_full_version >= "3.9.2" and python_version < "3.10" requests-file==2.1.0 ; python_full_version >= "3.9.2" and python_version < "3.10" @@ -83,7 +82,6 @@ tox==1.9.2 ; python_full_version >= "3.9.2" and python_version < "3.10" types-httplib2==0.22.0.20250401 ; python_full_version >= "3.9.2" and python_version < "3.10" types-pyrfc3339==2.0.1.20241107 ; python_full_version >= "3.9.2" and python_version < "3.10" types-python-dateutil==2.9.0.20241206 ; python_full_version >= "3.9.2" and python_version < "3.10" -types-pytz==2025.2.0.20250326 ; python_full_version >= "3.9.2" and python_version < "3.10" types-pywin32==310.0.0.20250429 ; python_full_version >= "3.9.2" and python_version < "3.10" types-requests==2.31.0.6 ; python_full_version >= "3.9.2" and python_version < "3.10" types-setuptools==80.0.0.20250429 ; python_full_version >= "3.9.2" and python_version < "3.10" diff --git a/tools/pinning/oldest/pyproject.toml b/tools/pinning/oldest/pyproject.toml index 64e06517a..fcbd23ac8 100644 --- a/tools/pinning/oldest/pyproject.toml +++ b/tools/pinning/oldest/pyproject.toml @@ -81,7 +81,6 @@ pycparser = "2.14" pyparsing = "2.4.7" python-augeas = "0.5.0" python-digitalocean = "1.11" -pytz = "2019.3" requests = "2.20.0" six = "1.11.0" urllib3 = "1.24.2" diff --git a/tools/requirements.txt b/tools/requirements.txt index 3421ef3da..2b14db4d7 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -136,7 +136,6 @@ pytest==8.4.1 ; python_full_version >= "3.9.2" and python_version < "4.0" python-augeas==1.2.0 ; python_full_version >= "3.9.2" and python_version < "4.0" python-dateutil==2.9.0.post0 ; python_full_version >= "3.9.2" and python_version < "4.0" python-digitalocean==1.17.0 ; python_full_version >= "3.9.2" and python_version < "4.0" -pytz==2025.2 ; python_full_version >= "3.9.2" and python_version < "4.0" pywin32-ctypes==0.2.3 ; python_full_version >= "3.9.2" and python_version < "4.0" and sys_platform == "win32" pywin32==311 ; python_full_version >= "3.9.2" and python_version < "4.0" and sys_platform == "win32" pyyaml==6.0.2 ; python_full_version >= "3.9.2" and python_version < "4.0" @@ -182,7 +181,6 @@ twine==6.1.0 ; python_full_version >= "3.9.2" and python_version < "4.0" types-httplib2==0.22.0.20250622 ; python_full_version >= "3.9.2" and python_version < "4.0" types-pyrfc3339==2.0.1.20241107 ; python_full_version >= "3.9.2" and python_version < "4.0" types-python-dateutil==2.9.0.20250708 ; python_full_version >= "3.9.2" and python_version < "4.0" -types-pytz==2025.2.0.20250516 ; python_full_version >= "3.9.2" and python_version < "4.0" types-pywin32==310.0.0.20250516 ; python_full_version >= "3.9.2" and python_version < "4.0" types-requests==2.31.0.6 ; python_full_version >= "3.9.2" and python_version < "3.10" types-requests==2.32.4.20250611 ; python_version >= "3.10" and python_version < "4.0"