github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/scripts/discourse-sync/main.py (about)

     1  import os
     2  import sys
     3  import yaml
     4  from pydiscourse import DiscourseClient
     5  from pydiscourse.exceptions import DiscourseClientError
     6  
     7  
     8  # Get configuration from environment variables
     9  DISCOURSE_HOST = os.environ.get('DISCOURSE_HOST', 'https://discourse.charmhub.io/')
    10  DISCOURSE_API_USERNAME = os.environ.get('DISCOURSE_API_USERNAME')
    11  DISCOURSE_API_KEY = os.environ.get('DISCOURSE_API_KEY')
    12  DOCS_DIR = os.environ.get('DOCS_DIR')
    13  TOPIC_IDS = os.environ.get('TOPIC_IDS')
    14  
    15  client = DiscourseClient(
    16      host=DISCOURSE_HOST,
    17      api_username=DISCOURSE_API_USERNAME,
    18      api_key=DISCOURSE_API_KEY,
    19  )
    20  
    21  
    22  def main():
    23      if len(sys.argv) < 1:
    24          sys.exit('no command provided, must be one of: check, sync, create, delete')
    25  
    26      command = sys.argv[1]
    27      if command == 'check':
    28          check()
    29      elif command == 'sync':
    30          sync()
    31      elif command == 'create':
    32          create()
    33      elif command == 'delete':
    34          delete()
    35      else:
    36          exit(f'unknown command "{command}"')
    37  
    38  
    39  def check():
    40      """
    41      Check all docs in the DOCS_DIR have a corresponding entry in the TOPIC_IDS
    42      file, and that the corresponding topic exists on Discourse.
    43      """
    44      topic_ids = get_topic_ids()
    45      no_topic_id = []
    46      no_discourse_topic = []
    47  
    48      for entry in os.scandir(DOCS_DIR):
    49          if not is_markdown_file(entry):
    50              print(f'entry {entry.name}: not a Markdown file: skipping')
    51              continue
    52  
    53          doc_name = removesuffix(entry.name, ".md")
    54  
    55          if doc_name not in topic_ids:
    56              print(f'doc {doc_name}: no topic ID found')
    57              no_topic_id.append(doc_name)
    58              continue
    59  
    60          topic_id = topic_ids[doc_name]
    61          print(f'doc {doc_name} (topic #{topic_id}): checking topic on Discourse')
    62          try:
    63              client.topic(
    64                  slug='',
    65                  topic_id=topic_ids[doc_name],
    66              )
    67          except DiscourseClientError:
    68              print(f'doc {doc_name} (topic #{topic_id}): not found on Discourse')
    69              no_discourse_topic.append(doc_name)
    70  
    71      if no_topic_id:
    72          print(f"The following docs don't have corresponding entries in {TOPIC_IDS}.")
    73          print(f"Please create new Discourse topics for them, and add the new topic IDs to {TOPIC_IDS}.")
    74          for doc_name in no_topic_id:
    75              print(f' - {doc_name}')
    76  
    77      if no_discourse_topic:
    78          print("The following docs don't have corresponding topics on Discourse.")
    79          print(f"Please create new Discourse topics for them, and update the topic IDs in f{TOPIC_IDS}.")
    80          for doc_name in no_discourse_topic:
    81              print(f' - {doc_name} (topic #{topic_ids[doc_name]})')
    82  
    83      if no_topic_id or no_discourse_topic:
    84          sys.exit(1)
    85  
    86  
    87  def sync():
    88      """
    89      Sync all docs in the DOCS_DIR with their corresponding topics on Discourse.
    90      """
    91      topic_ids = get_topic_ids()
    92      couldnt_sync = {}  # doc_name -> reason
    93  
    94      for entry in os.scandir(DOCS_DIR):
    95          if not is_markdown_file(entry):
    96              print(f'entry {entry.name}: not a Markdown file: skipping')
    97              continue
    98  
    99          doc_name = removesuffix(entry.name, ".md")
   100          content = open(entry.path, 'r').read()
   101  
   102          if doc_name not in topic_ids:
   103              couldnt_sync[doc_name] = 'no topic ID in yaml file'
   104              continue
   105  
   106          topic_id = topic_ids[doc_name]
   107          print(f'doc {doc_name} (topic #{topic_id}): checking for changes')
   108          try:
   109              # API call to get the post ID from the topic ID
   110              # TODO: we could save the post IDs in a separate yaml file and
   111              #   avoid this extra API call
   112              topic = client.topic(
   113                  slug='',
   114                  topic_id=topic_id,
   115              )
   116          except DiscourseClientError:
   117              couldnt_sync[doc_name] = f'no topic with ID #{topic_id} on Discourse'
   118              continue
   119  
   120          post_id = topic['post_stream']['posts'][0]['id']
   121          # Get current contents of post
   122          try:
   123              post2 = client.post_by_id(
   124                  post_id=post_id
   125              )
   126          except DiscourseClientError as e:
   127              couldnt_sync[doc_name] = f"couldn't get post for topic ID #{topic_id}: {e}"
   128              continue
   129  
   130          current_contents = post2['raw']
   131          if current_contents == content.rstrip('\n'):
   132              print(f'doc {doc_name} (topic #{topic_ids[doc_name]}): already up-to-date: skipping')
   133              continue
   134  
   135          # Update Discourse post
   136          print(f'doc {doc_name} (topic #{topic_ids[doc_name]}): updating')
   137          try:
   138              client.update_post(
   139                  post_id=post_id,
   140                  content=content,
   141              )
   142          except DiscourseClientError as e:
   143              couldnt_sync[doc_name] = f"couldn't update post with ID #{post_id}: {e}"
   144              continue
   145  
   146      if len(couldnt_sync) > 0:
   147          print("Failed to sync the following docs:")
   148          for doc_name, reason in couldnt_sync.items():
   149              print(f' - {doc_name}: {reason}')
   150          sys.exit(1)
   151  
   152  
   153  def create():
   154      """
   155      Create new Discourse topics for each doc name provided.
   156      """
   157      topic_ids = get_topic_ids()
   158      docs = sys.argv[2:]
   159  
   160      for doc_name in docs:
   161          if doc_name in topic_ids:
   162              print(f'skipping doc {doc_name}, it already has a topic ID')
   163              continue
   164  
   165          path = os.path.join(DOCS_DIR, doc_name+'.md')
   166          try:
   167              content = open(path, 'r').read()
   168          except OSError as e:
   169              print(f"couldn't open {path}: {e}")
   170              continue
   171  
   172          # Create new Discourse post
   173          print(f'creating new post for doc {doc_name}')
   174          post = client.create_post(
   175              title=post_title(doc_name),
   176              category_id=22,
   177              content=content,
   178              tags=['olm', 'autogenerated'],
   179          )
   180          new_topic_id = post['topic_id']
   181          print(f'doc {doc_name}: created new topic #{new_topic_id}')
   182  
   183          # Save topic ID in yaml map for later
   184          topic_ids[doc_name] = new_topic_id
   185          with open(TOPIC_IDS, 'w') as file:
   186              yaml.safe_dump(topic_ids, file)
   187  
   188  
   189  def delete():
   190      """
   191      Delete all Discourse topics in the TOPIC_IDS file.
   192      """
   193      topic_ids = get_topic_ids()
   194  
   195      for doc_name, topic_id in topic_ids.items():
   196          print(f'deleting doc {doc_name} (topic #{topic_id})')
   197          client.delete_topic(
   198              topic_id=topic_id
   199          )
   200  
   201          # Update topic ID yaml map
   202          del topic_ids[doc_name]
   203          with open(TOPIC_IDS, 'w') as file:
   204              yaml.safe_dump(topic_ids, file)
   205  
   206  
   207  def get_topic_ids():
   208      with open(TOPIC_IDS, 'r') as file:
   209          topic_ids = yaml.safe_load(file)
   210          return topic_ids or {}
   211  
   212  
   213  def is_markdown_file(entry: os.DirEntry) -> bool:
   214      return entry.is_file() and entry.name.endswith(".md")
   215  
   216  
   217  def removesuffix(text, suffix):
   218      if suffix and text.endswith(suffix):
   219          return text[:-len(suffix)]
   220      return text
   221  
   222  
   223  def post_title(doc_name: str) -> str:
   224      if doc_name == 'index':
   225          return 'Juju CLI commands'
   226      return f"Command '{doc_name}'"
   227  
   228  
   229  if __name__ == "__main__":
   230      main()