openedx-learning 0.16.1__py2.py3-none-any.whl → 0.17.0__py2.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.
@@ -2,4 +2,4 @@
2
2
  Open edX Learning ("Learning Core").
3
3
  """
4
4
 
5
- __version__ = "0.16.1"
5
+ __version__ = "0.17.0"
@@ -71,7 +71,6 @@ class ContentInline(admin.TabularInline):
71
71
  fields = [
72
72
  "key",
73
73
  "format_size",
74
- "learner_downloadable",
75
74
  "rendered_data",
76
75
  ]
77
76
  readonly_fields = [
@@ -12,6 +12,7 @@ are stored in this app.
12
12
  """
13
13
  from __future__ import annotations
14
14
 
15
+ import mimetypes
15
16
  from datetime import datetime, timezone
16
17
  from enum import StrEnum, auto
17
18
  from logging import getLogger
@@ -129,7 +130,7 @@ def create_component_version(
129
130
  def create_next_component_version(
130
131
  component_pk: int,
131
132
  /,
132
- content_to_replace: dict[str, int | None],
133
+ content_to_replace: dict[str, int | None | bytes],
133
134
  created: datetime,
134
135
  title: str | None = None,
135
136
  created_by: int | None = None,
@@ -140,11 +141,14 @@ def create_next_component_version(
140
141
  A very common pattern for making a new ComponentVersion is going to be "make
141
142
  it just like the last version, except changing these one or two things".
142
143
  Before calling this, you should create any new contents via the contents
143
- API, since ``content_to_replace`` needs Content IDs for the values.
144
+ API or send the content bytes as part of ``content_to_replace`` values.
144
145
 
145
146
  The ``content_to_replace`` dict is a mapping of strings representing the
146
- local path/key for a file, to ``Content.id`` values. Using a `None` for
147
- a value in this dict means to delete that key in the next version.
147
+ local path/key for a file, to ``Content.id`` or content bytes values. Using
148
+ `None` for a value in this dict means to delete that key in the next version.
149
+
150
+ Make sure to wrap the function call on a atomic statement:
151
+ ``with transaction.atomic():``
148
152
 
149
153
  It is okay to mark entries for deletion that don't exist. For instance, if a
150
154
  version has ``a.txt`` and ``b.txt``, sending a ``content_to_replace`` value
@@ -186,16 +190,31 @@ def create_next_component_version(
186
190
  component_id=component_pk,
187
191
  )
188
192
  # First copy the new stuff over...
189
- for key, content_pk in content_to_replace.items():
193
+ for key, content_pk_or_bytes in content_to_replace.items():
190
194
  # If the content_pk is None, it means we want to remove the
191
195
  # content represented by our key from the next version. Otherwise,
192
196
  # we add our key->content_pk mapping to the next version.
193
- if content_pk is not None:
197
+ if content_pk_or_bytes is not None:
198
+ if isinstance(content_pk_or_bytes, bytes):
199
+ file_path, file_content = key, content_pk_or_bytes
200
+ media_type_str, _encoding = mimetypes.guess_type(file_path)
201
+ # We use "application/octet-stream" as a generic fallback media type, per
202
+ # RFC 2046: https://datatracker.ietf.org/doc/html/rfc2046
203
+ media_type_str = media_type_str or "application/octet-stream"
204
+ media_type = contents_api.get_or_create_media_type(media_type_str)
205
+ content = contents_api.get_or_create_file_content(
206
+ component.learning_package.id,
207
+ media_type.id,
208
+ data=file_content,
209
+ created=created,
210
+ )
211
+ content_pk = content.pk
212
+ else:
213
+ content_pk = content_pk_or_bytes
194
214
  ComponentVersionContent.objects.create(
195
215
  content_id=content_pk,
196
216
  component_version=component_version,
197
217
  key=key,
198
- learner_downloadable=False,
199
218
  )
200
219
  # Now copy any old associations that existed, as long as they aren't
201
220
  # in conflict with the new stuff or marked for deletion.
@@ -207,7 +226,6 @@ def create_next_component_version(
207
226
  content_id=cvrc.content_id,
208
227
  component_version=component_version,
209
228
  key=cvrc.key,
210
- learner_downloadable=cvrc.learner_downloadable,
211
229
  )
212
230
 
213
231
  return component_version
@@ -402,7 +420,6 @@ def create_component_version_content(
402
420
  content_id: int,
403
421
  /,
404
422
  key: str,
405
- learner_downloadable: bool = False,
406
423
  ) -> ComponentVersionContent:
407
424
  """
408
425
  Add a Content to the given ComponentVersion
@@ -425,7 +442,6 @@ def create_component_version_content(
425
442
  component_version_id=component_version_id,
426
443
  content_id=content_id,
427
444
  key=key,
428
- learner_downloadable=learner_downloadable,
429
445
  )
430
446
  return cvrc
431
447
 
@@ -433,7 +449,6 @@ def create_component_version_content(
433
449
  class AssetError(StrEnum):
434
450
  """Error codes related to fetching ComponentVersion assets."""
435
451
  ASSET_PATH_NOT_FOUND_FOR_COMPONENT_VERSION = auto()
436
- ASSET_NOT_LEARNER_DOWNLOADABLE = auto()
437
452
  ASSET_HAS_NO_DOWNLOAD_FILE = auto()
438
453
 
439
454
 
@@ -464,7 +479,6 @@ def get_redirect_response_for_component_asset(
464
479
  component_version_uuid: UUID,
465
480
  asset_path: Path,
466
481
  public: bool = False,
467
- learner_downloadable_only: bool = True,
468
482
  ) -> HttpResponse:
469
483
  """
470
484
  ``HttpResponse`` for a reverse-proxy to serve a ``ComponentVersion`` asset.
@@ -478,11 +492,6 @@ def get_redirect_response_for_component_asset(
478
492
  If ``True``, this will return an ``HttpResponse`` that can be cached in
479
493
  a CDN and shared across many clients.
480
494
 
481
- :param learner_downloadable_only: Only return assets that are meant to be
482
- downloadable by Learners, i.e. in the LMS experience. If this is
483
- ``True``, then requests for assets that are not meant for student
484
- download will return a ``404`` error response.
485
-
486
495
  **Response Codes**
487
496
 
488
497
  If the asset exists for this ``ComponentVersion``, this function will return
@@ -492,15 +501,10 @@ def get_redirect_response_for_component_asset(
492
501
  the ``ComponentVersion`` itself does not exist, the response code will be
493
502
  ``404``.
494
503
 
495
- Other than checking the coarse-grained ``learner_downloadable_only`` flag,
496
- *this function does not do auth checking of any sort*–it will never return
504
+ This function does not do auth checking of any sort. It will never return
497
505
  a ``401`` or ``403`` response code. That is by design. Figuring out who is
498
506
  making the request and whether they have permission to do so is the
499
- responsiblity of whatever is calling this function. The
500
- ``learner_downloadable_only`` flag is intended to be a filter for the entire
501
- view. When it's True, not even staff can download component-internal assets.
502
- This is intended to protect us from accidentally allowing sensitive grading
503
- code to get leaked out.
507
+ responsiblity of whatever is calling this function.
504
508
 
505
509
  **Metadata Headers**
506
510
 
@@ -576,24 +580,6 @@ def get_redirect_response_for_component_asset(
576
580
  )
577
581
  return HttpResponseNotFound(headers=info_headers)
578
582
 
579
- # Check: If we're asking only for Learner Downloadable assets, and the asset
580
- # in question is not supposed to be downloadable by learners, then we give a
581
- # 404 error. Even staff members are not expected to be able to download
582
- # these assets via the LMS endpoint that serves students. Studio would be
583
- # expected to have an entirely different view to serve these assets in that
584
- # context (along with different timeouts, auth, and cache settings). So in
585
- # that sense, the asset doesn't exist for that particular endpoint.
586
- if learner_downloadable_only and (not cv_content.learner_downloadable):
587
- logger.error(
588
- f"ComponentVersion {component_version_uuid} has asset {asset_path}, "
589
- "but it is not meant to be downloadable by learners "
590
- "(ComponentVersionContent.learner_downloadable=False)."
591
- )
592
- info_headers.update(
593
- _error_header(AssetError.ASSET_NOT_LEARNER_DOWNLOADABLE)
594
- )
595
- return HttpResponseNotFound(headers=info_headers)
596
-
597
583
  # At this point, we know that there is valid Content that we want to send.
598
584
  # This adds Content-level headers, like the hash/etag and content type.
599
585
  info_headers.update(contents_api.get_content_info_headers(content))
@@ -4,14 +4,11 @@ Management command to add files to a Component.
4
4
  This is mostly meant to be a debugging tool to let us to easily load some test
5
5
  asset data into the system.
6
6
  """
7
- import mimetypes
8
7
  import pathlib
9
8
  from datetime import datetime, timezone
10
9
 
11
10
  from django.core.management.base import BaseCommand
12
11
 
13
- from ....components.api import create_component_version_content
14
- from ....contents.api import get_or_create_file_content, get_or_create_media_type
15
12
  from ....publishing.api import get_learning_package_by_key
16
13
  from ...api import create_next_component_version, get_component_by_key
17
14
 
@@ -69,39 +66,18 @@ class Command(BaseCommand):
69
66
  )
70
67
 
71
68
  created = datetime.now(tz=timezone.utc)
72
- keys_to_remove = set()
73
- local_keys_to_content = {}
69
+ local_keys_to_content_bytes = {}
74
70
 
75
71
  for file_mapping in file_mappings:
76
72
  local_key, file_path = file_mapping.split(":", 1)
77
73
 
78
- # No file_path means to delete this entry from the next version.
79
- if not file_path:
80
- keys_to_remove.add(local_key)
81
- continue
82
-
83
- media_type_str, _encoding = mimetypes.guess_type(file_path)
84
- media_type = get_or_create_media_type(media_type_str)
85
- content = get_or_create_file_content(
86
- learning_package.id,
87
- media_type.id,
88
- data=pathlib.Path(file_path).read_bytes(),
89
- created=created,
90
- )
91
- local_keys_to_content[local_key] = content.id
74
+ local_keys_to_content_bytes[local_key] = pathlib.Path(file_path).read_bytes() if file_path else None
92
75
 
93
76
  next_version = create_next_component_version(
94
77
  component.pk,
95
- content_to_replace={local_key: None for local_key in keys_to_remove},
78
+ content_to_replace=local_keys_to_content_bytes,
96
79
  created=created,
97
80
  )
98
- for local_key, content_id in sorted(local_keys_to_content.items()):
99
- create_component_version_content(
100
- next_version.pk,
101
- content_id,
102
- key=local_key,
103
- learner_downloadable=True,
104
- )
105
81
 
106
82
  self.stdout.write(
107
83
  f"Created v{next_version.version_num} of "
@@ -0,0 +1,17 @@
1
+ # Generated by Django 4.2.16 on 2024-11-06 17:14
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('oel_components', '0002_alter_componentversioncontent_key'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.RemoveField(
14
+ model_name='componentversioncontent',
15
+ name='learner_downloadable',
16
+ ),
17
+ ]
@@ -254,43 +254,6 @@ class ComponentVersionContent(models.Model):
254
254
  # identifiers that don't map as cleanly to file paths at some point.
255
255
  key = key_field(db_column="_key")
256
256
 
257
- # Long explanation for the ``learner_downloadable`` field:
258
- #
259
- # Is this Content downloadable during the learning experience? This is
260
- # NOT about public vs. private permissions on course assets, as that will be
261
- # a policy that can be changed independently of new versions of the content.
262
- # For instance, a course team could decide to flip their course assets from
263
- # private to public for CDN caching reasons, and that should not require
264
- # new ComponentVersions to be created.
265
- #
266
- # What the ``learner_downloadable`` field refers to is whether this asset is
267
- # supposed to *ever* be directly downloadable by browsers during the
268
- # learning experience. This will be True for things like images, PDFs, and
269
- # video transcript files. This field will be False for things like:
270
- #
271
- # * Problem Block OLX will contain the answers to the problem. The XBlock
272
- # runtime and ProblemBlock will use this information to generate HTML and
273
- # grade responses, but the the user's browser is never permitted to
274
- # actually download the raw OLX itself.
275
- # * Many courses include a python_lib.zip file holding custom Python code
276
- # to be used by codejail to assess student answers. This code will also
277
- # potentially reveal answers, and is never intended to be downloadable by
278
- # the student's browser.
279
- # * Some course teams will upload other file formats that their OLX is
280
- # derived from (e.g. specially formatted LaTeX files). These files will
281
- # likewise contain answers and should never be downloadable by the
282
- # student.
283
- # * Other custom metadata may be attached as files in the import, such as
284
- # custom identifiers, author information, etc.
285
- #
286
- # Even if ``learner_downloadble`` is True, the LMS may decide that this
287
- # particular student isn't allowed to see this particular piece of content
288
- # yet–e.g. because they are not enrolled, or because the exam this Component
289
- # is a part of hasn't started yet. That's a matter of LMS permissions and
290
- # policy that is not intrinsic to the content itself, and exists at a layer
291
- # above this.
292
- learner_downloadable = models.BooleanField(default=False)
293
-
294
257
  class Meta:
295
258
  constraints = [
296
259
  # Uniqueness is only by ComponentVersion and key. If for some reason
@@ -6,6 +6,7 @@ more intelligent data models to be useful.
6
6
  from __future__ import annotations
7
7
 
8
8
  from functools import cache, cached_property
9
+ from logging import getLogger
9
10
 
10
11
  from django.conf import settings
11
12
  from django.core.exceptions import ImproperlyConfigured, ValidationError
@@ -19,6 +20,8 @@ from ....lib.fields import MultiCollationTextField, case_insensitive_char_field,
19
20
  from ....lib.managers import WithRelationsManager
20
21
  from ..publishing.models import LearningPackage
21
22
 
23
+ logger = getLogger()
24
+
22
25
  __all__ = [
23
26
  "MediaType",
24
27
  "Content",
@@ -316,8 +319,11 @@ class Content(models.Model):
316
319
 
317
320
  This will return ``None`` if there is no backing file (has_file=False).
318
321
  """
319
- if self.has_file:
320
- return get_storage().path(self.path)
322
+ try:
323
+ if self.has_file:
324
+ return get_storage().path(self.path)
325
+ except NotImplementedError:
326
+ logger.warning("Storage backend does not support path()")
321
327
  return None
322
328
 
323
329
  def read_file(self) -> File:
@@ -5,7 +5,7 @@ Views for the media server application
5
5
  """
6
6
  from pathlib import Path
7
7
 
8
- from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
8
+ from django.core.exceptions import ObjectDoesNotExist
9
9
  from django.http import FileResponse, Http404
10
10
 
11
11
  from openedx_learning.apps.authoring.components.api import look_up_component_version_content
@@ -34,11 +34,6 @@ def component_asset(
34
34
  except ObjectDoesNotExist:
35
35
  raise Http404("File not found") # pylint: disable=raise-missing-from
36
36
 
37
- if not cvc.learner_downloadable and not (
38
- request.user and request.user.is_superuser
39
- ):
40
- raise PermissionDenied("This file is not publicly downloadable.")
41
-
42
37
  response = FileResponse(cvc.raw_content.file, filename=Path(asset_path).name)
43
38
  response["Content-Type"] = cvc.raw_content.mime_type
44
39
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: openedx-learning
3
- Version: 0.16.1
3
+ Version: 0.17.0
4
4
  Summary: Open edX Learning Core and Tagging.
5
5
  Home-page: https://github.com/openedx/openedx-learning
6
6
  Author: David Ormsbee
@@ -18,12 +18,12 @@ Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Requires-Python: >=3.11
20
20
  License-File: LICENSE.txt
21
+ Requires-Dist: Django<5.0
22
+ Requires-Dist: djangorestframework<4.0
23
+ Requires-Dist: edx-drf-extensions
21
24
  Requires-Dist: attrs
22
25
  Requires-Dist: celery
23
26
  Requires-Dist: rules<4.0
24
- Requires-Dist: Django<5.0
25
- Requires-Dist: edx-drf-extensions
26
- Requires-Dist: djangorestframework<4.0
27
27
 
28
28
  Open edX Learning Core (and Tagging)
29
29
  ====================================
@@ -1,4 +1,4 @@
1
- openedx_learning/__init__.py,sha256=kCKLJzAlWTUBzj2ePkbJPdM3AEukJRgn6BpyRHwAbdo,69
1
+ openedx_learning/__init__.py,sha256=IMpdIFxhnSJC8rI3HtW2SRy57WekVbE3qOwH-dTdmnw,69
2
2
  openedx_learning/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  openedx_learning/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  openedx_learning/api/authoring.py,sha256=vbRpiQ2wOfN3oR2bbN0-bI2ra0QRtha9tVixKW1ENis,929
@@ -17,21 +17,22 @@ openedx_learning/apps/authoring/collections/migrations/0004_collection_key.py,sh
17
17
  openedx_learning/apps/authoring/collections/migrations/0005_alter_collection_options_alter_collection_enabled.py,sha256=HdU_3zxN32nzzvOFpiVpQXleHleJhnq2d8k7jAxhUTM,504
18
18
  openedx_learning/apps/authoring/collections/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  openedx_learning/apps/authoring/components/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
- openedx_learning/apps/authoring/components/admin.py,sha256=3kFu_PR0BFb8U0zVv3WWhi27i__TDuJG0pFlwr3tKAw,4614
21
- openedx_learning/apps/authoring/components/api.py,sha256=TCLFCPb7ScoRWhe7YpQ7hBiJGjoa47OmazQWcN2hIwU,25759
20
+ openedx_learning/apps/authoring/components/admin.py,sha256=zfEpuBEySMYpUZzygaE2MDoI8SH-2H3xIL20YCSCMLo,4582
21
+ openedx_learning/apps/authoring/components/api.py,sha256=Kd8dscUAB5HWX84upmpfK-HjB_1olR7QBP7Ybxn0FVM,24894
22
22
  openedx_learning/apps/authoring/components/apps.py,sha256=YoYPsI9gcleA3uEs8CiLIrjUncRMo2DKbYt4mDfzePg,770
23
- openedx_learning/apps/authoring/components/models.py,sha256=T-wc7vxaWMlulQmMsVH7m6Pd857P3Eguo0vtflTLURI,13415
23
+ openedx_learning/apps/authoring/components/models.py,sha256=VX-pi4ZPJkCP3UtyTcVihgBdBj2pmenwdVC_xx8wCi0,11190
24
24
  openedx_learning/apps/authoring/components/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
25
  openedx_learning/apps/authoring/components/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
- openedx_learning/apps/authoring/components/management/commands/add_assets_to_component.py,sha256=v7mJfnu25M8w4-U1lPPn575W7lYAaOapcHEAZVaxil4,4142
26
+ openedx_learning/apps/authoring/components/management/commands/add_assets_to_component.py,sha256=0dJ77NZZoNzYheOdFPXtJrjdL_Z-pCNg3l1rbEGnMCY,3175
27
27
  openedx_learning/apps/authoring/components/migrations/0001_initial.py,sha256=446LkJSFeK8J_-l-bxakZ_BVx_CiJIllGcBYqWcEenA,4664
28
28
  openedx_learning/apps/authoring/components/migrations/0002_alter_componentversioncontent_key.py,sha256=98724dtucRjJCRyLt5p45qXYb2d6-ouVGp7PB6zTG6E,539
29
+ openedx_learning/apps/authoring/components/migrations/0003_remove_componentversioncontent_learner_downloadable.py,sha256=hDAkKdBvKULepML9pVMqkZg31nAyCeszQHJsFJ4qGws,382
29
30
  openedx_learning/apps/authoring/components/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
31
  openedx_learning/apps/authoring/contents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
32
  openedx_learning/apps/authoring/contents/admin.py,sha256=9Njd_lje1emcd168KBWUTGf0mVJ6K-dMYMcqHNjRU4k,1761
32
33
  openedx_learning/apps/authoring/contents/api.py,sha256=bXb9yQjPfoP1Ynf1aAYz3BEPffK7H5cnba6KdPFSiG0,8818
33
34
  openedx_learning/apps/authoring/contents/apps.py,sha256=EEUZEnww7TcYcyxMovZthG2muNxd7j7nxBIf21gKrp4,398
34
- openedx_learning/apps/authoring/contents/models.py,sha256=nv6T0SXHJovs0FeAtED1iPg_HbkJF5vz2N6WyzYXS6Q,17720
35
+ openedx_learning/apps/authoring/contents/models.py,sha256=RobNGdqkFhChcpub9FszOeqft4c1TJQgS5vQ3Cp2NCA,17899
35
36
  openedx_learning/apps/authoring/contents/migrations/0001_initial.py,sha256=FtOTmIGX2KHpjw-PHbfRjxkFEomI5CEDhNKCZ7IpFeE,3060
36
37
  openedx_learning/apps/authoring/contents/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
38
  openedx_learning/apps/authoring/publishing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -47,7 +48,7 @@ openedx_learning/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
47
48
  openedx_learning/contrib/media_server/__init__.py,sha256=iYijWFCl5RNR9omSu22kMl49EfponoqXBqXr0HMp4QI,56
48
49
  openedx_learning/contrib/media_server/apps.py,sha256=FPT0rsUFtPyhFpWKjSI1e_s58wU0IbDyaAW_66V6sY4,816
49
50
  openedx_learning/contrib/media_server/urls.py,sha256=newNjV41sM9A9Oy_rgnZSXdkTFxSHiupIiAsVIGE2CE,365
50
- openedx_learning/contrib/media_server/views.py,sha256=A4umcQr9xU5l-7dCSUX9jP9-gtwf4LGjoMn7NlsdLsk,1547
51
+ openedx_learning/contrib/media_server/views.py,sha256=qZPhdEW_oYj1MEdgLVP6Cq3tRiZtp7dTb7ASaSKZ2HY,1350
51
52
  openedx_learning/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
53
  openedx_learning/lib/admin_utils.py,sha256=5z9NrXxmT5j8azx9u1t0AgxV5PIDTc2jPyM5z5yW8cw,4021
53
54
  openedx_learning/lib/cache.py,sha256=ppT36KiPLdsAF3GfZCF0IdiHodckd2gLiF1sNhjSJuk,958
@@ -111,8 +112,8 @@ openedx_tagging/core/tagging/rest_api/v1/serializers.py,sha256=D7brBbgmU7MnbU7Ln
111
112
  openedx_tagging/core/tagging/rest_api/v1/urls.py,sha256=dNUKCtUCx_YzrwlbEbpDfjGVQbb2QdJ1VuJCkladj6E,752
112
113
  openedx_tagging/core/tagging/rest_api/v1/views.py,sha256=LA0EF7-p91JgVrLZpZaG474elKD1dswODGvoPIw47Mg,35837
113
114
  openedx_tagging/core/tagging/rest_api/v1/views_import.py,sha256=kbHUPe5A6WaaJ3J1lFIcYCt876ecLNQfd19m7YYub6c,1470
114
- openedx_learning-0.16.1.dist-info/LICENSE.txt,sha256=QTW2QN7q3XszgUAXm9Dzgtu5LXYKbR1SGnqMa7ufEuY,35139
115
- openedx_learning-0.16.1.dist-info/METADATA,sha256=hJck_BnmUyKCAGNXMfVpiJrfiI5iZGoAq6OSR88enOI,8777
116
- openedx_learning-0.16.1.dist-info/WHEEL,sha256=AHX6tWk3qWuce7vKLrj7lnulVHEdWoltgauo8bgCXgU,109
117
- openedx_learning-0.16.1.dist-info/top_level.txt,sha256=IYFbr5mgiEHd-LOtZmXj3q3a0bkGK1M9LY7GXgnfi4M,33
118
- openedx_learning-0.16.1.dist-info/RECORD,,
115
+ openedx_learning-0.17.0.dist-info/LICENSE.txt,sha256=QTW2QN7q3XszgUAXm9Dzgtu5LXYKbR1SGnqMa7ufEuY,35139
116
+ openedx_learning-0.17.0.dist-info/METADATA,sha256=m44VGrMwjZ7BC1hHVgyx3lA-0jUYasgctFJ4WxswbNU,8777
117
+ openedx_learning-0.17.0.dist-info/WHEEL,sha256=OpXWERl2xLPRHTvd2ZXo_iluPEQd8uSbYkJ53NAER_Y,109
118
+ openedx_learning-0.17.0.dist-info/top_level.txt,sha256=IYFbr5mgiEHd-LOtZmXj3q3a0bkGK1M9LY7GXgnfi4M,33
119
+ openedx_learning-0.17.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.1.0)
2
+ Generator: setuptools (75.3.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py2-none-any
5
5
  Tag: py3-none-any