ampachedata 0.1.0__py3-none-any.whl

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.
Files changed (57) hide show
  1. ampachedata/__init__.py +59 -0
  2. ampachedata/__main__.py +136 -0
  3. ampachedata/data/AmpacheClient.py +805 -0
  4. ampachedata/data/ApiMethod.py +27 -0
  5. ampachedata/data/Bootstrap.py +65 -0
  6. ampachedata/data/ErrorCode.py +18 -0
  7. ampachedata/data/ObjectType.py +14 -0
  8. ampachedata/data/StatsFilter.py +20 -0
  9. ampachedata/data/Transport.py +77 -0
  10. ampachedata/data/__init__.py +4 -0
  11. ampachedata/data/auth/Handshake.py +15 -0
  12. ampachedata/data/auth/SessionManager.py +99 -0
  13. ampachedata/data/auth/__init__.py +3 -0
  14. ampachedata/data/db/Database.py +25 -0
  15. ampachedata/data/db/__init__.py +3 -0
  16. ampachedata/data/db/mappers/AlbumMapper.py +41 -0
  17. ampachedata/data/db/mappers/ArtistMapper.py +32 -0
  18. ampachedata/data/db/mappers/GenreMapper.py +3 -0
  19. ampachedata/data/db/mappers/HistoryMapper.py +40 -0
  20. ampachedata/data/db/mappers/PlaylistMapper.py +32 -0
  21. ampachedata/data/db/mappers/PlaylistSongMapper.py +22 -0
  22. ampachedata/data/db/mappers/SessionMapper.py +46 -0
  23. ampachedata/data/db/mappers/SongMapper.py +77 -0
  24. ampachedata/data/db/mappers/UserMapper.py +3 -0
  25. ampachedata/data/db/mappers/__init__.py +3 -0
  26. ampachedata/data/db/repositories/AlbumRepository.py +115 -0
  27. ampachedata/data/db/repositories/ArtistRepository.py +98 -0
  28. ampachedata/data/db/repositories/CredentialsRepository.py +43 -0
  29. ampachedata/data/db/repositories/GenreRepository.py +3 -0
  30. ampachedata/data/db/repositories/HistoryRepository.py +51 -0
  31. ampachedata/data/db/repositories/LikePattern.py +21 -0
  32. ampachedata/data/db/repositories/PlaylistRepository.py +97 -0
  33. ampachedata/data/db/repositories/PlaylistSongRepository.py +32 -0
  34. ampachedata/data/db/repositories/SessionRepository.py +56 -0
  35. ampachedata/data/db/repositories/SongRepository.py +273 -0
  36. ampachedata/data/db/repositories/UserRepository.py +3 -0
  37. ampachedata/data/db/repositories/__init__.py +3 -0
  38. ampachedata/data/errors.py +86 -0
  39. ampachedata/domain/Album.py +24 -0
  40. ampachedata/domain/Artist.py +20 -0
  41. ampachedata/domain/Credentials.py +13 -0
  42. ampachedata/domain/Genre.py +3 -0
  43. ampachedata/domain/History.py +13 -0
  44. ampachedata/domain/OperationResult.py +15 -0
  45. ampachedata/domain/PageResult.py +20 -0
  46. ampachedata/domain/PingResult.py +15 -0
  47. ampachedata/domain/Playlist.py +19 -0
  48. ampachedata/domain/PlaylistSong.py +14 -0
  49. ampachedata/domain/Session.py +13 -0
  50. ampachedata/domain/Song.py +58 -0
  51. ampachedata/domain/User.py +3 -0
  52. ampachedata/domain/__init__.py +3 -0
  53. ampachedata-0.1.0.dist-info/METADATA +85 -0
  54. ampachedata-0.1.0.dist-info/RECORD +57 -0
  55. ampachedata-0.1.0.dist-info/WHEEL +5 -0
  56. ampachedata-0.1.0.dist-info/licenses/LICENSE +674 -0
  57. ampachedata-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,59 @@
