pymal-ng 1.0.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.
- pymal_ng-1.0.0/LICENSE +27 -0
- pymal_ng-1.0.0/PKG-INFO +177 -0
- pymal_ng-1.0.0/README.md +152 -0
- pymal_ng-1.0.0/pymal/__init__.py +9 -0
- pymal_ng-1.0.0/pymal/account.py +270 -0
- pymal_ng-1.0.0/pymal/account_objects/__init__.py +1 -0
- pymal_ng-1.0.0/pymal/account_objects/account_animes.py +181 -0
- pymal_ng-1.0.0/pymal/account_objects/account_friends.py +88 -0
- pymal_ng-1.0.0/pymal/account_objects/account_mangas.py +160 -0
- pymal_ng-1.0.0/pymal/account_objects/my_anime.py +715 -0
- pymal_ng-1.0.0/pymal/account_objects/my_manga.py +738 -0
- pymal_ng-1.0.0/pymal/anime.py +640 -0
- pymal_ng-1.0.0/pymal/consts.py +19 -0
- pymal_ng-1.0.0/pymal/decorators.py +34 -0
- pymal_ng-1.0.0/pymal/exceptions/__init__.py +29 -0
- pymal_ng-1.0.0/pymal/exceptions/failed_to_parse_error.py +18 -0
- pymal_ng-1.0.0/pymal/exceptions/my_anime_list_api_error.py +27 -0
- pymal_ng-1.0.0/pymal/global_functions.py +215 -0
- pymal_ng-1.0.0/pymal/inner_objects/__init__.py +4 -0
- pymal_ng-1.0.0/pymal/inner_objects/recommendation.py +51 -0
- pymal_ng-1.0.0/pymal/inner_objects/review.py +52 -0
- pymal_ng-1.0.0/pymal/inner_objects/season.py +102 -0
- pymal_ng-1.0.0/pymal/manga.py +587 -0
- pymal_ng-1.0.0/pymal/searches/README.md +15 -0
- pymal_ng-1.0.0/pymal/searches/__init__.py +47 -0
- pymal_ng-1.0.0/pymal/searches/search_accounts_provider.py +23 -0
- pymal_ng-1.0.0/pymal/searches/search_animes_provider.py +24 -0
- pymal_ng-1.0.0/pymal/searches/search_mangas_provider.py +24 -0
- pymal_ng-1.0.0/pymal/searches/search_provider.py +96 -0
- pymal_ng-1.0.0/pymal/seasons.py +73 -0
- pymal_ng-1.0.0/pymal/types/README.md +13 -0
- pymal_ng-1.0.0/pymal/types/ReloadedSet.py +110 -0
- pymal_ng-1.0.0/pymal/types/__init__.py +4 -0
- pymal_ng-1.0.0/pyproject.toml +115 -0
- pymal_ng-1.0.0/pyproject.toml.orig +80 -0
pymal_ng-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Copyright (c) 2014, tomergelber
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
|
6
|
+
|
|
7
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
8
|
+
list of conditions and the following disclaimer.
|
|
9
|
+
|
|
10
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
11
|
+
this list of conditions and the following disclaimer in the documentation
|
|
12
|
+
and/or other materials provided with the distribution.
|
|
13
|
+
|
|
14
|
+
* Neither the name of the {organization} nor the names of its
|
|
15
|
+
contributors may be used to endorse or promote products derived from
|
|
16
|
+
this software without specific prior written permission.
|
|
17
|
+
|
|
18
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
19
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
20
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
22
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
23
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
24
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
25
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
26
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
27
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
pymal_ng-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pymal-ng
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A python api for the website MyAnimeList (or MAL).
|
|
5
|
+
Keywords: myanimelist,mal,anime,manga,api
|
|
6
|
+
Author: varyg
|
|
7
|
+
License-Expression: BSD-3-Clause
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Requires-Dist: niquests[socks,utls]>=3.13.1,<4.0.0
|
|
16
|
+
Requires-Dist: beautifulsoup4>=4.12.3,<5.0.0
|
|
17
|
+
Requires-Dist: lxml>=6.0.0,<7.0.0
|
|
18
|
+
Requires-Dist: pillow>=12.3.0
|
|
19
|
+
Requires-Dist: singleton-factory>=0.1
|
|
20
|
+
Requires-Python: >=3.10, <3.15
|
|
21
|
+
Project-URL: Homepage, https://github.com/varyg1001/pymal
|
|
22
|
+
Project-URL: Source, https://github.com/varyg1001/pymal
|
|
23
|
+
Project-URL: Upstream, https://github.com/pymal-developers/pymal
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# pymal
|
|
27
|
+
|
|
28
|
+
Programmatic access to [MyAnimeList](https://myanimelist.net) data in Python.
|
|
29
|
+
Objects are **lazy-loading** — MAL is only contacted when you first access a property.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from pymal import anime
|
|
37
|
+
|
|
38
|
+
# Lazy — no network call yet
|
|
39
|
+
a = anime.Anime(52991)
|
|
40
|
+
|
|
41
|
+
# First property access triggers the MAL fetch
|
|
42
|
+
print(a.title) # "Sousou no Frieren"
|
|
43
|
+
print(a.score) # 9.07
|
|
44
|
+
print(a.episodes) # 28
|
|
45
|
+
print(a.genres) # {'Adventure': '...', 'Drama': '...', 'Fantasy': '...'}
|
|
46
|
+
print(a.english) # "Frieren: Beyond Journey's End"
|
|
47
|
+
print(a.japanese) # "葬送のフリーレン"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
### With uv (recommended)
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh # install uv once
|
|
58
|
+
|
|
59
|
+
git clone https://github.com/varyg1001/pymal
|
|
60
|
+
cd pymal
|
|
61
|
+
uv sync
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### With pip
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
git clone https://github.com/varyg1001/pymal
|
|
68
|
+
cd pymal
|
|
69
|
+
pip install -e .
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Requirements
|
|
75
|
+
|
|
76
|
+
- Python `>=3.10, <3.15`
|
|
77
|
+
- [`uv`](https://github.com/astral-sh/uv) (recommended) or pip
|
|
78
|
+
|
|
79
|
+
Key runtime dependencies (full list in `pyproject.toml`):
|
|
80
|
+
|
|
81
|
+
| Package | Purpose |
|
|
82
|
+
|---------|---------|
|
|
83
|
+
| `niquests[socks,utls]` | HTTP client (modern drop-in for `requests`) |
|
|
84
|
+
| `beautifulsoup4` + `lxml` | HTML parsing |
|
|
85
|
+
| `singleton-factory` | Object singleton lifecycle |
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Search & Endpoint Features
|
|
90
|
+
|
|
91
|
+
- **Search API**: Search providers (`SearchAnimesProvider`, `SearchMangasProvider`) automatically query MyAnimeList's modern `prefix.json` API endpoint (`/search/prefix.json`), falling back gracefully if needed.
|
|
92
|
+
- **Seasons**: Seasonal anime lookups (`Season`, `Seasons`) scrape directly from native MyAnimeList season pages (`/anime/season/<year>/<season>`) and the archive listing (`/anime/season/archive`).
|
|
93
|
+
- **User Lists**: Account anime and manga lists (`AccountAnimes`, `AccountMangas`) utilize MAL's modern JSON list endpoints (`load.json`).
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Running tests
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# Offline suite — no network or account needed (40 tests)
|
|
101
|
+
uv run pytest tests/test_fixes.py -v
|
|
102
|
+
|
|
103
|
+
# Full suite — account tests auto-skip unless credentials are set
|
|
104
|
+
MAL_USERNAME=you MAL_PASSWORD=secret uv run pytest -v
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Project structure
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
pymal/
|
|
113
|
+
├── pymal/
|
|
114
|
+
│ ├── anime.py # Anime object
|
|
115
|
+
│ ├── manga.py # Manga object
|
|
116
|
+
│ ├── account.py # MAL account & auth
|
|
117
|
+
│ ├── global_functions.py # HTTP + HTML parsing helpers
|
|
118
|
+
│ ├── account_objects/ # MyAnime, MyManga, lists, friends
|
|
119
|
+
│ ├── searches/ # Search providers
|
|
120
|
+
│ └── inner_objects/ # Season, recommendation, etc.
|
|
121
|
+
├── tests/
|
|
122
|
+
│ ├── test_fixes.py # Offline test suite (40 tests)
|
|
123
|
+
│ └── account_objects/ # Live account tests (skipped without creds)
|
|
124
|
+
├── docs/ # MkDocs documentation source
|
|
125
|
+
├── mkdocs.yml # Docs config (GitHub Pages)
|
|
126
|
+
├── pyproject.toml # Build config & pinned deps (uv_build)
|
|
127
|
+
└── uv.lock # Lockfile — commit this
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## What changed in v1.0.0
|
|
133
|
+
|
|
134
|
+
The original project (v0.5b4–v0.6) had broken against MAL's HTML updates and
|
|
135
|
+
modern Python. v1.0.0 brings it back to life:
|
|
136
|
+
|
|
137
|
+
| # | Area | Fix |
|
|
138
|
+
|---|------|-----|
|
|
139
|
+
| 1 | Parser | `html5lib` → `lxml` (no more `six` conflict) |
|
|
140
|
+
| 2 | HTTP | `requests` → `niquests[socks,utls]` (HTTP/2, QUIC, modern TLS) |
|
|
141
|
+
| 3 | Sidebar | Full rewrite from brittle index-based to label-based lookup (`span.dark_text`) |
|
|
142
|
+
| 4 | Title | Fixed `IndexError` — MAL now wraps `<h1>` content in `<strong>` |
|
|
143
|
+
| 5 | Table | Fixed `AttributeError` — MAL removed `<tbody>` from tables |
|
|
144
|
+
| 6 | Search | Migrated from legacy `anime.php` to MAL's modern `prefix.json` API |
|
|
145
|
+
| 7 | Seasons | Migrated from dead `malupdater.com` to native MAL `/anime/season` pages |
|
|
146
|
+
| 8 | User Lists | Updated `AccountAnimes` & `AccountMangas` to MAL's `load.json` API |
|
|
147
|
+
| 9 | Build | `setup.py` → `uv_build` + `pyproject.toml` |
|
|
148
|
+
| 10 | Tests | New offline suite with 40 tests; account tests skip without credentials |
|
|
149
|
+
| 11 | Docs | Migrated from Sphinx/RST to MkDocs Material (GitHub Pages) |
|
|
150
|
+
| 12 | Cleanup | Removed `.travis.yml`, `MANIFEST.in`, `upload_new_version.bat`, dead HTML fixtures |
|
|
151
|
+
|
|
152
|
+
Also fixed latent bugs: `==` used instead of `=` in `my_anime.py` and
|
|
153
|
+
`my_manga.py` meant `download_episodes` and `times_rewatched` were never
|
|
154
|
+
actually stored.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Credits
|
|
159
|
+
|
|
160
|
+
### Original authors
|
|
161
|
+
|
|
162
|
+
pymal was created and developed by:
|
|
163
|
+
|
|
164
|
+
- **tomer gelber** — [tomergelber@gmail.com](mailto:tomergelber@gmail.com)
|
|
165
|
+
- **Aur Saraf** — [sonoflilit@gmail.com](mailto:sonoflilit@gmail.com)
|
|
166
|
+
|
|
167
|
+
Original repository: [pymal-developers/pymal](https://github.com/pymal-developers/pymal)
|
|
168
|
+
|
|
169
|
+
### v1.0.0
|
|
170
|
+
|
|
171
|
+
The update was made with mostly AI. The main reason for the update is the errors cause by the old things used in the last released version. I still feel that this is the easiest way to get info from MAL, even with the original version, since MAL has not changed much.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
BSD License — see original repository for full text.
|
pymal_ng-1.0.0/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# pymal
|
|
2
|
+
|
|
3
|
+
Programmatic access to [MyAnimeList](https://myanimelist.net) data in Python.
|
|
4
|
+
Objects are **lazy-loading** — MAL is only contacted when you first access a property.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Quick start
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from pymal import anime
|
|
12
|
+
|
|
13
|
+
# Lazy — no network call yet
|
|
14
|
+
a = anime.Anime(52991)
|
|
15
|
+
|
|
16
|
+
# First property access triggers the MAL fetch
|
|
17
|
+
print(a.title) # "Sousou no Frieren"
|
|
18
|
+
print(a.score) # 9.07
|
|
19
|
+
print(a.episodes) # 28
|
|
20
|
+
print(a.genres) # {'Adventure': '...', 'Drama': '...', 'Fantasy': '...'}
|
|
21
|
+
print(a.english) # "Frieren: Beyond Journey's End"
|
|
22
|
+
print(a.japanese) # "葬送のフリーレン"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
### With uv (recommended)
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh # install uv once
|
|
33
|
+
|
|
34
|
+
git clone https://github.com/varyg1001/pymal
|
|
35
|
+
cd pymal
|
|
36
|
+
uv sync
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### With pip
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
git clone https://github.com/varyg1001/pymal
|
|
43
|
+
cd pymal
|
|
44
|
+
pip install -e .
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Requirements
|
|
50
|
+
|
|
51
|
+
- Python `>=3.10, <3.15`
|
|
52
|
+
- [`uv`](https://github.com/astral-sh/uv) (recommended) or pip
|
|
53
|
+
|
|
54
|
+
Key runtime dependencies (full list in `pyproject.toml`):
|
|
55
|
+
|
|
56
|
+
| Package | Purpose |
|
|
57
|
+
|---------|---------|
|
|
58
|
+
| `niquests[socks,utls]` | HTTP client (modern drop-in for `requests`) |
|
|
59
|
+
| `beautifulsoup4` + `lxml` | HTML parsing |
|
|
60
|
+
| `singleton-factory` | Object singleton lifecycle |
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Search & Endpoint Features
|
|
65
|
+
|
|
66
|
+
- **Search API**: Search providers (`SearchAnimesProvider`, `SearchMangasProvider`) automatically query MyAnimeList's modern `prefix.json` API endpoint (`/search/prefix.json`), falling back gracefully if needed.
|
|
67
|
+
- **Seasons**: Seasonal anime lookups (`Season`, `Seasons`) scrape directly from native MyAnimeList season pages (`/anime/season/<year>/<season>`) and the archive listing (`/anime/season/archive`).
|
|
68
|
+
- **User Lists**: Account anime and manga lists (`AccountAnimes`, `AccountMangas`) utilize MAL's modern JSON list endpoints (`load.json`).
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Running tests
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Offline suite — no network or account needed (40 tests)
|
|
76
|
+
uv run pytest tests/test_fixes.py -v
|
|
77
|
+
|
|
78
|
+
# Full suite — account tests auto-skip unless credentials are set
|
|
79
|
+
MAL_USERNAME=you MAL_PASSWORD=secret uv run pytest -v
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Project structure
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
pymal/
|
|
88
|
+
├── pymal/
|
|
89
|
+
│ ├── anime.py # Anime object
|
|
90
|
+
│ ├── manga.py # Manga object
|
|
91
|
+
│ ├── account.py # MAL account & auth
|
|
92
|
+
│ ├── global_functions.py # HTTP + HTML parsing helpers
|
|
93
|
+
│ ├── account_objects/ # MyAnime, MyManga, lists, friends
|
|
94
|
+
│ ├── searches/ # Search providers
|
|
95
|
+
│ └── inner_objects/ # Season, recommendation, etc.
|
|
96
|
+
├── tests/
|
|
97
|
+
│ ├── test_fixes.py # Offline test suite (40 tests)
|
|
98
|
+
│ └── account_objects/ # Live account tests (skipped without creds)
|
|
99
|
+
├── docs/ # MkDocs documentation source
|
|
100
|
+
├── mkdocs.yml # Docs config (GitHub Pages)
|
|
101
|
+
├── pyproject.toml # Build config & pinned deps (uv_build)
|
|
102
|
+
└── uv.lock # Lockfile — commit this
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## What changed in v1.0.0
|
|
108
|
+
|
|
109
|
+
The original project (v0.5b4–v0.6) had broken against MAL's HTML updates and
|
|
110
|
+
modern Python. v1.0.0 brings it back to life:
|
|
111
|
+
|
|
112
|
+
| # | Area | Fix |
|
|
113
|
+
|---|------|-----|
|
|
114
|
+
| 1 | Parser | `html5lib` → `lxml` (no more `six` conflict) |
|
|
115
|
+
| 2 | HTTP | `requests` → `niquests[socks,utls]` (HTTP/2, QUIC, modern TLS) |
|
|
116
|
+
| 3 | Sidebar | Full rewrite from brittle index-based to label-based lookup (`span.dark_text`) |
|
|
117
|
+
| 4 | Title | Fixed `IndexError` — MAL now wraps `<h1>` content in `<strong>` |
|
|
118
|
+
| 5 | Table | Fixed `AttributeError` — MAL removed `<tbody>` from tables |
|
|
119
|
+
| 6 | Search | Migrated from legacy `anime.php` to MAL's modern `prefix.json` API |
|
|
120
|
+
| 7 | Seasons | Migrated from dead `malupdater.com` to native MAL `/anime/season` pages |
|
|
121
|
+
| 8 | User Lists | Updated `AccountAnimes` & `AccountMangas` to MAL's `load.json` API |
|
|
122
|
+
| 9 | Build | `setup.py` → `uv_build` + `pyproject.toml` |
|
|
123
|
+
| 10 | Tests | New offline suite with 40 tests; account tests skip without credentials |
|
|
124
|
+
| 11 | Docs | Migrated from Sphinx/RST to MkDocs Material (GitHub Pages) |
|
|
125
|
+
| 12 | Cleanup | Removed `.travis.yml`, `MANIFEST.in`, `upload_new_version.bat`, dead HTML fixtures |
|
|
126
|
+
|
|
127
|
+
Also fixed latent bugs: `==` used instead of `=` in `my_anime.py` and
|
|
128
|
+
`my_manga.py` meant `download_episodes` and `times_rewatched` were never
|
|
129
|
+
actually stored.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Credits
|
|
134
|
+
|
|
135
|
+
### Original authors
|
|
136
|
+
|
|
137
|
+
pymal was created and developed by:
|
|
138
|
+
|
|
139
|
+
- **tomer gelber** — [tomergelber@gmail.com](mailto:tomergelber@gmail.com)
|
|
140
|
+
- **Aur Saraf** — [sonoflilit@gmail.com](mailto:sonoflilit@gmail.com)
|
|
141
|
+
|
|
142
|
+
Original repository: [pymal-developers/pymal](https://github.com/pymal-developers/pymal)
|
|
143
|
+
|
|
144
|
+
### v1.0.0
|
|
145
|
+
|
|
146
|
+
The update was made with mostly AI. The main reason for the update is the errors cause by the old things used in the last released version. I still feel that this is the easiest way to get info from MAL, even with the original version, since MAL has not changed much.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
BSD License — see original repository for full text.
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
__authors__ = ""
|
|
2
|
+
__copyright__ = "(c) 2014, pymal"
|
|
3
|
+
__license__ = "BSD License"
|
|
4
|
+
__contact__ = "Name Of Current Guardian of this file <email@address>"
|
|
5
|
+
|
|
6
|
+
from urllib import request
|
|
7
|
+
|
|
8
|
+
import niquests
|
|
9
|
+
import singleton_factory
|
|
10
|
+
|
|
11
|
+
from pymal import global_functions
|
|
12
|
+
from pymal.consts import HOST_NAME
|
|
13
|
+
from pymal.decorators import load
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = ["Account"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Account(metaclass=singleton_factory.SingletonFactory):
|
|
20
|
+
"""
|
|
21
|
+
Object that keeps all the account data in MAL.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
__AUTH_CHECKER_URL = request.urljoin(HOST_NAME, r"api/account/verify_credentials.xml")
|
|
25
|
+
|
|
26
|
+
__MY_LOGIN_URL = request.urljoin(HOST_NAME, "login.php")
|
|
27
|
+
__DATA_FORM = "user_name={0:s}&password={1:s}&cookie=1&sublogin=Login&submit=1"
|
|
28
|
+
|
|
29
|
+
def __init__(self, username: str, password: str | None = None):
|
|
30
|
+
"""
|
|
31
|
+
:param username: The account username.
|
|
32
|
+
:type username: str
|
|
33
|
+
:param password: Required for quick connection, instead of calling later change_password.
|
|
34
|
+
:type password: str or None
|
|
35
|
+
"""
|
|
36
|
+
from pymal.account_objects import account_animes, account_mangas
|
|
37
|
+
|
|
38
|
+
self.__username = username
|
|
39
|
+
self.__password = password
|
|
40
|
+
self.connect = global_functions.connect
|
|
41
|
+
self.__user_id = None
|
|
42
|
+
self.__auth_object = None
|
|
43
|
+
|
|
44
|
+
self._main_profile_url = request.urljoin(HOST_NAME, f"profile/{self.username:s}")
|
|
45
|
+
|
|
46
|
+
self.__animes = account_animes.AccountAnimes(self)
|
|
47
|
+
self.__mangas = account_mangas.AccountMangas(self)
|
|
48
|
+
self.__friends = None
|
|
49
|
+
self.__image_url = ""
|
|
50
|
+
|
|
51
|
+
self.__session = global_functions.generate_session()
|
|
52
|
+
|
|
53
|
+
self._is_loaded = False
|
|
54
|
+
|
|
55
|
+
if password is not None:
|
|
56
|
+
self.change_password(password)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def username(self) -> str:
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
:returns: the user name.
|
|
63
|
+
:rtype: str
|
|
64
|
+
"""
|
|
65
|
+
return self.__username
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def user_id(self) -> int:
|
|
69
|
+
"""
|
|
70
|
+
:returns: the user id. If unknown loading it.
|
|
71
|
+
:rtype: int
|
|
72
|
+
"""
|
|
73
|
+
if self.__user_id is None:
|
|
74
|
+
import bs4
|
|
75
|
+
|
|
76
|
+
ret = self.connect(self._main_profile_url)
|
|
77
|
+
html = bs4.BeautifulSoup(ret)
|
|
78
|
+
bla = html.find(name="input", attrs={"name": "profileMemId"})
|
|
79
|
+
self.__user_id = int(bla["value"])
|
|
80
|
+
return self.__user_id
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def mangas(self):
|
|
84
|
+
"""
|
|
85
|
+
:returns: list of account's mangas.
|
|
86
|
+
:rtype: :class:`account_objects.account_mangas.AccountMangas`
|
|
87
|
+
"""
|
|
88
|
+
return self.__mangas
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def animes(self):
|
|
92
|
+
"""
|
|
93
|
+
:returns: list of account's animes.
|
|
94
|
+
:rtype: :class:`account_objects.account_animes.AccountAnimes`
|
|
95
|
+
"""
|
|
96
|
+
return self.__animes
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def friends(self) -> set:
|
|
100
|
+
"""
|
|
101
|
+
:returns: list of account's friends.
|
|
102
|
+
:rtype: :class:`account_objects.account_friends.AccountFriends`
|
|
103
|
+
"""
|
|
104
|
+
from pymal.account_objects import account_friends
|
|
105
|
+
|
|
106
|
+
if self.__friends is None:
|
|
107
|
+
self.__friends = account_friends.AccountFriends(self)
|
|
108
|
+
return self.__friends
|
|
109
|
+
|
|
110
|
+
def search(self, search_line: str, is_anime: bool = True) -> map:
|
|
111
|
+
"""
|
|
112
|
+
Searching like regular search but switching all the object in "my" lists to the "my" objects.
|
|
113
|
+
|
|
114
|
+
:param search_line: the search line.
|
|
115
|
+
:type search_line: str
|
|
116
|
+
:param is_anime: True is searching for anime, False for manga.
|
|
117
|
+
:type is_anime: bool
|
|
118
|
+
:returns: searched objects
|
|
119
|
+
:rtype: map
|
|
120
|
+
"""
|
|
121
|
+
from pymal import searches
|
|
122
|
+
|
|
123
|
+
if is_anime:
|
|
124
|
+
results = searches.search_animes(search_line)
|
|
125
|
+
account_object_list = self.animes
|
|
126
|
+
else:
|
|
127
|
+
results = searches.search_mangas(search_line)
|
|
128
|
+
account_object_list = self.mangas
|
|
129
|
+
|
|
130
|
+
def get_object(result):
|
|
131
|
+
if result not in account_object_list:
|
|
132
|
+
return result
|
|
133
|
+
# if account_object_list was set:
|
|
134
|
+
# return account_object_list.intersection([result]).pop()
|
|
135
|
+
return list(filter(lambda x: x == result, account_object_list))[0]
|
|
136
|
+
|
|
137
|
+
return map(get_object, results)
|
|
138
|
+
|
|
139
|
+
def change_password(self, password: str) -> bool:
|
|
140
|
+
"""
|
|
141
|
+
Checking if the new password is valid
|
|
142
|
+
|
|
143
|
+
:param password: password
|
|
144
|
+
:type password: str
|
|
145
|
+
:returns: True if password is right.
|
|
146
|
+
:rtype: bool
|
|
147
|
+
:exception exceptions.FailedToParseError: when failed
|
|
148
|
+
"""
|
|
149
|
+
from xml.etree import ElementTree
|
|
150
|
+
|
|
151
|
+
from niquests.auth import HTTPBasicAuth
|
|
152
|
+
|
|
153
|
+
from pymal import exceptions
|
|
154
|
+
|
|
155
|
+
self.__auth_object = HTTPBasicAuth(self.username, password)
|
|
156
|
+
data = self.auth_connect(self.__AUTH_CHECKER_URL)
|
|
157
|
+
if data == "Invalid credentials":
|
|
158
|
+
self.__auth_object = None
|
|
159
|
+
self.__password = None
|
|
160
|
+
return False
|
|
161
|
+
xml_user = ElementTree.fromstring(data)
|
|
162
|
+
|
|
163
|
+
if xml_user.tag != "user":
|
|
164
|
+
raise exceptions.FailedToParseError(f"user == {xml_user.tag:s}")
|
|
165
|
+
xml_children = list(xml_user)
|
|
166
|
+
xml_username = xml_children[1]
|
|
167
|
+
if xml_username.tag != "username":
|
|
168
|
+
raise exceptions.FailedToParseError(f"username == {xml_username.tag:s}")
|
|
169
|
+
if self.username != xml_username.text.strip():
|
|
170
|
+
raise exceptions.FailedToParseError(
|
|
171
|
+
f"username = {xml_username.text.strip():s}"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
xml_id = xml_children[0]
|
|
175
|
+
if xml_id.tag != "id":
|
|
176
|
+
raise exceptions.FailedToParseError(f"id == {xml_id.tag:s}")
|
|
177
|
+
if self.user_id != int(xml_id.text):
|
|
178
|
+
raise exceptions.FailedToParseError()
|
|
179
|
+
|
|
180
|
+
self.__password = password
|
|
181
|
+
|
|
182
|
+
data_form = self.__DATA_FORM.format(self.username, password).encode("utf-8")
|
|
183
|
+
headers = {
|
|
184
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
185
|
+
"name": "loginForm",
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
self.auth_connect(self.__MY_LOGIN_URL, data=data_form, headers=headers)
|
|
189
|
+
|
|
190
|
+
return True
|
|
191
|
+
|
|
192
|
+
def auth_connect(
|
|
193
|
+
self, url: str, data: str | None = None, headers: dict | None = None
|
|
194
|
+
) -> str:
|
|
195
|
+
"""
|
|
196
|
+
:param url: The url to get.
|
|
197
|
+
:type url: str
|
|
198
|
+
:param data: The data to pass (will change the request to "POST")
|
|
199
|
+
:type data: str or None
|
|
200
|
+
:param headers: Headers for the request.
|
|
201
|
+
:type headers: dict or None
|
|
202
|
+
:returns: data after using the account authenticate to get the data.
|
|
203
|
+
:rtype: str
|
|
204
|
+
"""
|
|
205
|
+
from pymal import exceptions
|
|
206
|
+
|
|
207
|
+
if not self.is_auth:
|
|
208
|
+
raise exceptions.UnauthenticatedAccountError(self.username)
|
|
209
|
+
return global_functions._connect(
|
|
210
|
+
url,
|
|
211
|
+
data=data,
|
|
212
|
+
headers=headers,
|
|
213
|
+
auth=self.__auth_object,
|
|
214
|
+
session=self.__session,
|
|
215
|
+
).text.strip()
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
@load
|
|
219
|
+
def image_url(self):
|
|
220
|
+
"""
|
|
221
|
+
:return: path for the image of the avatar of the account.
|
|
222
|
+
:rtype: str
|
|
223
|
+
"""
|
|
224
|
+
return self.__image_url
|
|
225
|
+
|
|
226
|
+
def get_image(self):
|
|
227
|
+
"""
|
|
228
|
+
:return: image of the account's avatar
|
|
229
|
+
:rtype: :class:`PIL.Image.Image`
|
|
230
|
+
"""
|
|
231
|
+
import io
|
|
232
|
+
|
|
233
|
+
from PIL import Image
|
|
234
|
+
|
|
235
|
+
sock = niquests.get(self.image_url)
|
|
236
|
+
data = io.BytesIO(sock.content)
|
|
237
|
+
return Image.open(data)
|
|
238
|
+
|
|
239
|
+
def reload(self):
|
|
240
|
+
"""
|
|
241
|
+
reloading account image (all the other things are already lazy load!
|
|
242
|
+
"""
|
|
243
|
+
div = global_functions.get_content_wrapper_div(
|
|
244
|
+
self._main_profile_url, self.connect
|
|
245
|
+
)
|
|
246
|
+
profile_leftcell = div.table.tbody.tr.find(
|
|
247
|
+
name="td", attrs={"class": "profile_leftcell"}, recursive=False
|
|
248
|
+
)
|
|
249
|
+
self.__image_url = profile_leftcell.div.img["src"]
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def is_auth(self) -> bool:
|
|
253
|
+
"""
|
|
254
|
+
:returns: True if the password is right (and able to authenticate).
|
|
255
|
+
:rtype: bool
|
|
256
|
+
"""
|
|
257
|
+
return self.__auth_object is not None
|
|
258
|
+
|
|
259
|
+
def __repr__(self):
|
|
260
|
+
return f"<Account username: {self.username:s}>"
|
|
261
|
+
|
|
262
|
+
def __hash__(self):
|
|
263
|
+
import hashlib
|
|
264
|
+
|
|
265
|
+
hash_md5 = hashlib.md5()
|
|
266
|
+
hash_md5.update(self.username.encode())
|
|
267
|
+
return int(hash_md5.hexdigest(), 16)
|
|
268
|
+
|
|
269
|
+
def __format__(self, format_spec):
|
|
270
|
+
return str(self).__format__(format_spec)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__author__ = "user"
|