udemy-userAPI 0.3.2__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.
@@ -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