pyvko 0.0.1__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.
pyvko/__init__.py ADDED
File without changes
pyvko/api_based.py ADDED
@@ -0,0 +1,57 @@
1
+ from abc import abstractmethod, ABC
2
+ from typing import Any, Dict
3
+
4
+ from vk_api import VkApi
5
+
6
+
7
+ class RequestRoot:
8
+ def get_request(self, parameters: Dict | None = None):
9
+ return parameters or {}
10
+
11
+
12
+ class ApiMixin(RequestRoot, ABC):
13
+ @property
14
+ @abstractmethod
15
+ def new_api(self) -> VkApi:
16
+ pass
17
+
18
+ @property
19
+ def api(self) -> Any:
20
+ assert False, f"{type(self).__name__} still uses unmigrated api — switch to new_api"
21
+
22
+ @abstractmethod
23
+ def get_request(self, parameters: Dict | None = None) -> Dict:
24
+ return super().get_request(parameters)
25
+
26
+
27
+ class ApiBased(RequestRoot):
28
+ __VERSION = 5.199
29
+
30
+ def __init__(self, api: Any) -> None:
31
+ super().__init__()
32
+
33
+ self.__api = api
34
+
35
+ @property
36
+ def new_api(self) -> Any:
37
+ return self.__api
38
+
39
+ @property
40
+ def api(self) -> Any:
41
+ assert False, f"{type(self).__name__} still uses unmigrated api — switch to new_api"
42
+
43
+ @staticmethod
44
+ def __get_default_object():
45
+ return {
46
+ # "v": ApiBased.__VERSION,
47
+ }
48
+
49
+ def get_request(self, parameters: Dict | None = None) -> Dict:
50
+ if parameters is None:
51
+ parameters = {}
52
+
53
+ assert "v" not in parameters
54
+
55
+ request = ApiBased.__get_default_object() | parameters | super().get_request()
56
+
57
+ return request
File without changes
@@ -0,0 +1,139 @@
1
+ from abc import abstractmethod, ABC
2
+ from pathlib import Path
3
+ from typing import Any, Dict, List
4
+
5
+
6
+ from pyvko.api_based import ApiMixin, ApiBased
7
+ from pyvko.attachment.attachment import Attachment
8
+ from pyvko.attachment.photo import Photo
9
+ from pyvko.shared.photos_uploader import AlbumPhotoUploader
10
+ from pyvko.shared.utils import get_all
11
+
12
+
13
+ class Album(ApiBased, Attachment):
14
+ def __init__(self, api: Any, api_object: dict) -> None:
15
+ super().__init__(api)
16
+
17
+ self.__name = api_object["title"]
18
+ self.__id = api_object["id"]
19
+ self.__owner_id = api_object["owner_id"]
20
+
21
+ @property
22
+ def name(self) -> str:
23
+ return self.__name
24
+
25
+ @property
26
+ def id(self) -> int:
27
+ return self.__id
28
+
29
+ @property
30
+ def url(self) -> str:
31
+ return f"https://vk.com/album{self.owner_id}_{self.id}"
32
+
33
+ def get_photos(self) -> List[Photo]:
34
+ parameters = self.get_request()
35
+
36
+ photos_descriptions = get_all(parameters, self.api.photos.get)
37
+
38
+ photos = [Photo.from_photo_object(self.api, photo_object) for photo_object in photos_descriptions]
39
+
40
+ return photos
41
+
42
+ def get_request(self, parameters: Dict | None = None) -> dict:
43
+ if parameters is None:
44
+ parameters = {}
45
+
46
+ parameters |= {
47
+ "owner_id": self.__owner_id,
48
+ "album_id": self.__id
49
+ }
50
+
51
+ return super().get_request(parameters)
52
+
53
+ def set_cover(self, cover: Photo):
54
+ request = self.get_request({
55
+ "photo_id": cover.id
56
+ })
57
+
58
+ self.api.photos.makeCover(**request)
59
+
60
+ def add_photo(self, path: Path) -> Photo:
61
+ uploader = AlbumPhotoUploader(self.api, self.id, -self.__owner_id)
62
+
63
+ return uploader.upload(path)
64
+
65
+ # region Attachment
66
+
67
+ @property
68
+ def type(self) -> str:
69
+ return "album"
70
+
71
+ @property
72
+ def owner_id(self) -> int:
73
+ return self.__owner_id
74
+
75
+ @property
76
+ def media_id(self) -> int:
77
+ return self.id
78
+
79
+ # endregion Attachment
80
+
81
+
82
+ class Albums(ApiMixin, ABC):
83
+ @property
84
+ @abstractmethod
85
+ def id(self) -> int:
86
+ pass
87
+
88
+ def __get_albums(self, parameters: Dict | None = None) -> List[Album]:
89
+ request = self.__get_owned_request(parameters)
90
+
91
+ result = self.api.photos.getAlbums(**request)
92
+
93
+ albums = [Album(self.api, album_object) for album_object in result["items"]]
94
+
95
+ return albums
96
+
97
+ def get_all_albums(self) -> List[Album]:
98
+ return self.__get_albums()
99
+
100
+ def get_album_by_id(self, album_id: int) -> Album | None:
101
+ albums_list = self.__get_albums({
102
+ "album_ids": [album_id]
103
+ })
104
+
105
+ if len(albums_list) == 1:
106
+ return albums_list[0]
107
+ elif len(albums_list) == 0:
108
+ return None
109
+ else:
110
+ assert False
111
+
112
+ def create_album(self, name: str) -> Album:
113
+ parameters = {
114
+ "title": name,
115
+ "group_id": abs(self.id),
116
+ "upload_by_admins_only": 1
117
+ }
118
+
119
+ parameters = self.get_request(parameters)
120
+
121
+ response = self.api.photos.createAlbum(**parameters)
122
+
123
+ created_album = Album(self.api, response)
124
+
125
+ return created_album
126
+
127
+ def __get_owned_request(self, parameters: Dict | None = None) -> dict:
128
+ if parameters is None:
129
+ parameters = {}
130
+ else:
131
+ parameters = parameters.copy()
132
+
133
+ assert "owner_id" not in parameters
134
+
135
+ parameters.update({
136
+ "owner_id": self.id
137
+ })
138
+
139
+ return self.get_request(parameters)
@@ -0,0 +1,143 @@
1
+ from abc import abstractmethod, ABC
2
+ from dataclasses import dataclass
3
+ from datetime import datetime
4
+ from typing import List, Dict, Any
5
+
6
+ from pyvko.api_based import ApiMixin, ApiBased
7
+ from pyvko.attachment.attachment import Attachment
8
+ from pyvko.attachment.attachment_parser import AttachmentParser
9
+ from pyvko.aspects.likes import Likes
10
+ from pyvko.shared.utils import get_all
11
+
12
+
13
+ @dataclass
14
+ class CommentModel:
15
+ text: str
16
+ from_group: int = 0
17
+
18
+ attachments: List[Attachment] | None = None
19
+
20
+ def to_request(self):
21
+ request = {}
22
+
23
+ if self.text:
24
+ request["message"] = self.text
25
+
26
+ if self.from_group != 0:
27
+ request["from_group"] = self.from_group
28
+
29
+ if self.attachments:
30
+ request["attachments"] = ",".join([a.to_attach() for a in self.attachments])
31
+
32
+ return request
33
+
34
+
35
+ class Comment(ApiBased, Likes):
36
+ @property
37
+ def item_id(self) -> int:
38
+ return self.id
39
+
40
+ @property
41
+ def like_object_type(self) -> str:
42
+ return "comment"
43
+
44
+ @property
45
+ def owner_id(self) -> int:
46
+ return self.__owner_id
47
+
48
+ @property
49
+ def id(self):
50
+ return self.__id
51
+
52
+ @property
53
+ def text(self) -> str:
54
+ return self.__text
55
+
56
+ @property
57
+ def date(self) -> datetime:
58
+ return self.__date
59
+
60
+ def __init__(
61
+ self,
62
+ api: Any,
63
+ comment_id: int,
64
+ owner_id: int,
65
+ date: datetime,
66
+ text: str,
67
+ attachments: List[Attachment] | None
68
+ ) -> None:
69
+ super().__init__(api)
70
+
71
+ self.__id = comment_id
72
+ self.__owner_id = owner_id
73
+ self.__date = date
74
+ self.__text = text
75
+ self.__attachments = attachments
76
+
77
+ @classmethod
78
+ def from_api_object(cls, api_object: Dict, api: Any) -> 'Comment':
79
+ if "attachments" in api_object:
80
+ parser = AttachmentParser.shared()
81
+
82
+ attachments = [parser.parse_object(o, api) for o in api_object["attachments"]]
83
+ else:
84
+ attachments = None
85
+
86
+ return Comment(
87
+ api=api,
88
+ comment_id=api_object["id"],
89
+ owner_id=api_object["owner_id"],
90
+ date=datetime.fromtimestamp(api_object["date"]),
91
+ text=api_object["text"],
92
+ attachments=attachments
93
+ )
94
+
95
+
96
+ class Comments(ApiMixin, ABC):
97
+ @property
98
+ @abstractmethod
99
+ def post_id(self) -> int:
100
+ pass
101
+
102
+ @property
103
+ @abstractmethod
104
+ def owner_id(self) -> int:
105
+ pass
106
+
107
+ def add_comment(self, comment: CommentModel) -> Comment:
108
+ add_comment_request = self.get_request(comment.to_request() | {
109
+ "post_id": self.post_id,
110
+ "owner_id": self.owner_id,
111
+ })
112
+
113
+ add_comment_response = self.api.wall.createComment(**add_comment_request)
114
+
115
+ comment_id = add_comment_response["comment_id"]
116
+
117
+ return self.get_comment(comment_id)
118
+
119
+ def get_comments(self) -> List[Comment]:
120
+ request = self.get_request({
121
+ "post_id": self.post_id,
122
+ "owner_id": self.owner_id,
123
+ # "count": 10,
124
+ })
125
+
126
+ comments_objects = list(get_all(request, self.api.wall.getComments, count_key="current_level_count"))
127
+
128
+ comments = [Comment.from_api_object(comments_object, self.api) for comments_object in comments_objects]
129
+
130
+ return comments
131
+
132
+ def get_comment(self, comment_id: int) -> Comment:
133
+ request = self.get_request({
134
+ "comment_id": comment_id,
135
+ "owner_id": self.owner_id,
136
+ # "count": 10,
137
+ })
138
+
139
+ comment_object = self.api.wall.getComment(**request)
140
+
141
+ comment = Comment.from_api_object(comment_object["items"][0], self.api)
142
+
143
+ return comment
@@ -0,0 +1,216 @@
1
+ from abc import ABC
2
+ from datetime import datetime
3
+ from enum import Enum
4
+ from functools import cache
5
+ from typing import Optional, List, Dict, Tuple, Any
6
+ from urllib.parse import urlparse
7
+
8
+ from vk_api.exceptions import ApiError
9
+
10
+ from pyvko.api_based import ApiMixin, ApiBased
11
+ from pyvko.aspects.albums import Albums
12
+ from pyvko.aspects.posts import Posts
13
+
14
+
15
+ class Event(ApiBased, Posts, Albums):
16
+ class Category(Enum):
17
+ CIRCUS = 1120
18
+ CONCERT = 1107
19
+ PARTY = 1103
20
+ EXHIBITION = 1104
21
+
22
+ class Section(Enum):
23
+ WALL = "wall"
24
+ DISCUSSION = "topics"
25
+ PHOTOS = "photos"
26
+ VIDEOS = "video"
27
+ MUSIC = "audio"
28
+ FILES = "docs"
29
+ WIKI = "wiki"
30
+ ARTICLES = "articles"
31
+ NARRATIVES = "narratives"
32
+ MESSAGES = "messages"
33
+
34
+ @staticmethod
35
+ @cache
36
+ def __section_index_mapping() -> List[Tuple['Event.Section', int]]:
37
+ return [
38
+ (Event.Section.PHOTOS, 1),
39
+ (Event.Section.VIDEOS, 4),
40
+ ]
41
+
42
+ @classmethod
43
+ def from_index(cls, index: int) -> Optional['Event.Section']:
44
+
45
+ for section, section_index in Event.Section.__section_index_mapping():
46
+ if index == section_index:
47
+ return section
48
+
49
+ return None
50
+
51
+ def to_index(self) -> Optional[int]:
52
+ for section, section_index in Event.Section.__section_index_mapping():
53
+ if section == self:
54
+ return section_index
55
+
56
+ return None
57
+
58
+ class SectionState(Enum):
59
+ NOT_AVAILABLE = -1
60
+ DISABLED = 0
61
+ OPEN = 1
62
+ ENABLED = 1
63
+ LIMITED = 2
64
+ RESTRICTED = 3
65
+
66
+ def __init__(self, api: Any, event_object: Dict, settings_object: Dict | None) -> None:
67
+ super().__init__(api)
68
+
69
+ self.__id: int = -event_object["id"]
70
+ self.name = event_object["name"]
71
+ self.start_date: datetime = datetime.fromtimestamp(event_object["start_date"])
72
+ self.end_date: datetime | None
73
+
74
+ if "finish_date" in event_object:
75
+ self.end_date = datetime.fromtimestamp(event_object["finish_date"])
76
+ else:
77
+ self.end_date = None
78
+
79
+ self.is_closed = bool(event_object["is_closed"])
80
+ self.url = event_object["screen_name"]
81
+
82
+ self.event_category: Event.Category | None = None
83
+ self.__sections: Dict[Event.Section, Event.SectionState] = {}
84
+ self.main_section: Event.Section | None = None
85
+ self.secondary_section: Event.Section | None = None
86
+ self.organiser: int | None = None
87
+
88
+ if settings_object is not None:
89
+ category_value = settings_object["public_category"]
90
+
91
+ if category_value != 0:
92
+ self.event_category = Event.Category(category_value)
93
+
94
+ self.__sections = {
95
+ s: Event.SectionState(settings_object[s.value]) for s in Event.Section
96
+ }
97
+
98
+ self.main_section = Event.Section.from_index(settings_object["main_section"])
99
+ self.secondary_section = Event.Section.from_index(settings_object["secondary_section"])
100
+ self.organiser = settings_object.get("event_object_id")
101
+
102
+ @property
103
+ def id(self) -> int:
104
+ return self.__id
105
+
106
+ @property
107
+ def browser_url(self):
108
+ return f"http://vk.com/event{abs(self.id)}"
109
+
110
+ def section_state(self, section: Section) -> Optional[SectionState]:
111
+ return self.__sections.get(section)
112
+
113
+ def set_section_state(self, section: Section, new_state: SectionState):
114
+ assert new_state != Event.SectionState.NOT_AVAILABLE
115
+ assert self.__sections[section] != Event.SectionState.NOT_AVAILABLE
116
+
117
+ self.__sections[section] = new_state
118
+
119
+ def save(self):
120
+ params = {
121
+ "group_id": abs(self.__id),
122
+ "access": int(self.is_closed),
123
+ "event_start_date": int(self.start_date.timestamp()),
124
+ "title": self.name,
125
+ }
126
+
127
+ for section, state in self.__sections.items():
128
+ if state == Event.SectionState.NOT_AVAILABLE:
129
+ continue
130
+
131
+ params[section.value] = state.value
132
+
133
+ if self.end_date is not None:
134
+ params["event_finish_date"] = int(self.end_date.timestamp())
135
+
136
+ if self.event_category is not None:
137
+ params["public_category"] = self.event_category.value
138
+
139
+ if self.main_section is not None:
140
+ params["main_section"] = self.main_section.to_index()
141
+
142
+ if self.secondary_section is not None:
143
+ params["secondary_section"] = self.secondary_section.to_index()
144
+
145
+ if self.organiser is not None:
146
+ params["event_group_id"] = abs(self.organiser)
147
+
148
+ request = self.get_request(params)
149
+
150
+ self.new_api.groups.edit(**request)
151
+
152
+
153
+ class Events(ApiMixin, ABC):
154
+ def create_event(self, title: str) -> Event:
155
+ request = self.get_request({
156
+ "title": title,
157
+ "type": "event",
158
+ })
159
+
160
+ response = self.new_api.groups.create(**request)
161
+
162
+ event = self.get_event(response["id"])
163
+
164
+ assert event is not None
165
+
166
+ return event
167
+
168
+ def get_event(self, url: str | int) -> Event | None:
169
+ url = str(url)
170
+
171
+ parse_result = urlparse(url)
172
+
173
+ qualifier = parse_result.path \
174
+ .split("/")[-1] \
175
+ .removeprefix("event") \
176
+ .removeprefix("-")
177
+
178
+ group_request = self.get_request({
179
+ "group_id": qualifier,
180
+ "fields": [
181
+ "start_date",
182
+ "finish_date",
183
+ ]
184
+ })
185
+
186
+ event_request = {
187
+ "fields": [
188
+ "start_date",
189
+ "finish_date",
190
+ "main_section",
191
+ ]
192
+ }
193
+
194
+ event_request.update(group_request)
195
+
196
+ event_response = self.new_api.groups.getById(**event_request)
197
+
198
+ event_object = event_response["groups"][0]
199
+
200
+ if event_object["type"] not in ["event", ]:
201
+ return None
202
+
203
+ group_request["group_id"] = event_object["id"]
204
+
205
+ try:
206
+ settings_response = self.new_api.groups.getSettings(**group_request)
207
+ except ApiError as err:
208
+ # 15 — ACCESS_DENIED in VK API
209
+ if err.code not in [3, 15]:
210
+ raise
211
+
212
+ settings_response = None
213
+
214
+ event = Event(self.new_api, event_object=event_object, settings_object=settings_response)
215
+
216
+ return event
@@ -0,0 +1,95 @@
1
+ from abc import ABC
2
+ from typing import Any, Dict, List, TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from pyvko.entities.user import User
6
+
7
+ from pyvko.api_based import ApiMixin, ApiBased
8
+ from pyvko.aspects.albums import Albums
9
+ from pyvko.aspects.events import Events, Event
10
+ from pyvko.aspects.posts import Posts
11
+ from pyvko.entities.user import User
12
+ from pyvko.shared.utils import get_all
13
+
14
+
15
+ class Group(ApiBased, Posts, Albums, Events):
16
+ # from pyvko.entities.user import User
17
+
18
+ def __init__(self, api: Any, group_object: Dict) -> None:
19
+ super().__init__(api)
20
+
21
+ self.__group_object = group_object
22
+
23
+ self.__id = -group_object["id"]
24
+ self.__name = group_object["name"]
25
+ self.__url = group_object["screen_name"]
26
+
27
+ def __str__(self) -> str:
28
+ return f"Group: {self.__name}({self.id})"
29
+
30
+ @property
31
+ def name(self) -> str:
32
+ return self.__name
33
+
34
+ # region Wall
35
+
36
+ @property
37
+ def id(self) -> int:
38
+ return self.__id
39
+
40
+ # endregion Wall
41
+
42
+ @property
43
+ def url(self) -> str:
44
+ return self.__url
45
+
46
+ def get_members(self) -> List['User']:
47
+ parameters = {
48
+ "group_id": self.id,
49
+ "sort": "time_desc",
50
+ "fields": [
51
+ "online",
52
+ ]
53
+ }
54
+
55
+ parameters = self.get_request(parameters)
56
+
57
+ users_descriptions = get_all(parameters, self.new_api.groups.getMembers)
58
+
59
+ users = [User(api=self.new_api, user_object=description) for description in users_descriptions]
60
+
61
+ return users
62
+
63
+ # region Events
64
+
65
+ def create_event(self, title: str) -> Event:
66
+ event = super().create_event(title)
67
+
68
+ event.organiser = self.id
69
+
70
+ event.save()
71
+
72
+ return event
73
+
74
+ # endregion Events
75
+
76
+
77
+ class Groups(ApiMixin, ABC):
78
+ def get_group(self, url: str | int) -> Group | None:
79
+ if isinstance(url, int):
80
+ url = abs(url)
81
+
82
+ group_request = self.get_request({
83
+ "group_id": url
84
+ })
85
+
86
+ group_response = self.new_api.groups.getById(**group_request)
87
+
88
+ group_object = group_response["groups"][0]
89
+
90
+ if group_object["type"] not in ["page", "group"]:
91
+ return None
92
+
93
+ group = Group(api=self.new_api, group_object=group_object)
94
+
95
+ return group