udemy-userAPI 0.3.2__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,224 @@
1
+ import re
2
+ import xml.etree.ElementTree as Et
3
+
4
+
5
+ def calculate_segment_url2(media_template, segment_number, segment_time, rep_id):
6
+ """
7
+ Calcula a URL de um segmento específico, substituindo variáveis no template.
8
+ """
9
+ url = media_template.replace('$Number$', str(segment_number))
10
+ url = url.replace('$RepresentationID$', rep_id)
11
+ if '$Time$' in url:
12
+ url = url.replace('$Time$', str(segment_time))
13
+ return url
14
+
15
+
16
+ def build_url2(template, rep_id):
17
+ """
18
+ Constrói a URL substituindo variáveis no template com base nos atributos.
19
+ """
20
+ if '$RepresentationID$' in template:
21
+ template = template.replace('$RepresentationID$', rep_id)
22
+ return template
23
+
24
+
25
+ def parse_duration(duration_str):
26
+ """
27
+ Converte uma duração em formato ISO 8601 (ex: "PT163.633S") para segundos (float).
28
+ """
29
+ match = re.match(r'PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?', duration_str)
30
+ if match:
31
+ hours = int(match.group(1)) if match.group(1) else 0
32
+ minutes = int(match.group(2)) if match.group(2) else 0
33
+ seconds = float(match.group(3)) if match.group(3) else 0.0
34
+ return hours * 3600 + minutes * 60 + seconds
35
+ return 0.0
36
+
37
+
38
+ class MPDParser:
39
+ """
40
+ Classe para analisar e extrair informações de manifestos MPD (Media Presentation Description),
41
+ com foco em arquivos VOD (Video on Demand). Atualmente, não oferece suporte para transmissões ao vivo.
42
+ """
43
+
44
+ def __init__(self, mpd_content: str):
45
+ """
46
+ Inicializa o parser para um arquivo MPD.
47
+
48
+ Args:
49
+ mpd_content (str): Caminho do arquivo MPD ou conteúdo bruto.
50
+ """
51
+ self._mpd_content = mpd_content
52
+ self._video_representations = {}
53
+ self._audio_representations = {}
54
+ self._content_protection = {}
55
+ self._selected_resolution = None
56
+
57
+ # Tenta fazer o parsing com diferentes métodos
58
+ if not self.__parse_mpd_v2():
59
+ self.__parse_mpd_v1()
60
+
61
+ def __parse_mpd_v1(self):
62
+ """
63
+ Parsing básico do MPD (versão 1).
64
+ """
65
+ content = self._mpd_content
66
+ root = Et.fromstring(content)
67
+ ns = {'dash': 'urn:mpeg:dash:schema:mpd:2011'}
68
+
69
+ for adaptation_set in root.findall('.//dash:AdaptationSet', ns):
70
+ mime_type = adaptation_set.attrib.get('mimeType', '')
71
+ self.__parse_adaptation_set(adaptation_set, mime_type, ns)
72
+
73
+ def __parse_mpd_v2(self):
74
+ """
75
+ Parsing avançado do MPD (versão 2).
76
+ """
77
+ content = self._mpd_content
78
+ root = Et.fromstring(content)
79
+ ns = {'dash': 'urn:mpeg:dash:schema:mpd:2011'}
80
+
81
+ for adaptation_set in root.findall('.//dash:AdaptationSet', ns):
82
+ mime_type = adaptation_set.attrib.get('mimeType', '')
83
+ self.__parse_adaptation_set(adaptation_set, mime_type, ns)
84
+ return True
85
+
86
+ def __parse_adaptation_set(self, adaptation_set, mime_type, ns):
87
+ """
88
+ Analisa um AdaptationSet para representações de vídeo ou áudio.
89
+
90
+ Args:
91
+ adaptation_set (ET.Element): Elemento do AdaptationSet.
92
+ mime_type (str): Tipo MIME (vídeo ou áudio).
93
+ ns (dict): Namespace para parsing do XML.
94
+ """
95
+ # Extrai informações de proteção de conteúdo
96
+ for content_protection in adaptation_set.findall('dash:ContentProtection', ns):
97
+ scheme_id_uri = content_protection.attrib.get('schemeIdUri', '')
98
+ value = content_protection.attrib.get('value', '')
99
+ self._content_protection[scheme_id_uri] = value
100
+
101
+ # Processa representações dentro do AdaptationSet
102
+ for representation in adaptation_set.findall('dash:Representation', ns):
103
+ self.__process_representation(representation, mime_type, ns)
104
+
105
+ def __process_representation(self, representation, mime_type, ns):
106
+ """
107
+ Processa uma representação de mídia (vídeo ou áudio).
108
+
109
+ Args:
110
+ representation (ET.Element): Elemento da Representação.
111
+ mime_type (str): Tipo MIME da mídia.
112
+ ns (dict): Namespace para parsing do XML.
113
+ """
114
+ rep_id = representation.attrib.get('id')
115
+ width = int(representation.attrib.get('width', 0))
116
+ height = int(representation.attrib.get('height', 0))
117
+ resolution = (width, height) if width and height else None
118
+ bandwidth = int(representation.attrib.get('bandwidth', 0))
119
+
120
+ # Extrai informações do SegmentTemplate
121
+ segment_template = representation.find('dash:SegmentTemplate', ns)
122
+ if segment_template:
123
+ init_url = self.__build_url(segment_template.get('initialization'), rep_id, bandwidth)
124
+ segments = self.__generate_segments(segment_template, ns, rep_id, bandwidth)
125
+
126
+ representation_info = {
127
+ 'id': rep_id,
128
+ 'resolution': resolution,
129
+ 'bandwidth': bandwidth,
130
+ 'init_url': init_url,
131
+ 'segments': segments,
132
+ }
133
+ if 'video' in mime_type:
134
+ self._video_representations[resolution] = representation_info
135
+ elif 'audio' in mime_type:
136
+ self._audio_representations[bandwidth] = representation_info
137
+
138
+ def __generate_segments(self, segment_template, ns, rep_id, bandwidth):
139
+ """
140
+ Gera a lista de URLs de segmentos com base no SegmentTemplate.
141
+
142
+ Args:
143
+ segment_template (ET.Element): Elemento do SegmentTemplate.
144
+ ns (dict): Namespace para parsing do XML.
145
+ rep_id (str): ID da representação.
146
+ bandwidth (int): Largura de banda da representação.
147
+
148
+ Returns:
149
+ list: URLs dos segmentos.
150
+ """
151
+ segments = []
152
+ media_template = segment_template.get('media')
153
+ segment_timeline = segment_template.find('dash:SegmentTimeline', ns)
154
+
155
+ if segment_timeline:
156
+ segment_number = int(segment_template.get('startNumber', 1))
157
+ for segment in segment_timeline.findall('dash:S', ns):
158
+ t = int(segment.get('t', 0))
159
+ d = int(segment.get('d'))
160
+ r = int(segment.get('r', 0))
161
+ for i in range(r + 1):
162
+ segment_time = t + i * d
163
+ segments.append(self.__build_url(media_template, rep_id, bandwidth, segment_time, segment_number))
164
+ segment_number += 1
165
+ return segments
166
+
167
+ @staticmethod
168
+ def __build_url(template, rep_id, bandwidth, segment_time=None, segment_number=None):
169
+ """
170
+ Constrói uma URL substituindo placeholders.
171
+
172
+ Args:
173
+ template (str): Template de URL.
174
+ rep_id (str): ID da representação.
175
+ bandwidth (int): Largura de banda.
176
+ segment_time (int, opcional): Timestamp do segmento.
177
+ segment_number (int, opcional): Número do segmento.
178
+
179
+ Returns:
180
+ str: URL formatada.
181
+ """
182
+ url = template.replace('$RepresentationID$', rep_id).replace('$Bandwidth$', str(bandwidth))
183
+ if segment_time is not None:
184
+ url = url.replace('$Time$', str(segment_time))
185
+ if segment_number is not None:
186
+ url = url.replace('$Number$', str(segment_number))
187
+ return url
188
+
189
+ def set_selected_resolution(self, resolution: tuple):
190
+ """
191
+ Define a resolução selecionada para a recuperação de segmentos de vídeo.
192
+
193
+ Args:
194
+ resolution (tuple): Resolução desejada (largura, altura).
195
+
196
+ Raises:
197
+ Exception: Se a resolução não estiver disponível no manifesto.
198
+ """
199
+ if resolution in self._video_representations:
200
+ self._selected_resolution = resolution
201
+ else:
202
+ raise Exception(
203
+ f'A resolução {resolution} não está disponível!\n\n'
204
+ f'\t=> Resoluções disponíveis no arquivo: {self.get_all_video_resolutions()}')
205
+
206
+ def get_selected_video_init_url(self):
207
+ """
208
+ Retorna o URL de inicialização para a resolução de vídeo selecionada.
209
+
210
+ Returns:
211
+ str: URL de inicialização do vídeo, ou None se não houver resolução selecionada.
212
+ """
213
+ if self._selected_resolution:
214
+ return self._video_representations[self._selected_resolution].get('init_url')
215
+ return None
216
+
217
+ def get_all_video_resolutions(self):
218
+ """
219
+ Retorna uma lista de todas as resoluções de vídeo disponíveis.
220
+
221
+ Returns:
222
+ list: lista de tuplas com resoluções de vídeo (largura, altura).
223
+ """
224
+ return list(self._video_representations.keys())
@@ -0,0 +1,117 @@
1
+ import json
2
+ import requests
3
+ from .exeptions import UdemyUserApiExceptions, LoginException
4
+
5
+
6
+ def get_courses_plan(tipe: str) -> list:
7
+ """ Obtém uma lista de cursos com base no tipo de plano.
8
+
9
+ Args: tipe (str): Tipo de plano ('default' ou 'plan'). Returns: list: Lista de cursos. Raises: LoginException: Se
10
+ a sessão estiver expirada. UdemyUserApiExceptions: Se houver erro ao obter os cursos."""
11
+ from .api import HEADERS_USER
12
+ from .authenticate import UdemyAuth
13
+ auth = UdemyAuth()
14
+ if not auth.verif_login():
15
+ raise LoginException("Sessão expirada!")
16
+ courses_data = []
17
+ if tipe == 'default':
18
+ response = requests.get(f"https://www.udemy.com/api-2.0/users/me/subscribed-courses/?page_size=1000"
19
+ f"&ordering=-last_accessed&fields[course]=image_240x135,title,completion_ratio&"
20
+ f"is_archived=false",
21
+ headers=HEADERS_USER)
22
+ if response.status_code == 200:
23
+ r = json.loads(response.text)
24
+ results = r.get("results", None)
25
+ if results:
26
+ courses_data.append(results)
27
+ else:
28
+ r = json.loads(response.text)
29
+ raise UdemyUserApiExceptions(f"Error obtain courses 'default' -> {r}")
30
+ elif tipe == 'plan':
31
+ response2 = requests.get(
32
+ url="https://www.udemy.com/api-2.0/users/me/subscription-course-enrollments/?"
33
+ "fields[course]=@min,visible_instructors,image_240x135,image_480x270,completion_ratio,"
34
+ "last_accessed_time,enrollment_time,is_practice_test_course,features,num_collections,"
35
+ "published_title,buyable_object_type,remaining_time,is_assigned,next_to_watch_item,"
36
+ "is_in_user_subscription&fields[user]=@min&ordering=-last_accessed&page_size=1000&"
37
+ "max_progress=99.9&fields[lecture]=@min,content_details,asset,url,thumbnail_url,"
38
+ "last_watched_second,object_index&fields[quiz]=@min,content_details,asset,url,object_index&"
39
+ "fields[practice]=@min,content_details,asset,estimated_duration,learn_url,object_index",
40
+ headers=HEADERS_USER)
41
+ if response2.status_code == 200:
42
+ r = json.loads(response2.text)
43
+ results2 = r.get("results", None)
44
+ if results2:
45
+ courses_data.append(results2)
46
+ else:
47
+ r = json.loads(response2.text)
48
+ raise UdemyUserApiExceptions(f"Error obtain courses 'plan' -> {r}")
49
+ else:
50
+ raise UdemyUserApiExceptions("Atenção dev! os parametros são : 'plan' e 'default'")
51
+ return courses_data
52
+
53
+
54
+ def get_details_courses(course_id):
55
+ """
56
+ Obtém detalhes de um curso específico.
57
+
58
+ Args:
59
+ course_id (int): ID do curso.
60
+
61
+ Returns:
62
+ dict: Dicionário contendo os detalhes do curso.
63
+
64
+ Raises:
65
+ LoginException: Se a sessão estiver expirada.
66
+ UdemyUserApiExceptions: Se houver erro ao obter os detalhes do curso.
67
+ """
68
+ from .api import HEADERS_USER
69
+ from .authenticate import UdemyAuth
70
+ auth = UdemyAuth()
71
+ if not auth.verif_login():
72
+ raise LoginException("Sessão expirada!")
73
+ response = requests.get(
74
+ f"https://www.udemy.com/api-2.0/courses/{course_id}/subscriber-curriculum-items/?"
75
+ f"caching_intent=True&fields%5Basset%5D=title%2Cfilename%2Casset_type%2Cstatus%2Ctime_estimation%2"
76
+ f"Cis_external&fields%5Bchapter%5D=title%2Cobject_index%2Cis_published%2Csort_order&fields%5Blecture"
77
+ f"%5D=title%2Cobject_index%2Cis_published%2Csort_order%2Ccreated%2Casset%2Csupplementary_assets%2"
78
+ f"Cis_free&fields%5Bpractice%5D=title%2Cobject_index%2Cis_published%2Csort_order&fields%5Bquiz%5D="
79
+ f"title%2Cobject_index%2Cis_published%2Csort_order%2Ctype&pages&page_size=400&fields[lecture]=asset,"
80
+ f"description,download_url,is_free,last_watched_second&fields[asset]=asset_type,length,"
81
+ f"media_license_token,course_is_drmed,external_url&q=0.3108014137011559",
82
+ headers=HEADERS_USER)
83
+ if response.status_code == 200:
84
+ resposta = json.loads(response.text)
85
+ return resposta
86
+ else:
87
+ raise UdemyUserApiExceptions(f"Erro ao obter detalhes do curso! Código de status: {response.status_code}")
88
+
89
+
90
+ def get_course_infor(course_id):
91
+ """
92
+ Obtém informações de um curso específico.
93
+
94
+ Args:
95
+ course_id (int): ID do curso.
96
+
97
+ Returns:
98
+ dict: Dicionário contendo as informações do curso.
99
+
100
+ Raises:
101
+ LoginException: Se a sessão estiver expirada.
102
+ UdemyUserApiExceptions: Se houver erro ao obter as informações do curso.
103
+ """
104
+ from .api import HEADERS_USER
105
+ from .authenticate import UdemyAuth
106
+ auth = UdemyAuth()
107
+ if not auth.verif_login():
108
+ raise LoginException("Sessão expirada!")
109
+ end_point = (
110
+ f'https://www.udemy.com/api-2.0/courses/{course_id}/?fields[course]=title,context_info,primary_category,'
111
+ 'primary_subcategory,avg_rating_recent,visible_instructors,locale,estimated_content_length,'
112
+ 'num_subscribers')
113
+ response = requests.get(end_point, headers=HEADERS_USER)
114
+ if response.status_code == 200:
115
+ return json.loads(response.text)
116
+ else:
117
+ raise UdemyUserApiExceptions("Erro ao obter informações do curso!")
udemy_userAPI/udemy.py ADDED
@@ -0,0 +1,93 @@
1
+ from .exeptions import UdemyUserApiExceptions, UnhandledExceptions, LoginException
2
+ from .sections import get_courses_plan, get_details_courses
3
+ from .api import HEADERS_USER
4
+ from .bultins import Course
5
+ from .authenticate import UdemyAuth
6
+
7
+ auth = UdemyAuth()
8
+ verif_login = auth.verif_login()
9
+
10
+
11
+ class Udemy:
12
+ """Wrapper para API de usuário da plataforma Udemy"""
13
+
14
+ def __init__(self):
15
+ """
16
+ Inicializa o objeto Udemy.
17
+
18
+ Raises:
19
+ LoginException: Se a sessão estiver expirada.
20
+ """
21
+ self.__headers = HEADERS_USER
22
+ if not verif_login:
23
+ raise LoginException("Sessão expirada!")
24
+
25
+ @staticmethod
26
+ def my_subscribed_courses_by_plan() -> list[dict]:
27
+ """
28
+ Obtém os cursos que o usuário está inscrito, obtidos através de planos (assinatura).
29
+
30
+ Returns:
31
+ list[dict]: Lista de cursos inscritos através de planos.
32
+
33
+ Raises:
34
+ UdemyUserApiExceptions: Se houver erro ao obter os cursos.
35
+ """
36
+ try:
37
+ courses = get_courses_plan(tipe='plan')
38
+ return courses
39
+ except UdemyUserApiExceptions as e:
40
+ raise UnhandledExceptions(e)
41
+
42
+ @staticmethod
43
+ def my_subscribed_courses() -> list[dict]:
44
+ """
45
+ Obtém os cursos que o usuário está inscrito, excluindo listas vazias ou nulas.
46
+
47
+ Returns:
48
+ list[dict]: Lista de todos os cursos inscritos.
49
+
50
+ Raises:
51
+ UdemyUserApiExceptions: Se houver erro ao obter os cursos.
52
+ """
53
+ try:
54
+ # Obtém os cursos
55
+ courses1 = get_courses_plan(tipe='default') # lista de cursos padrão
56
+ courses2 = get_courses_plan(tipe='plan') # lista de cursos de um plano
57
+
58
+ # Cria uma lista vazia para armazenar os cursos válidos
59
+ all_courses = []
60
+
61
+ # Adiciona a lista somente se não estiver vazia ou nula
62
+ if courses1:
63
+ for i in courses1:
64
+ all_courses.extend(i)
65
+ if courses2:
66
+ for i in courses2:
67
+ all_courses.extend(i)
68
+
69
+ return all_courses
70
+
71
+ except UdemyUserApiExceptions as e:
72
+ raise UnhandledExceptions(e)
73
+
74
+ @staticmethod
75
+ def get_details_course(course_id):
76
+ """
77
+ Obtém detalhes de um curso através do ID.
78
+
79
+ Args:
80
+ course_id: O ID do curso.
81
+
82
+ Returns:
83
+ Course: Um objeto Course contendo os detalhes do curso.
84
+
85
+ Raises:
86
+ UnhandledExceptions: Se houver erro ao obter os detalhes do curso.
87
+ """
88
+ try:
89
+ d = get_details_courses(course_id)
90
+ b = Course(course_id=course_id, results=d)
91
+ return b
92
+ except UnhandledExceptions as e:
93
+ raise UnhandledExceptions(e)
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 PauloCesar-dev404
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.1
2
+ Name: udemy_userAPI
3
+ Version: 0.3.2
4
+ Summary: Obtenha detalhes de cursos que o usuário esteja inscrito da plataforma Udemy,usando o EndPoint de usuário o mesmo que o navegador utiliza para acessar e redenrizar os cursos.
5
+ Author: PauloCesar-dev404
6
+ Author-email: paulocesar0073dev404@gmail.com
7
+ License: MIT
8
+ Project-URL: GitHub, https://github.com/PauloCesar-dev404/udemy-userAPI
9
+ Project-URL: Bugs/Melhorias, https://github.com/PauloCesar-dev404/udemy-userAPI/issues
10
+ Project-URL: Documentação, https://github.com/PauloCesar-dev404/udemy-userAPI/wiki
11
+ Keywords: udemy,udemy python,pyudemy,udemy_userAPI,udemy api
12
+ Platform: any
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: requests
16
+ Requires-Dist: cloudscraper
17
+ Requires-Dist: pywidevine
18
+
19
+ # udemy-userAPI
20
+
21
+
22
+ ![Versão](https://img.shields.io/badge/version-0.3.2-orange)
23
+ ![Licença](https://img.shields.io/badge/license-MIT-orange)
24
+ [![Sponsor](https://img.shields.io/badge/💲Donate-yellow)](https://paulocesar-dev404.github.io/me-apoiando-online/)
25
+ [![Sponsor](https://img.shields.io/badge/Documentation-green)](https://github.com/PauloCesar-dev404/udemy-userAPI/blob/main/docs/iniciando.md)
26
+
27
+
28
+ Obtenha detalhes de cursos da plataforma udemy com a api de usuário,usando esta lib
29
+
30
+
31
+ - [x] Obter cursos inscritos(acesso por plano ou cursos free)
32
+ - [x] Obter detalhes de Aulas
33
+ - [x] Obter detalhes de um Curso
34
+
@@ -0,0 +1,29 @@
1
+ animation_consoles/__init__.py,sha256=5uHhe-PVZ54FHWxbF1sNvNt4fuQf3FtZWVo2Mjo11a8,40
2
+ animation_consoles/animation.py,sha256=ZreNtdD0HYeqlRx-f1d1twUU4sOFTR7vJ3S6tMQpnHM,2122
3
+ ffmpeg_for_python/__config__.py,sha256=nCPrYs1NkMnyfyg5ITw9wOar4nUJOxwONrItVpVBVBM,4719
4
+ ffmpeg_for_python/__init__.py,sha256=-BMtoX8Yof_pnHra2OzoV3faxMubpMvUedMy8TqI8dc,214
5
+ ffmpeg_for_python/__utils.py,sha256=Qy3J5f4lOIPcSNbTwiawfiHjYPdZ_tq7hafStnnqwA4,3263
6
+ ffmpeg_for_python/__version__.py,sha256=HLFuN4n_leeJE5twr7yH2AAFyfIcEHzxElLRP1FUKmQ,422
7
+ ffmpeg_for_python/exeptions.py,sha256=tg-TBdaq_NHxZOCAhkMttzwtJVILPAQPLOKqofe5PPA,3627
8
+ ffmpeg_for_python/ffmpeg.py,sha256=G2VGHOIhErsqQI4OVlUnIQGmleNCjxyFqzNAMNnoD6I,7920
9
+ m3u8_analyzer/M3u8Analyzer.py,sha256=aUgxk2jS84MFDNbjlOT8FRiJerFI_jGcKMu9uv1EwcE,36620
10
+ m3u8_analyzer/__init__.py,sha256=v7CiVqsCq2YH347C-QR1kHPJtXFFdru8qole3E9adCY,217
11
+ m3u8_analyzer/__version__.py,sha256=YP3yT87ZKrU3eARUUdQ_pg4xAXLGfBXjH4ZgEoZSq1I,25
12
+ m3u8_analyzer/exeptions.py,sha256=fK6bU3YxNSbfsPmCp4yudUvmwy_g6dj2KwIkH0dW4LI,3672
13
+ udemy_userAPI/__init__.py,sha256=BPle89xE_CMTKKe_Lw6jioYLgpH-q_Lpho2S-n1PIUA,206
14
+ udemy_userAPI/__version__.py,sha256=aMKbixxAkks_VfsbQJs0sU6jFllEvVm4k3e-E32e5u8,405
15
+ udemy_userAPI/api.py,sha256=oDVylMQ6CsMeJ7V7FKdjjjvY0GrHHqJdVMTTptQmSiE,24277
16
+ udemy_userAPI/authenticate.py,sha256=gpHwS34WboQCpktQg6NsvLBJzX9AL8Do3Gk08PLR4GY,14133
17
+ udemy_userAPI/bultins.py,sha256=ZEksThVSaDe7jsyeCqhCiFLJsy-pIN8EdnJ6Aoh4s9k,16428
18
+ udemy_userAPI/exeptions.py,sha256=kfnPdZpqYY8nd0gnl6_Vh-MIz-XupmmbRPIuFnyXupk,692
19
+ udemy_userAPI/sections.py,sha256=oP3jvbsWocemqhzzOAOoeL7ICF1f4gNvjL4FJBt47pE,5474
20
+ udemy_userAPI/udemy.py,sha256=SpK0LI4hjO45nZDz5waw-Py-d0uulBb28TVjltyWBxM,2920
21
+ udemy_userAPI/.cache/.udemy_userAPI,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ udemy_userAPI/mpd_analyzer/__init__.py,sha256=i3JVWyvcFLaj5kPmx8c1PgjsLht7OUIQQClD4yqYbo8,102
23
+ udemy_userAPI/mpd_analyzer/bin.wvd,sha256=1rAJdCc120hQlX9qe5KUS628eY2ZHYxQSmyhGNefSzo,2956
24
+ udemy_userAPI/mpd_analyzer/mpd_parser.py,sha256=PgUkHc5x8FTuXFCuYkWPZr9TaO_nsKalb02EFYl_zeA,8926
25
+ udemy_userAPI-0.3.2.dist-info/LICENSE,sha256=l4jdKYt8gSdDFOGr09vCKnMn_Im55XIcQKqTDEtFfNs,1095
26
+ udemy_userAPI-0.3.2.dist-info/METADATA,sha256=ir_5qB1Q1qu66M3pCaL9VN7BUZBX0MwStZg0iQ0r03k,1438
27
+ udemy_userAPI-0.3.2.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
28
+ udemy_userAPI-0.3.2.dist-info/top_level.txt,sha256=ijTINaSDRKhdahY_X7dmSRFTxBIwQErWv9ATCG55mog,14
29
+ udemy_userAPI-0.3.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.6.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ udemy_userAPI