1
+ # SPDX-FileCopyrightText: 2026 icefields
2
+ # SPDX-License-Identifier: GPL-3.0-only
3
+ """ampachedata — public API. Anything not re-exported here is private."""
4
+ from .data.AmpacheClient import AmpacheClient
5
+ from .data.Bootstrap import storeCredentialsFromKey, storeCredentialsFromPassword
6
+ from .data.ObjectType import ObjectType
7
+ from .data.errors import (
8
+ AccessDeniedError,
9
+ AmpacheError,
10
+ ApiError,
11
+ BadRequestError,
12
+ CacheVerificationError,
13
+ CredentialValidationError,
14
+ DatabaseError,
15
+ DeprecatedError,
16
+ InvalidHandshakeError,
17
+ NotFoundError,
18
+ UnknownApiError,
19
+ )
20
+ from .domain.Artist import Artist
21
+ from .domain.Album import Album
22
+ from .domain.Credentials import Credentials
23
+ from .domain.History import History
24
+ from .domain.OperationResult import OperationResult
25
+ from .domain.PageResult import PageResult
26
+ from .domain.PingResult import PingResult
27
+ from .domain.Playlist import Playlist
28
+ from .domain.PlaylistSong import PlaylistSong
29
+ from .domain.Session import Session
30
+ from .domain.Song import Song
31
+
32
+ __all__ = [
33
+ "AmpacheClient",
34
+ "Artist",
35
+ "Album",
36
+ "Song",
37
+ "Playlist",
38
+ "PlaylistSong",
39
+ "History",
40
+ "Session",
41
+ "Credentials",
42
+ "PingResult",
43
+ "OperationResult",
44
+ "PageResult",
45
+ "ObjectType",
46
+ "storeCredentialsFromPassword",
47
+ "storeCredentialsFromKey",
48
+ "AmpacheError",
49
+ "ApiError",
50
+ "DatabaseError",
51
+ "InvalidHandshakeError",
52
+ "AccessDeniedError",
53
+ "NotFoundError",
54
+ "DeprecatedError",
55
+ "BadRequestError",
56
+ "CredentialValidationError",
57
+ "CacheVerificationError",
58
+ "UnknownApiError",
59
+ ]
@@ -0,0 +1,136 @@
1
+ # SPDX-FileCopyrightText: 2026 icefields
2
+ # SPDX-License-Identifier: GPL-3.0-only
3
+ """CLI: python -m ampachedata init-credentials ...
4
+
5
+ First-run credentials bootstrap. The cleartext password is accepted ONCE here —
6
+ via hidden getpass prompt, stdin, or an environment variable — hashed with SHA256
7
+ in memory, and only the digest is written to the single CredentialsEntity row.
8
+ There is deliberately no --password flag: cleartext never appears in argv, shell
9
+ history, or ps. Nothing else is touched: no handshake, no SessionEntity write."""
10
+ import argparse
11
+ import getpass
12
+ import os
13
+ import sys
14
+
15
+ from .data.Bootstrap import storeCredentialsFromKey, storeCredentialsFromPassword
16
+ from .data.errors import AmpacheError, CredentialValidationError
17
+
18
+
19
+ def main(argv=None) -> int:
20
+ args = _buildParser().parse_args(argv)
21
+ try:
22
+ return _runInitCredentials(args)
23
+ except (EOFError, KeyboardInterrupt):
24
+ print("aborted", file=sys.stderr)
25
+ return 130
26
+
27
+
28
+ def _buildParser():
29
+ parser = argparse.ArgumentParser(prog="python -m ampachedata")
30
+ subparsers = parser.add_subparsers(dest="command", required=True)
31
+ init = subparsers.add_parser(
32
+ "init-credentials",
33
+ help="first-run bootstrap: write the single CredentialsEntity row (hash only)",
34
+ description=(
35
+ "Store the single CredentialsEntity row. The cleartext password is "
36
+ "accepted once — hidden prompt, stdin, or env var — hashed with "
37
+ "SHA256 in memory, and only the digest is stored. There is no "
38
+ "--password flag: cleartext never appears in argv."
39
+ ),
40
+ )
41
+ init.add_argument("--db-path", required=True,
42
+ help="path to an existing musicdb.db (never created)")
43
+ init.add_argument("--username",
44
+ help="Ampache username (prompted when omitted interactively)")
45
+ init.add_argument("--server-url",
46
+ help="https://server (prompted when omitted interactively)")
47
+ source = init.add_mutually_exclusive_group()
48
+ source.add_argument("--password-stdin", action="store_true",
49
+ help="read the cleartext password from one stdin line")
50
+ source.add_argument("--password-env", metavar="VARNAME",
51
+ help="read the cleartext password from this environment variable")
52
+ source.add_argument("--key", metavar="HASH",
53
+ help="pre-hashed 64-hex key (visible in argv/shell history — "
54
+ "prefer --key-stdin)")
55
+ source.add_argument("--key-stdin", action="store_true",
56
+ help="read the pre-hashed 64-hex key from one stdin line")
57
+ return parser
58
+
59
+
60
+ def _runInitCredentials(args) -> int:
61
+ source = _secretSource(args)
62
+ username = args.username
63
+ serverUrl = args.server_url
64
+
65
+ if source is None: # interactive
66
+ if not sys.stdin.isatty():
67
+ print("error: no secret source given and stdin is not a TTY — pass "
68
+ "--password-stdin, --password-env VARNAME, --key, or --key-stdin",
69
+ file=sys.stderr)
70
+ return 2
71
+ if not username:
72
+ username = input("Username: ").strip()
73
+ if not serverUrl:
74
+ serverUrl = input("Server URL: ").strip()
75
+ password = getpass.getpass("Password: ")
76
+ if password != getpass.getpass("Confirm password: "):
77
+ print("error: passwords do not match", file=sys.stderr)
78
+ return 2
79
+ return _finish(storeCredentialsFromPassword, args.db_path, username, serverUrl, password)
80
+
81
+ if not username or not serverUrl:
82
+ print("error: --username and --server-url are required with " + source,
83
+ file=sys.stderr)
84
+ return 2
85
+ if source == "--password-stdin":
86
+ return _finish(storeCredentialsFromPassword, args.db_path, username, serverUrl,
87
+ _readSecretLine())
88
+ if source == "--password-env":
89
+ password = os.environ.get(args.password_env)
90
+ if password is None:
91
+ print("error: environment variable " + args.password_env + " is not set",
92
+ file=sys.stderr)
93
+ return 2
94
+ return _finish(storeCredentialsFromPassword, args.db_path, username, serverUrl, password)
95
+ if source == "--key":
96
+ return _finish(storeCredentialsFromKey, args.db_path, username, serverUrl, args.key)
97
+ return _finish(storeCredentialsFromKey, args.db_path, username, serverUrl,
98
+ _readSecretLine()) # --key-stdin
99
+
100
+
101
+ def _secretSource(args):
102
+ if args.password_stdin:
103
+ return "--password-stdin"
104
+ if args.password_env:
105
+ return "--password-env"
106
+ if args.key is not None:
107
+ return "--key"
108
+ if args.key_stdin:
109
+ return "--key-stdin"
110
+ return None
111
+
112
+
113
+ def _readSecretLine() -> str:
114
+ line = sys.stdin.readline()
115
+ if line.endswith("\n"):
116
+ line = line[:-1]
117
+ if line.endswith("\r"):
118
+ line = line[:-1]
119
+ return line
120
+
121
+
122
+ def _finish(storeFn, dbPath, username, serverUrl, secret) -> int:
123
+ try:
124
+ storeFn(dbPath, username, serverUrl, secret)
125
+ except CredentialValidationError as exc:
126
+ print("error: " + str(exc), file=sys.stderr)
127
+ return 2
128
+ except AmpacheError as exc:
129
+ print("error: " + str(exc), file=sys.stderr)
130
+ return 1
131
+ print("Stored credentials for user '" + username + "' at " + serverUrl)
132
+ return 0
133
+
134
+
135
+ if __name__ == "__main__":
136
+ sys.exit(main())