scmrepo 3.1.0__tar.gz → 3.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of scmrepo might be problematic. Click here for more details.

Files changed (69) hide show
  1. {scmrepo-3.1.0 → scmrepo-3.2.0}/.pre-commit-config.yaml +1 -1
  2. {scmrepo-3.1.0/src/scmrepo.egg-info → scmrepo-3.2.0}/PKG-INFO +1 -1
  3. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/git/backend/dulwich/__init__.py +14 -1
  4. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/git/lfs/client.py +99 -28
  5. {scmrepo-3.1.0 → scmrepo-3.2.0/src/scmrepo.egg-info}/PKG-INFO +1 -1
  6. {scmrepo-3.1.0 → scmrepo-3.2.0}/.coveragerc +0 -0
  7. {scmrepo-3.1.0 → scmrepo-3.2.0}/.cruft.json +0 -0
  8. {scmrepo-3.1.0 → scmrepo-3.2.0}/.gitattributes +0 -0
  9. {scmrepo-3.1.0 → scmrepo-3.2.0}/.github/dependabot.yml +0 -0
  10. {scmrepo-3.1.0 → scmrepo-3.2.0}/.github/workflows/release.yaml +0 -0
  11. {scmrepo-3.1.0 → scmrepo-3.2.0}/.github/workflows/tests.yaml +0 -0
  12. {scmrepo-3.1.0 → scmrepo-3.2.0}/.github/workflows/update-template.yaml +0 -0
  13. {scmrepo-3.1.0 → scmrepo-3.2.0}/.gitignore +0 -0
  14. {scmrepo-3.1.0 → scmrepo-3.2.0}/CODE_OF_CONDUCT.rst +0 -0
  15. {scmrepo-3.1.0 → scmrepo-3.2.0}/CONTRIBUTING.rst +0 -0
  16. {scmrepo-3.1.0 → scmrepo-3.2.0}/LICENSE +0 -0
  17. {scmrepo-3.1.0 → scmrepo-3.2.0}/README.rst +0 -0
  18. {scmrepo-3.1.0 → scmrepo-3.2.0}/noxfile.py +0 -0
  19. {scmrepo-3.1.0 → scmrepo-3.2.0}/pyproject.toml +0 -0
  20. {scmrepo-3.1.0 → scmrepo-3.2.0}/setup.cfg +0 -0
  21. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/__init__.py +0 -0
  22. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/asyn.py +0 -0
  23. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/base.py +0 -0
  24. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/exceptions.py +0 -0
  25. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/fs.py +0 -0
  26. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/git/__init__.py +0 -0
  27. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/git/backend/__init__.py +0 -0
  28. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/git/backend/base.py +0 -0
  29. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/git/backend/dulwich/asyncssh_vendor.py +0 -0
  30. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/git/backend/dulwich/client.py +0 -0
  31. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/git/backend/gitpython.py +0 -0
  32. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/git/backend/pygit2/__init__.py +0 -0
  33. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/git/backend/pygit2/callbacks.py +0 -0
  34. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/git/backend/pygit2/filter.py +0 -0
  35. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/git/config.py +0 -0
  36. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/git/credentials.py +0 -0
  37. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/git/lfs/__init__.py +0 -0
  38. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/git/lfs/exceptions.py +0 -0
  39. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/git/lfs/fetch.py +0 -0
  40. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/git/lfs/object.py +0 -0
  41. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/git/lfs/pointer.py +0 -0
  42. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/git/lfs/progress.py +0 -0
  43. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/git/lfs/smudge.py +0 -0
  44. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/git/lfs/storage.py +0 -0
  45. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/git/objects.py +0 -0
  46. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/git/stash.py +0 -0
  47. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/noscm.py +0 -0
  48. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/progress.py +0 -0
  49. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/py.typed +0 -0
  50. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo/utils.py +0 -0
  51. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo.egg-info/SOURCES.txt +0 -0
  52. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo.egg-info/dependency_links.txt +0 -0
  53. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo.egg-info/requires.txt +0 -0
  54. {scmrepo-3.1.0 → scmrepo-3.2.0}/src/scmrepo.egg-info/top_level.txt +0 -0
  55. {scmrepo-3.1.0 → scmrepo-3.2.0}/tests/__init__.py +0 -0
  56. {scmrepo-3.1.0 → scmrepo-3.2.0}/tests/conftest.py +0 -0
  57. {scmrepo-3.1.0 → scmrepo-3.2.0}/tests/docker-compose.yml +0 -0
  58. {scmrepo-3.1.0 → scmrepo-3.2.0}/tests/git-init/git.sh +0 -0
  59. {scmrepo-3.1.0 → scmrepo-3.2.0}/tests/test_credentials.py +0 -0
  60. {scmrepo-3.1.0 → scmrepo-3.2.0}/tests/test_dulwich.py +0 -0
  61. {scmrepo-3.1.0 → scmrepo-3.2.0}/tests/test_fs.py +0 -0
  62. {scmrepo-3.1.0 → scmrepo-3.2.0}/tests/test_git.py +0 -0
  63. {scmrepo-3.1.0 → scmrepo-3.2.0}/tests/test_lfs.py +0 -0
  64. {scmrepo-3.1.0 → scmrepo-3.2.0}/tests/test_noscm.py +0 -0
  65. {scmrepo-3.1.0 → scmrepo-3.2.0}/tests/test_pygit2.py +0 -0
  66. {scmrepo-3.1.0 → scmrepo-3.2.0}/tests/test_scmrepo.py +0 -0
  67. {scmrepo-3.1.0 → scmrepo-3.2.0}/tests/test_stash.py +0 -0
  68. {scmrepo-3.1.0 → scmrepo-3.2.0}/tests/user.key +0 -0
  69. {scmrepo-3.1.0 → scmrepo-3.2.0}/tests/user.key.pub +0 -0
