content_system.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. """
  2. A registry for content sources that work in terms of the View Model (view_model.py).
  3. Generally a source returns a CollectionPage or individual items.
  4. At present many sources return a List of Maps because the design is being discovered and solidified as it makes sense rather than big design up front.
  5. May end up similar to Android's ContentProvider, found later. I was
  6. thinking about using content:// URI scheme.
  7. https://developer.android.com/reference/android/content/ContentProvider
  8. Could also be similar to a Coccoon Generator
  9. https://cocoon.apache.org/1363_1_1.html
  10. Later processing in Python:
  11. https://www.innuy.com/blog/build-data-pipeline-python/
  12. https://www.bonobo-project.org/
  13. """
  14. import re
  15. import inspect
  16. content_sources = {}
  17. hooks = {}
  18. def register_content_source (id_prefix, content_source_fn, id_pattern='(\d+)', source_id=None):
  19. if not source_id:
  20. source_id=f'{inspect.getmodule(content_source_fn).__name__}:{content_source_fn.__name__}'
  21. print(f'register_content_source: {id_prefix}: {source_id} with ID pattern {id_pattern}')
  22. content_sources[ id_prefix ] = [content_source_fn, id_pattern, source_id]
  23. def find_content_id_args (id_pattern, content_id):
  24. id_args = re.fullmatch(id_pattern, content_id)
  25. if not id_args:
  26. return [], {}
  27. args = []
  28. kwargs = id_args.groupdict()
  29. if not kwargs:
  30. args = id_args.groups()
  31. return args, kwargs
  32. def get_content (content_id, *extra_args, **extra_kwargs):
  33. id_prefixes = list(content_sources.keys())
  34. id_prefixes.sort(key=lambda id_prefix: len(id_prefix), reverse=True)
  35. for id_prefix in id_prefixes:
  36. [content_source_fn, id_pattern, source_id] = content_sources[ id_prefix ]
  37. if not content_id.startswith(id_prefix):
  38. continue
  39. source_content_id = content_id[len(id_prefix):]
  40. print(f'get_content {content_id} from source {source_id}, resolves to {source_content_id}')
  41. args, kwargs = find_content_id_args(id_pattern, source_content_id)
  42. if id_prefix.endswith(':') and not args and not kwargs:
  43. continue
  44. if extra_args:
  45. args += extra_args
  46. if extra_kwargs:
  47. kwargs = {**extra_kwargs, **kwargs}
  48. content = content_source_fn(*args, **kwargs)
  49. if content:
  50. invoke_hooks('got_content', content_id, content)
  51. return content
  52. def get_all_content (content_ids):
  53. """
  54. Get content from all sources, using a grouping call if possible.
  55. Returns a map of source_id to to result; the caller needs
  56. to have the intelligence to merge and paginate.
  57. Native implementation is to juse make one call to get_content per ID,
  58. but we need to figure out a way to pass a list of IDs and pagination
  59. per source; for exampe a list of 100+ Tweet IDs and 100+ YT videos
  60. from a Swipe file.
  61. """
  62. return get_all_content2(content_ids)
  63. def get_all_content2 (content_collection_ids, content_args = None, max_results = None):
  64. """
  65. Takes a list of collection IDs and content_args is a map of (args, kwargs) keyed by collection ID.
  66. We could just use keys from content_args with empty values but that's a little confusing.
  67. Interleaving the next page of a source into existing results is a problem.
  68. Gracefully degraded could simply get the next page at the end of all pages and then
  69. view older content.
  70. We also need intelligence about content types, meaning perhaps some lambdas pass in.
  71. Eg. CollectionPage.
  72. See feeds facade for an example of merging one page.
  73. Seems like keeping feed items in a DB is becoming the way to go, serving things in order.
  74. Client side content merging might work to insert nodes above, eg. HTMx.
  75. Might be jarring to reader, so make optional. Append all new or merge.
  76. Cache feed between requests on disk, merge in memory, send merge/append result.
  77. """
  78. result = {}
  79. for content_id in content_collection_ids:
  80. if content_args and content_id in content_args:
  81. extra_args, extra_kwargs = content_args[content_id]
  82. result[ content_id ] = get_content(content_id, *extra_args, **extra_kwargs)
  83. return result
  84. def register_hook (hook_type, hook_fn, *extra_args, **extra_kwargs):
  85. if not hook_type in hooks:
  86. hooks[hook_type] = []
  87. hooks[hook_type].append([hook_fn, extra_args, extra_kwargs])
  88. def invoke_hooks (hook_type, *args, **kwargs):
  89. if not hook_type in hooks:
  90. return
  91. for hook, extra_args, extra_kwargs in hooks[hook_type]:
  92. hook_args = args
  93. hook_kwargs = kwargs
  94. if extra_args:
  95. hook_args = args + extra_args
  96. if extra_kwargs:
  97. hook_kwargs = {**extra_kwargs, **hook_kwargs}
  98. hook(*hook_args, **hook_kwargs)
  99. #try:
  100. # hook(*args, **kwargs)
  101. #except TypeError as e:
  102. # print ('tried to call a hook with wrong args. no problem')
  103. # continue