Repository: conan-io/conan
Pull request #16211 (2024-05-06 17:47:55)

Feature/cache save compression level
Changelog: Feature: Let ``conan cache save`` listen to the ``core.gzip:compresslevel`` conf
Docs: Omit



Patch:
diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py
index 1fcb49219..4fcfeebc7 100644
--- a/conan/api/subapi/cache.py
+++ b/conan/api/subapi/cache.py
@@ -119,21 +119,24 @@ class CacheAPI:
 
     def save(self, package_list, tgz_path):
         cache_folder = self.conan_api.cache_folder
-        app = ConanApp(self.conan_api)
+        global_conf = self.conan_api.config.global_conf
+        cache = ClientCache(cache_folder, global_conf)
         out = ConanOutput()
         mkdir(os.path.dirname(tgz_path))
         name = os.path.basename(tgz_path)
+        compresslevel = global_conf.get("core.gzip:compresslevel", check_type=int)
         with open(tgz_path, "wb") as tgz_handle:
-            tgz = gzopen_without_timestamps(name, mode="w", fileobj=tgz_handle)
+            tgz = gzopen_without_timestamps(name, mode="w", fileobj=tgz_handle,
+                                            compresslevel=compresslevel)
             for ref, ref_bundle in package_list.refs().items():
-                ref_layout = app.cache.recipe_layout(ref)
+                ref_layout = cache.recipe_layout(ref)
                 recipe_folder = os.path.relpath(ref_layout.base_folder, cache_folder)
                 recipe_folder = recipe_folder.replace("\\", "/")  # make win paths portable
                 ref_bundle["recipe_folder"] = recipe_folder
                 out.info(f"Saving {ref}: {recipe_folder}")
                 tgz.add(os.path.join(cache_folder, recipe_folder), recipe_folder, recursive=True)
                 for pref, pref_bundle in package_list.prefs(ref, ref_bundle).items():
-                    pref_layout = app.cache.pkg_layout(pref)
+                    pref_layout = cache.pkg_layout(pref)
                     pkg_folder = pref_layout.package()
                     folder = os.path.relpath(pkg_folder, cache_folder)
                     folder = folder.replace("\\", "/")  # make win paths portable
diff --git a/conan/cli/cli.py b/conan/cli/cli.py
index a9a420c58..a99a055ac 100644
--- a/conan/cli/cli.py
+++ b/conan/cli/cli.py
@@ -38,7 +38,7 @@ class Cli:
         self._groups = defaultdict(list)
         self._commands = {}
 
-    def _add_commands(self):
+    def add_commands(self):
         if Cli._builtin_commands is None:
             conan_cmd_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "commands")
             for module in pkgutil.iter_modules([conan_cmd_path]):