@@ -20,7 +20,7 @@ repos:
20
20
  - id: sort-simple-yaml
21
21
  - id: trailing-whitespace
22
22
  - repo: https://github.com/astral-sh/ruff-pre-commit
23
- rev: 'v0.1.13'
23
+ rev: 'v0.2.2'
24
24
  hooks:
25
25
  - id: ruff
26
26
  args: [--fix, --exit-non-zero-on-fix]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: scmrepo
3
- Version: 3.1.0
3
+ Version: 3.2.0
4
4
  Summary: scmrepo
5
5
  Author-email: Iterative <support@dvc.org>
6
6
  License: Apache-2.0
@@ -842,7 +842,7 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
842
842
  if revision and revision not in rev_mapping:
843
843
  rev_mapping[revision] = ref
844
844
  for rev in revs:
845
- results[rev] = rev_mapping.get(rev, None)
845
+ results[rev] = rev_mapping.get(rev)
846
846
  return results
847
847
 
848
848
  def diff(self, rev_a: str, rev_b: str, binary=False) -> str:
@@ -978,3 +978,16 @@ def _parse_identity(identity: str) -> tuple[str, str]:
978
978
  if not m:
979
979
  raise SCMError("Could not parse tagger identity '{identity}'")
980
980
  return m.group("name"), m.group("email")
981
+
982
+
983
+ def ls_remote(url: str) -> dict[str, str]:
984
+ from dulwich import porcelain
985
+ from dulwich.client import HTTPUnauthorized
986
+
987
+ try:
988
+ refs = porcelain.ls_remote(url)
989
+ return {os.fsdecode(ref): sha.decode("ascii") for ref, sha in refs.items()}
990
+ except HTTPUnauthorized as exc:
991
+ raise AuthError(url) from exc
992
+ except Exception as exc: # noqa: BLE001
993
+ raise InvalidRemote(url) from exc
@@ -1,6 +1,9 @@
1
+ import json
1
2
  import logging
2
3
  import os
4
+ import re
3
5
  import shutil
6
+ from abc import abstractmethod
4
7
  from collections.abc import Iterable, Iterator
5
8
  from contextlib import AbstractContextManager, contextmanager, suppress
6
9
  from tempfile import NamedTemporaryFile
@@ -13,6 +16,7 @@ from fsspec.callbacks import DEFAULT_CALLBACK
13
16
  from fsspec.implementations.http import HTTPFileSystem
14
17
  from funcy import cached_property
15
18
 
19
+ from scmrepo.git.backend.dulwich import _get_ssh_vendor
16
20
  from scmrepo.git.credentials import Credential, CredentialNotFoundError
17
21
 
18
22
  from .exceptions import LFSError
@@ -35,19 +39,12 @@ class LFSClient(AbstractContextManager):
35
39
  _SESSION_RETRIES = 5
36
40
  _SESSION_BACKOFF_FACTOR = 0.1
37
41
 
38
- def __init__(
39
- self,
40
- url: str,
41
- git_url: Optional[str] = None,
42
- headers: Optional[dict[str, str]] = None,
43
- ):
42
+ def __init__(self, url: str):
44
43
  """
45
44
  Args:
46
45
  url: LFS server URL.
47
46
  """
48
47
  self.url = url
49
- self.git_url = git_url
50
- self.headers: dict[str, str] = headers or {}
51
48
 
52
49
  def __exit__(self, *args, **kwargs):
53
50
  self.close()
@@ -84,23 +81,18 @@ class LFSClient(AbstractContextManager):
84
81
 
85
82
  @classmethod
86
83
  def from_git_url(cls, git_url: str) -> "LFSClient":
87
- if git_url.endswith(".git"):
88
- url = f"{git_url}/info/lfs"
89
- else:
90
- url = f"{git_url}.git/info/lfs"
91
- return cls(url, git_url=git_url)
84
+ if git_url.startswith(("ssh://", "git@")):
85
+ return _SSHLFSClient.from_git_url(git_url)
86
+ if git_url.startswith("https://"):
87
+ return _HTTPLFSClient.from_git_url(git_url)
88
+ raise NotImplementedError(f"Unsupported Git URL: {git_url}")
92
89
 
93
90
  def close(self):
94
91
  pass
95
92
 
96
- def _get_auth(self) -> Optional[aiohttp.BasicAuth]:
97
- try:
98
- creds = Credential(url=self.git_url).fill()
99
- if creds.username and creds.password:
100
- return aiohttp.BasicAuth(creds.username, creds.password)
101
- except CredentialNotFoundError:
102
- pass
103
- return None
93
+ @abstractmethod
94
+ def _get_auth_header(self, *, upload: bool) -> dict:
95
+ ...
104
96
 