@@ -167,7 +167,7 @@ class Cli:
         methods
         """
         output = ConanOutput()
-        self._add_commands()
+        self.add_commands()
         try:
             command_argument = args[0][0]
         except IndexError:  # No parameters
diff --git a/conans/__init__.py b/conans/__init__.py
index 75491bf9a..78ef1f15f 100644
--- a/conans/__init__.py
+++ b/conans/__init__.py
@@ -2,4 +2,4 @@ CHECKSUM_DEPLOY = "checksum_deploy"  # Only when v2
 REVISIONS = "revisions"  # Only when enabled in config, not by default look at server_launcher.py
 OAUTH_TOKEN = "oauth_token"
 
-__version__ = '2.3.0-dev'
+__version__ = '2.4.0-dev'
diff --git a/conans/server/crypto/jwt/jwt_manager.py b/conans/server/crypto/jwt/jwt_manager.py
index ba4f35bdc..fadd972f2 100644
--- a/conans/server/crypto/jwt/jwt_manager.py
+++ b/conans/server/crypto/jwt/jwt_manager.py
@@ -1,10 +1,10 @@
-from datetime import datetime
+from datetime import datetime, timezone
 from calendar import timegm
 
 import jwt
 
 
-class JWTManager(object):
+class JWTManager:
     """
         Handles the JWT token generation and encryption.
     """
@@ -21,7 +21,8 @@ class JWTManager(object):
         profile_fields = profile_fields or {}
 
         if self.expire_time:
-            profile_fields["exp"] = timegm((datetime.utcnow() + self.expire_time).timetuple())
+            profile_fields["exp"] = timegm((datetime.now(timezone.utc)
+                                            + self.expire_time).timetuple())
 
         return jwt.encode(profile_fields, self.secret, algorithm="HS256")
 
diff --git a/conans/util/dates.py b/conans/util/dates.py
index a76067c5d..1881326af 100644
--- a/conans/util/dates.py
+++ b/conans/util/dates.py
@@ -9,7 +9,7 @@ from conans.errors import ConanException
 
 def from_timestamp_to_iso8601(timestamp):
     # Used exclusively by conan_server to return the date in iso format (same as artifactory)
-    return "%sZ" % datetime.datetime.utcfromtimestamp(timestamp).isoformat()
+    return "%s" % datetime.datetime.fromtimestamp(timestamp, datetime.timezone.utc).isoformat()
 
 
 def _from_iso8601_to_datetime(iso_str):
@@ -34,7 +34,7 @@ def revision_timestamp_now():
 def timestamp_to_str(timestamp):
     # used by ref.repr_humantime() to print human readable time
     assert timestamp is not None
-    return datetime.datetime.utcfromtimestamp(int(timestamp)).strftime('%Y-%m-%d %H:%M:%S UTC')
+    return datetime.datetime.fromtimestamp(int(timestamp), datetime.timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')
 
 
 def timelimit(expression):


Test patch:
diff --git a/conans/test/integration/command_v2/test_cache_save_restore.py b/conans/test/integration/command_v2/test_cache_save_restore.py
index 2d5301b60..c5e7c5b91 100644
--- a/conans/test/integration/command_v2/test_cache_save_restore.py
+++ b/conans/test/integration/command_v2/test_cache_save_restore.py
@@ -14,7 +14,8 @@ def test_cache_save_restore():
     c.run("create . --name=pkg --version=1.0 -s os=Linux")
     c.run("create . --name=pkg --version=1.1 -s os=Linux")
     c.run("create . --name=other --version=2.0 -s os=Linux")
-    c.run("cache save pkg/*:* ")
+    # Force the compress level just to make sure it doesn't crash
+    c.run("cache save pkg/*:* -cc core.gzip:compresslevel=9")
     cache_path = os.path.join(c.current_folder, "conan_cache_save.tgz")
     assert os.path.exists(cache_path)
     _validate_restore(cache_path)
diff --git a/conans/test/integration/conan_api/test_cli.py b/conans/test/integration/conan_api/test_cli.py
index 64d94e1c6..10804ba0d 100644
--- a/conans/test/integration/conan_api/test_cli.py
+++ b/conans/test/integration/conan_api/test_cli.py
@@ -21,3 +21,18 @@ def test_cli():
         cli.run(["list", "*"])
         cli2.run(["list", "*"])
         cli.run(["list", "*"])
+
+
+def test_basic_api():
+    api = ConanAPI(cache_folder=temp_folder())
+    result = api.remotes.list()
+    assert result[0].name == "conancenter"
+
+
+def test_api_command():
+    # The ``CommandAPI`` requires a bit more of setup
+    api = ConanAPI()
+    cli = Cli(api)
+    cli.add_commands()
+    result = api.command.run(["remote", "list"])
+    assert result[0].name == "conancenter"
diff --git a/conans/test/unittests/client/util/time_test.py b/conans/test/unittests/client/util/time_test.py
index dda8bfeff..e90050f68 100644
--- a/conans/test/unittests/client/util/time_test.py
+++ b/conans/test/unittests/client/util/time_test.py
@@ -3,7 +3,8 @@ import unittest
 
 from dateutil.tz import tzutc
 
-from conans.util.dates import from_timestamp_to_iso8601, _from_iso8601_to_datetime
+from conans.util.dates import from_timestamp_to_iso8601, _from_iso8601_to_datetime, \
+    from_iso8601_to_timestamp
 
 
 class TimeTest(unittest.TestCase):
@@ -11,7 +12,7 @@ class TimeTest(unittest.TestCase):
     def test_time_conversions(self):
         timestamp = 1547138099
         iso = from_timestamp_to_iso8601(timestamp)
-        self.assertEqual(iso, "2019-01-10T16:34:59Z")
+        self.assertEqual(iso, "2019-01-10T16:34:59+00:00")
 
         dt = _from_iso8601_to_datetime(iso)
         expected = datetime.datetime(year=2019, month=1, day=10, hour=16, minute=34, second=59,
@@ -29,3 +30,11 @@ class TimeTest(unittest.TestCase):
         expected = datetime.datetime(year=2019, month=5, day=14, hour=15, minute=52, second=28,
                                      microsecond=383000, tzinfo=tzutc())
         self.assertEqual(dt, expected)
+
+        artifactory_ret = "2024-01-23T08:39:53.776+0000"
+        ts = from_iso8601_to_timestamp(artifactory_ret)
+        assert ts == 1705999193.776
+
+        artifactory_ret = "2024-01-23T08:39:53.776+00:00"
+        ts = from_iso8601_to_timestamp(artifactory_ret)
+        assert ts == 1705999193.776