105
97
  async def _batch_request(
106
98
  self,
@@ -120,9 +112,10 @@ class LFSClient(AbstractContextManager):
120
112
  if ref:
121
113
  body["ref"] = [{"name": ref}]
122
114
  session = await self._fs.set_session()
123
- headers = dict(self.headers)
124
- headers["Accept"] = self.JSON_CONTENT_TYPE
125
- headers["Content-Type"] = self.JSON_CONTENT_TYPE
115
+ headers = {
116
+ "Accept": self.JSON_CONTENT_TYPE,
117
+ "Content-Type": self.JSON_CONTENT_TYPE,
118
+ }
126
119
  try:
127
120
  async with session.post(
128
121
  url,
@@ -134,13 +127,12 @@ class LFSClient(AbstractContextManager):
134
127
  except aiohttp.ClientResponseError as exc:
135
128
  if exc.status != 401:
136
129
  raise
137
- auth = self._get_auth()
138
- if auth is None:
130
+ auth_header = self._get_auth_header(upload=upload)
131
+ if not auth_header:
139
132
  raise
140
133
  async with session.post(
141
134
  url,
142
- auth=auth,
143
- headers=headers,
135
+ headers={**headers, **auth_header},
144
136
  json=body,
145
137
  raise_for_status=True,
146
138
  ) as resp:
@@ -186,6 +178,85 @@ class LFSClient(AbstractContextManager):
186
178
  download = sync_wrapper(_download)
187
179
 
188
180
 
181
+ class _HTTPLFSClient(LFSClient):
182
+ def __init__(self, url: str, git_url: str):
183
+ """
184
+ Args:
185
+ url: LFS server URL.
186
+ git_url: Git HTTP URL.
187
+ """
188
+ super().__init__(url)
189
+ self.git_url = git_url
190
+
191
+ @classmethod
192
+ def from_git_url(cls, git_url: str) -> "_HTTPLFSClient":
193
+ if git_url.endswith(".git"):
194
+ url = f"{git_url}/info/lfs"
195
+ else:
196
+ url = f"{git_url}.git/info/lfs"
197
+ return cls(url, git_url=git_url)
198
+
199
+ def _get_auth_header(self, *, upload: bool) -> dict:
200
+ try:
201
+ creds = Credential(url=self.git_url).fill()
202
+ if creds.username and creds.password:
203
+ return {
204
+ aiohttp.hdrs.AUTHORIZATION: aiohttp.BasicAuth(
205
+ creds.username, creds.password
206
+ ).encode()
207
+ }
208
+ except CredentialNotFoundError:
209
+ pass
210
+ return {}
211
+
212
+
213
+ class _SSHLFSClient(LFSClient):
214
+ _URL_PATTERN = re.compile(
215
+ r"(?:ssh://)?git@(?P<host>\S+?)(?::(?P<port>\d+))?(?:[:/])(?P<path>\S+?)\.git"
216
+ )
217
+
218
+ def __init__(self, url: str, host: str, port: int, path: str):
219
+ """
220
+ Args:
221
+ url: LFS server URL.
222
+ host: Git SSH server host.
223
+ port: Git SSH server port.
224
+ path: Git project path.
225
+ """
226
+ super().__init__(url)
227
+ self.host = host
228
+ self.port = port
229
+ self.path = path
230
+ self._ssh = _get_ssh_vendor()
231
+
232
+ @classmethod
233
+ def from_git_url(cls, git_url: str) -> "_SSHLFSClient":
234
+ result = cls._URL_PATTERN.match(git_url)
235
+ if not result:
236
+ raise ValueError(f"Invalid Git SSH URL: {git_url}")
237
+ host, port, path = result.group("host", "port", "path")
238
+ url = f"https://{host}/{path}.git/info/lfs"
239
+ return cls(url, host, int(port or 22), path)
240
+
241
+ def _get_auth_header(self, *, upload: bool) -> dict:
242
+ return self._git_lfs_authenticate(
243
+ self.host, self.port, f"{self.path}.git", upload=upload
244
+ ).get("header", {})
245
+
246
+ def _git_lfs_authenticate(
247
+ self, host: str, port: int, path: str, *, upload: bool = False
248
+ ) -> dict:
249
+ action = "upload" if upload else "download"
250
+ return json.loads(
251
+ self._ssh.run_command(
252
+ command=f"git-lfs-authenticate {path} {action}",
253
+ host=host,
254
+ port=port,
255
+ username="git",
256
+ ).read()
257
+ )
258
+
259
+
189
260
  @contextmanager
190
261
  def _as_atomic(to_info: str, create_parents: bool = False) -> Iterator[str]:
191
262
  parent = os.path.dirname(to_info)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: scmrepo
3
- Version: 3.1.0
3
+ Version: 3.2.0
4
4
  Summary: scmrepo
5
5
  Author-email: Iterative <support@dvc.org>
6
6
  License: Apache-2.0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes