LGTM (40 Part Series)
1 LGTM Devlog 0: Teaching Git through playing a game
2 LGTM Devlog 1: New Logo, and jumping the gun by buying a domain
… 36 more parts…
3 LGTM Devlog 2: Design Ideas for the game I
4 LGTM Devlog 3: Design Ideas for the game 2
5 LGTM Devlog 4: Technical Decisions – GitHub API and Python
6 LGTM Devlog 5: Technical Decisions – Serverless Architecture
7 LGTM Devlog 6: Sprint 1 plans
8 LGTM Devlog 7: Creating a Firebase project
9 LGTM Devlog 8: Starting a git repo for an open source project from scratch
10 LGTM Devlog 9: Python Google Cloud Functions with Unit Tests and Linting
11 LGTM Devlog 10: Capturing the GitHub webhook for fork requests
12 LGTM Devlog 11: Writing the Serverless Function for receiving GitHub webhooks with Pydantic validation
13 LGTM Devlog 12: CI/CD with GitHub Actions to run Unit Tests and deploy Firebase Functions
14 LGTM Devlog 13: GitHub Branch Protection and Security
15 LGTM Devlog 14: Sprint 1 Retrospective
16 LGTM Devlog 15: Sprint 2 plans
17 LGTM Devlog 16: A serverless data base access rule conundrum
18 LGTM Devlog 17: Website and GitHub OAuth
19 LGTM Devlog 18: Python Serverless functions using GitHub API to validate users
20 LGTM Devlog 19: Game data/quest storage
21 LGTM Devlog 20: Python Abstract Base Class-based data/quest storage
22 LGTM Devlog 21: Deploying Pub/Sub-triggered Python Google Cloud Functions
23 LGTM Devlog 22: Modularization
24 LGTM Devlog 23: Sprint 2 Retrospective
25 LGTM Devlog 24: Sprint 3 plans
26 LGTM Devlog 25: Some cleanup
27 LGTM Devlog 26: Python Graphlib DAGs for Quest Stages
28 LGTM Devlog 27: Branching quests
29 LGTM Devlog 28: Game event loop using Google Cloud Scheduler and PubSub
30 LGTM Devlog 29: ORM for Firestore and __init__subclass__ dunders and metaclasses
31 LGTM Devlog 30: Sprint 3 Retrospective
32 LGTM Devlog 31: Sprint 4 Plan
33 LGTM Devlog 32: Secrets Management to avoid storing API keys in services
34 LGTM Devlog 33: Using PyGithub to post GitHub issues and comments
35 LGTM Devlog 34: Characters Posting on GitHub Issues
36 LGTM Devlog 35: Responding to the player’s answers on GitHub Issues Comments
37 LGTM Devlog 36: Character profiles! On GitHub
38 LGTM Devlog 37: Sprint 4 Retrospective
39 LGTM Devlog 38: Sprint 5 Plan
40 LGTM Devlog 39: Planning the story
Continuing on from last post, I’m adding more types of stages to the stages
module which contain the actual implementation of quests. This is an exciting time because after all the framework building, this is finally where the core of what LGTM is begins to emerge. Again, most of the work is now in the stages
module, the code for this post is at commit 0e67f0d
Sending comments
The previous post describes the CreateIssueStage()
which creates an issue. But we also need to be able to create comments, so I have a viry similar-looking CreateIssueCommentsStage()
. The reason it’s comments
plural is because this stage allows me to define a whole conversation that takes place in the comments section between two different accounts. Makes it easy to convey narrative through a conversation.
<span>class</span> <span>CreateIssueConversationStage</span><span>(</span><span>Stage</span><span>):</span><span>""" This stage posts multiple comment to an existing issue to a user's fork """</span><span>@</span><span>property</span><span>@</span><span>abstractmethod</span><span>def</span> <span>character_comment_pairs</span><span>(</span><span>cls</span><span>)</span> <span>-></span> <span>List</span><span>[</span><span>Tuple</span><span>[</span><span>Character</span><span>,</span> <span>str</span><span>]]:</span><span>""" Pairs of characters and comments to post """</span><span>return</span> <span>NotImplemented</span><span>@</span><span>property</span><span>@</span><span>abstractmethod</span><span>def</span> <span>issue_id_variable</span><span>(</span><span>cls</span><span>)</span> <span>-></span> <span>int</span><span>:</span><span>""" Variable containing the ID of the issue to use """</span><span>return</span> <span>NotImplemented</span><span># variable to store last comment ID in for later </span> <span>comment_id_variable</span><span>:</span> <span>Optional</span><span>[</span><span>str</span><span>]</span> <span>=</span> <span>None</span><span># variable to store datetime of comment </span> <span>comment_datetime_variable</span><span>:</span> <span>Optional</span><span>[</span><span>str</span><span>]</span> <span>=</span> <span>None</span><span>def</span> <span>execute</span><span>(</span><span>self</span><span>)</span> <span>-></span> <span>None</span><span>:</span><span>""" Post the issue to the fork """</span><span>game</span> <span>=</span> <span>self</span><span>.</span><span>quest</span><span>.</span><span>quest_page</span><span>.</span><span>game</span><span>game</span><span>.</span><span>load</span><span>()</span><span>fork_url</span> <span>=</span> <span>game</span><span>.</span><span>data</span><span>.</span><span>fork_url</span><span>issue_id</span> <span>=</span> <span>getattr</span><span>(</span><span>self</span><span>.</span><span>quest</span><span>.</span><span>quest_data</span><span>,</span> <span>self</span><span>.</span><span>issue_id_variable</span><span>)</span><span>logger</span><span>.</span><span>info</span><span>(</span><span>"Creating comments"</span><span>,</span> <span>issue_id</span><span>=</span><span>issue_id</span><span>,</span> <span>fork_url</span><span>=</span><span>fork_url</span><span>)</span><span>for</span> <span>character</span><span>,</span> <span>body</span> <span>in</span> <span>self</span><span>.</span><span>character_comment_pairs</span><span>:</span><span>comment_id</span> <span>=</span> <span>character</span><span>.</span><span>issue_comment_create</span><span>(</span><span>fork_url</span><span>,</span> <span>issue_id</span><span>,</span> <span>body</span><span>)</span><span>if</span> <span>self</span><span>.</span><span>comment_id_variable</span><span>:</span><span>logger</span><span>.</span><span>info</span><span>(</span><span>"Storing comment Id in variable"</span><span>,</span><span>comment_id</span><span>=</span><span>comment_id</span><span>,</span><span>variable</span><span>=</span><span>self</span><span>.</span><span>comment_id_variable</span><span>,</span><span>)</span><span>setattr</span><span>(</span><span>self</span><span>.</span><span>quest</span><span>.</span><span>quest_data</span><span>,</span> <span>self</span><span>.</span><span>comment_id_variable</span><span>,</span> <span>comment_id</span><span>)</span><span>if</span> <span>self</span><span>.</span><span>comment_datetime_variable</span><span>:</span><span>setattr</span><span>(</span><span>self</span><span>.</span><span>quest</span><span>.</span><span>quest_data</span><span>,</span> <span>self</span><span>.</span><span>comment_datetime_variable</span><span>,</span> <span>datetime</span><span>.</span><span>now</span><span>()</span><span>)</span><span>class</span> <span>CreateIssueConversationStage</span><span>(</span><span>Stage</span><span>):</span> <span>""" This stage posts multiple comment to an existing issue to a user's fork """</span> <span>@</span><span>property</span> <span>@</span><span>abstractmethod</span> <span>def</span> <span>character_comment_pairs</span><span>(</span><span>cls</span><span>)</span> <span>-></span> <span>List</span><span>[</span><span>Tuple</span><span>[</span><span>Character</span><span>,</span> <span>str</span><span>]]:</span> <span>""" Pairs of characters and comments to post """</span> <span>return</span> <span>NotImplemented</span> <span>@</span><span>property</span> <span>@</span><span>abstractmethod</span> <span>def</span> <span>issue_id_variable</span><span>(</span><span>cls</span><span>)</span> <span>-></span> <span>int</span><span>:</span> <span>""" Variable containing the ID of the issue to use """</span> <span>return</span> <span>NotImplemented</span> <span># variable to store last comment ID in for later </span> <span>comment_id_variable</span><span>:</span> <span>Optional</span><span>[</span><span>str</span><span>]</span> <span>=</span> <span>None</span> <span># variable to store datetime of comment </span> <span>comment_datetime_variable</span><span>:</span> <span>Optional</span><span>[</span><span>str</span><span>]</span> <span>=</span> <span>None</span> <span>def</span> <span>execute</span><span>(</span><span>self</span><span>)</span> <span>-></span> <span>None</span><span>:</span> <span>""" Post the issue to the fork """</span> <span>game</span> <span>=</span> <span>self</span><span>.</span><span>quest</span><span>.</span><span>quest_page</span><span>.</span><span>game</span> <span>game</span><span>.</span><span>load</span><span>()</span> <span>fork_url</span> <span>=</span> <span>game</span><span>.</span><span>data</span><span>.</span><span>fork_url</span> <span>issue_id</span> <span>=</span> <span>getattr</span><span>(</span><span>self</span><span>.</span><span>quest</span><span>.</span><span>quest_data</span><span>,</span> <span>self</span><span>.</span><span>issue_id_variable</span><span>)</span> <span>logger</span><span>.</span><span>info</span><span>(</span><span>"Creating comments"</span><span>,</span> <span>issue_id</span><span>=</span><span>issue_id</span><span>,</span> <span>fork_url</span><span>=</span><span>fork_url</span><span>)</span> <span>for</span> <span>character</span><span>,</span> <span>body</span> <span>in</span> <span>self</span><span>.</span><span>character_comment_pairs</span><span>:</span> <span>comment_id</span> <span>=</span> <span>character</span><span>.</span><span>issue_comment_create</span><span>(</span><span>fork_url</span><span>,</span> <span>issue_id</span><span>,</span> <span>body</span><span>)</span> <span>if</span> <span>self</span><span>.</span><span>comment_id_variable</span><span>:</span> <span>logger</span><span>.</span><span>info</span><span>(</span> <span>"Storing comment Id in variable"</span><span>,</span> <span>comment_id</span><span>=</span><span>comment_id</span><span>,</span> <span>variable</span><span>=</span><span>self</span><span>.</span><span>comment_id_variable</span><span>,</span> <span>)</span> <span>setattr</span><span>(</span><span>self</span><span>.</span><span>quest</span><span>.</span><span>quest_data</span><span>,</span> <span>self</span><span>.</span><span>comment_id_variable</span><span>,</span> <span>comment_id</span><span>)</span> <span>if</span> <span>self</span><span>.</span><span>comment_datetime_variable</span><span>:</span> <span>setattr</span><span>(</span> <span>self</span><span>.</span><span>quest</span><span>.</span><span>quest_data</span><span>,</span> <span>self</span><span>.</span><span>comment_datetime_variable</span><span>,</span> <span>datetime</span><span>.</span><span>now</span><span>()</span> <span>)</span>class CreateIssueConversationStage(Stage): """ This stage posts multiple comment to an existing issue to a user's fork """ @property @abstractmethod def character_comment_pairs(cls) -> List[Tuple[Character, str]]: """ Pairs of characters and comments to post """ return NotImplemented @property @abstractmethod def issue_id_variable(cls) -> int: """ Variable containing the ID of the issue to use """ return NotImplemented # variable to store last comment ID in for later comment_id_variable: Optional[str] = None # variable to store datetime of comment comment_datetime_variable: Optional[str] = None def execute(self) -> None: """ Post the issue to the fork """ game = self.quest.quest_page.game game.load() fork_url = game.data.fork_url issue_id = getattr(self.quest.quest_data, self.issue_id_variable) logger.info("Creating comments", issue_id=issue_id, fork_url=fork_url) for character, body in self.character_comment_pairs: comment_id = character.issue_comment_create(fork_url, issue_id, body) if self.comment_id_variable: logger.info( "Storing comment Id in variable", comment_id=comment_id, variable=self.comment_id_variable, ) setattr(self.quest.quest_data, self.comment_id_variable, comment_id) if self.comment_datetime_variable: setattr( self.quest.quest_data, self.comment_datetime_variable, datetime.now() )
Enter fullscreen mode Exit fullscreen mode
In the CreateIssueStage()
we created an issue and stored the issue_id in the quest data model. This stage now reads that ID, as it’s needed for it to decide which issue to post in.
It simply loops through a list of character and text tuples to post each comment in turn. It additionally stores the last comment’s comment_id in the quest storage as well as its post time. The reason for this is in case we have another quest stage that needs to detect emoji reactions to a comment. The comment post time can be used to fetch comments from that date onwards, to avoid seeing older comments no longer relevant to this stage.
Checking for replies
The final piece of the puzzle needed to build the most basic form of quest, is to detect a response from a user. This type of quest will ask the user to “find out” something in a git repository or the history or metadata, and the user must respond with an answer. We have to be able to detect the correct answer returned, so this stage checks for user replies:
<span>class</span> <span>CheckIssueCommentReply</span><span>(</span><span>Stage</span><span>):</span><span>""" Check issues for reply """</span><span>@</span><span>property</span><span>@</span><span>abstractmethod</span><span>def</span> <span>character</span><span>(</span><span>cls</span><span>)</span> <span>-></span> <span>Character</span><span>:</span><span>""" Which character will do the check and reply, character needs permission for the repo """</span><span>return</span> <span>NotImplemented</span><span>@</span><span>property</span><span>@</span><span>abstractmethod</span><span>def</span> <span>regex_pattern</span><span>(</span><span>cls</span><span>)</span> <span>-></span> <span>Pattern</span><span>:</span><span>""" Compiled regex pattern using re.compile() """</span><span>return</span> <span>NotImplemented</span><span>@</span><span>property</span><span>@</span><span>abstractmethod</span><span>def</span> <span>issue_id_variable</span><span>(</span><span>cls</span><span>)</span> <span>-></span> <span>int</span><span>:</span><span>""" Variable containing the ID of the issue to use """</span><span>return</span> <span>NotImplemented</span><span># A list of possible responses </span> <span>incorrect_responses</span><span>:</span> <span>List</span><span>[</span><span>str</span><span>]</span> <span>=</span> <span>[]</span><span># variable to store matching group values in </span> <span>result_groups_variable</span><span>:</span> <span>Optional</span><span>[</span><span>str</span><span>]</span> <span>=</span> <span>None</span><span># variable to store matching id in in </span> <span>result_id_variable</span><span>:</span> <span>Optional</span><span>[</span><span>str</span><span>]</span> <span>=</span> <span>None</span><span># variable to get check since from </span> <span>comment_datetime_variable</span><span>:</span> <span>Optional</span><span>[</span><span>str</span><span>]</span> <span>=</span> <span>None</span><span>def</span> <span>fast_condition</span><span>(</span><span>self</span><span>)</span> <span>-></span> <span>bool</span><span>:</span><span>"""If hasn't been run before, run once, otherwise fail to avoid hitting github API too much, letting notification scan process run this quest when it receives a notification"""</span><span>if</span> <span>self</span><span>.</span><span>get_stage_data</span><span>()</span> <span>is</span> <span>None</span><span>:</span><span>return</span> <span>self</span><span>.</span><span>condition</span><span>()</span><span>return</span> <span>False</span><span>def</span> <span>condition</span><span>(</span><span>self</span><span>)</span> <span>-></span> <span>bool</span><span>:</span><span>""" Check messages """</span><span>game</span> <span>=</span> <span>self</span><span>.</span><span>quest</span><span>.</span><span>quest_page</span><span>.</span><span>game</span><span>game</span><span>.</span><span>load</span><span>()</span><span>fork_url</span> <span>=</span> <span>game</span><span>.</span><span>data</span><span>.</span><span>fork_url</span><span>user</span> <span>=</span> <span>game</span><span>.</span><span>parent</span><span>user</span><span>.</span><span>load</span><span>()</span><span>user_id</span> <span>=</span> <span>user</span><span>.</span><span>data</span><span>.</span><span>id</span><span>issue_id</span> <span>=</span> <span>getattr</span><span>(</span><span>self</span><span>.</span><span>quest</span><span>.</span><span>quest_data</span><span>,</span> <span>self</span><span>.</span><span>issue_id_variable</span><span>)</span><span># use either last runtime (saved in stage data), or otherwise last comment datetime variable provided </span> <span>check_datetime</span> <span>=</span> <span>datetime</span><span>.</span><span>utcfromtimestamp</span><span>(</span><span>self</span><span>.</span><span>get_stage_data</span><span>(</span><span>0</span><span>))</span><span>if</span> <span>self</span><span>.</span><span>comment_datetime_variable</span> <span>is</span> <span>not</span> <span>None</span><span>:</span><span>check_datetime</span> <span>=</span> <span>max</span><span>(</span><span>check_datetime</span><span>,</span><span>getattr</span><span>(</span><span>self</span><span>.</span><span>quest</span><span>.</span><span>quest_data</span><span>,</span> <span>self</span><span>.</span><span>comment_datetime_variable</span><span>),</span><span>)</span><span>logger</span><span>.</span><span>info</span><span>(</span><span>"Fetching comments"</span><span>,</span><span>user_id</span><span>=</span><span>user_id</span><span>,</span><span>issue_id</span><span>=</span><span>issue_id</span><span>,</span><span>fork_url</span><span>=</span><span>fork_url</span><span>,</span><span>check_datetime</span><span>=</span><span>check_datetime</span><span>,</span><span>)</span><span>if</span> <span>check_datetime</span> <span>is</span> <span>None</span><span>:</span><span>comments</span> <span>=</span> <span>self</span><span>.</span><span>character</span><span>.</span><span>issue_comment_get_from_user</span><span>(</span><span>fork_url</span><span>,</span> <span>issue_id</span><span>,</span> <span>user_id</span><span>)</span><span>else</span><span>:</span><span>comments</span> <span>=</span> <span>self</span><span>.</span><span>character</span><span>.</span><span>issue_comment_get_from_user_since</span><span>(</span><span>fork_url</span><span>,</span> <span>issue_id</span><span>,</span> <span>user_id</span><span>,</span> <span>check_datetime</span><span>)</span><span>logger</span><span>.</span><span>info</span><span>(</span><span>"Got comments"</span><span>,</span> <span>count</span><span>=</span><span>len</span><span>(</span><span>comments</span><span>))</span><span>self</span><span>.</span><span>set_stage_data</span><span>(</span><span>datetime</span><span>.</span><span>now</span><span>().</span><span>timestamp</span><span>())</span><span>for</span> <span>comment_id</span><span>,</span> <span>comment_body</span> <span>in</span> <span>comments</span><span>.</span><span>items</span><span>():</span><span>results</span> <span>=</span> <span>self</span><span>.</span><span>regex_pattern</span><span>.</span><span>match</span><span>(</span><span>comment_body</span><span>)</span><span>if</span> <span>results</span><span>:</span><span>logger</span><span>.</span><span>info</span><span>(</span><span>"Got comment match on pattern!"</span><span>,</span> <span>comment_id</span><span>=</span><span>comment_id</span><span>)</span><span>if</span> <span>self</span><span>.</span><span>result_groups_variable</span><span>:</span><span>setattr</span><span>(</span><span>self</span><span>.</span><span>quest</span><span>.</span><span>quest_data</span><span>,</span><span>self</span><span>.</span><span>result_groups_variable</span><span>,</span><span>results</span><span>.</span><span>groups</span><span>(),</span><span>)</span><span>if</span> <span>self</span><span>.</span><span>result_id_variable</span><span>:</span><span>setattr</span><span>(</span><span>self</span><span>.</span><span>quest</span><span>.</span><span>quest_data</span><span>,</span> <span>self</span><span>.</span><span>result_id_variable</span><span>,</span> <span>comment_id</span><span>)</span><span>return</span> <span>True</span><span># issue incorrect response </span> <span>if</span> <span>len</span><span>(</span><span>comments</span><span>)</span> <span>and</span> <span>self</span><span>.</span><span>incorrect_responses</span><span>:</span><span>comment_id</span> <span>=</span> <span>self</span><span>.</span><span>character</span><span>.</span><span>issue_comment_create</span><span>(</span><span>fork_url</span><span>,</span> <span>issue_id</span><span>,</span> <span>random</span><span>.</span><span>choice</span><span>(</span><span>self</span><span>.</span><span>incorrect_responses</span><span>)</span><span>)</span><span>return</span> <span>False</span><span>class</span> <span>CheckIssueCommentReply</span><span>(</span><span>Stage</span><span>):</span> <span>""" Check issues for reply """</span> <span>@</span><span>property</span> <span>@</span><span>abstractmethod</span> <span>def</span> <span>character</span><span>(</span><span>cls</span><span>)</span> <span>-></span> <span>Character</span><span>:</span> <span>""" Which character will do the check and reply, character needs permission for the repo """</span> <span>return</span> <span>NotImplemented</span> <span>@</span><span>property</span> <span>@</span><span>abstractmethod</span> <span>def</span> <span>regex_pattern</span><span>(</span><span>cls</span><span>)</span> <span>-></span> <span>Pattern</span><span>:</span> <span>""" Compiled regex pattern using re.compile() """</span> <span>return</span> <span>NotImplemented</span> <span>@</span><span>property</span> <span>@</span><span>abstractmethod</span> <span>def</span> <span>issue_id_variable</span><span>(</span><span>cls</span><span>)</span> <span>-></span> <span>int</span><span>:</span> <span>""" Variable containing the ID of the issue to use """</span> <span>return</span> <span>NotImplemented</span> <span># A list of possible responses </span> <span>incorrect_responses</span><span>:</span> <span>List</span><span>[</span><span>str</span><span>]</span> <span>=</span> <span>[]</span> <span># variable to store matching group values in </span> <span>result_groups_variable</span><span>:</span> <span>Optional</span><span>[</span><span>str</span><span>]</span> <span>=</span> <span>None</span> <span># variable to store matching id in in </span> <span>result_id_variable</span><span>:</span> <span>Optional</span><span>[</span><span>str</span><span>]</span> <span>=</span> <span>None</span> <span># variable to get check since from </span> <span>comment_datetime_variable</span><span>:</span> <span>Optional</span><span>[</span><span>str</span><span>]</span> <span>=</span> <span>None</span> <span>def</span> <span>fast_condition</span><span>(</span><span>self</span><span>)</span> <span>-></span> <span>bool</span><span>:</span> <span>"""If hasn't been run before, run once, otherwise fail to avoid hitting github API too much, letting notification scan process run this quest when it receives a notification"""</span> <span>if</span> <span>self</span><span>.</span><span>get_stage_data</span><span>()</span> <span>is</span> <span>None</span><span>:</span> <span>return</span> <span>self</span><span>.</span><span>condition</span><span>()</span> <span>return</span> <span>False</span> <span>def</span> <span>condition</span><span>(</span><span>self</span><span>)</span> <span>-></span> <span>bool</span><span>:</span> <span>""" Check messages """</span> <span>game</span> <span>=</span> <span>self</span><span>.</span><span>quest</span><span>.</span><span>quest_page</span><span>.</span><span>game</span> <span>game</span><span>.</span><span>load</span><span>()</span> <span>fork_url</span> <span>=</span> <span>game</span><span>.</span><span>data</span><span>.</span><span>fork_url</span> <span>user</span> <span>=</span> <span>game</span><span>.</span><span>parent</span> <span>user</span><span>.</span><span>load</span><span>()</span> <span>user_id</span> <span>=</span> <span>user</span><span>.</span><span>data</span><span>.</span><span>id</span> <span>issue_id</span> <span>=</span> <span>getattr</span><span>(</span><span>self</span><span>.</span><span>quest</span><span>.</span><span>quest_data</span><span>,</span> <span>self</span><span>.</span><span>issue_id_variable</span><span>)</span> <span># use either last runtime (saved in stage data), or otherwise last comment datetime variable provided </span> <span>check_datetime</span> <span>=</span> <span>datetime</span><span>.</span><span>utcfromtimestamp</span><span>(</span><span>self</span><span>.</span><span>get_stage_data</span><span>(</span><span>0</span><span>))</span> <span>if</span> <span>self</span><span>.</span><span>comment_datetime_variable</span> <span>is</span> <span>not</span> <span>None</span><span>:</span> <span>check_datetime</span> <span>=</span> <span>max</span><span>(</span> <span>check_datetime</span><span>,</span> <span>getattr</span><span>(</span><span>self</span><span>.</span><span>quest</span><span>.</span><span>quest_data</span><span>,</span> <span>self</span><span>.</span><span>comment_datetime_variable</span><span>),</span> <span>)</span> <span>logger</span><span>.</span><span>info</span><span>(</span> <span>"Fetching comments"</span><span>,</span> <span>user_id</span><span>=</span><span>user_id</span><span>,</span> <span>issue_id</span><span>=</span><span>issue_id</span><span>,</span> <span>fork_url</span><span>=</span><span>fork_url</span><span>,</span> <span>check_datetime</span><span>=</span><span>check_datetime</span><span>,</span> <span>)</span> <span>if</span> <span>check_datetime</span> <span>is</span> <span>None</span><span>:</span> <span>comments</span> <span>=</span> <span>self</span><span>.</span><span>character</span><span>.</span><span>issue_comment_get_from_user</span><span>(</span> <span>fork_url</span><span>,</span> <span>issue_id</span><span>,</span> <span>user_id</span> <span>)</span> <span>else</span><span>:</span> <span>comments</span> <span>=</span> <span>self</span><span>.</span><span>character</span><span>.</span><span>issue_comment_get_from_user_since</span><span>(</span> <span>fork_url</span><span>,</span> <span>issue_id</span><span>,</span> <span>user_id</span><span>,</span> <span>check_datetime</span> <span>)</span> <span>logger</span><span>.</span><span>info</span><span>(</span><span>"Got comments"</span><span>,</span> <span>count</span><span>=</span><span>len</span><span>(</span><span>comments</span><span>))</span> <span>self</span><span>.</span><span>set_stage_data</span><span>(</span><span>datetime</span><span>.</span><span>now</span><span>().</span><span>timestamp</span><span>())</span> <span>for</span> <span>comment_id</span><span>,</span> <span>comment_body</span> <span>in</span> <span>comments</span><span>.</span><span>items</span><span>():</span> <span>results</span> <span>=</span> <span>self</span><span>.</span><span>regex_pattern</span><span>.</span><span>match</span><span>(</span><span>comment_body</span><span>)</span> <span>if</span> <span>results</span><span>:</span> <span>logger</span><span>.</span><span>info</span><span>(</span><span>"Got comment match on pattern!"</span><span>,</span> <span>comment_id</span><span>=</span><span>comment_id</span><span>)</span> <span>if</span> <span>self</span><span>.</span><span>result_groups_variable</span><span>:</span> <span>setattr</span><span>(</span> <span>self</span><span>.</span><span>quest</span><span>.</span><span>quest_data</span><span>,</span> <span>self</span><span>.</span><span>result_groups_variable</span><span>,</span> <span>results</span><span>.</span><span>groups</span><span>(),</span> <span>)</span> <span>if</span> <span>self</span><span>.</span><span>result_id_variable</span><span>:</span> <span>setattr</span><span>(</span><span>self</span><span>.</span><span>quest</span><span>.</span><span>quest_data</span><span>,</span> <span>self</span><span>.</span><span>result_id_variable</span><span>,</span> <span>comment_id</span><span>)</span> <span>return</span> <span>True</span> <span># issue incorrect response </span> <span>if</span> <span>len</span><span>(</span><span>comments</span><span>)</span> <span>and</span> <span>self</span><span>.</span><span>incorrect_responses</span><span>:</span> <span>comment_id</span> <span>=</span> <span>self</span><span>.</span><span>character</span><span>.</span><span>issue_comment_create</span><span>(</span> <span>fork_url</span><span>,</span> <span>issue_id</span><span>,</span> <span>random</span><span>.</span><span>choice</span><span>(</span><span>self</span><span>.</span><span>incorrect_responses</span><span>)</span> <span>)</span> <span>return</span> <span>False</span>class CheckIssueCommentReply(Stage): """ Check issues for reply """ @property @abstractmethod def character(cls) -> Character: """ Which character will do the check and reply, character needs permission for the repo """ return NotImplemented @property @abstractmethod def regex_pattern(cls) -> Pattern: """ Compiled regex pattern using re.compile() """ return NotImplemented @property @abstractmethod def issue_id_variable(cls) -> int: """ Variable containing the ID of the issue to use """ return NotImplemented # A list of possible responses incorrect_responses: List[str] = [] # variable to store matching group values in result_groups_variable: Optional[str] = None # variable to store matching id in in result_id_variable: Optional[str] = None # variable to get check since from comment_datetime_variable: Optional[str] = None def fast_condition(self) -> bool: """If hasn't been run before, run once, otherwise fail to avoid hitting github API too much, letting notification scan process run this quest when it receives a notification""" if self.get_stage_data() is None: return self.condition() return False def condition(self) -> bool: """ Check messages """ game = self.quest.quest_page.game game.load() fork_url = game.data.fork_url user = game.parent user.load() user_id = user.data.id issue_id = getattr(self.quest.quest_data, self.issue_id_variable) # use either last runtime (saved in stage data), or otherwise last comment datetime variable provided check_datetime = datetime.utcfromtimestamp(self.get_stage_data(0)) if self.comment_datetime_variable is not None: check_datetime = max( check_datetime, getattr(self.quest.quest_data, self.comment_datetime_variable), ) logger.info( "Fetching comments", user_id=user_id, issue_id=issue_id, fork_url=fork_url, check_datetime=check_datetime, ) if check_datetime is None: comments = self.character.issue_comment_get_from_user( fork_url, issue_id, user_id ) else: comments = self.character.issue_comment_get_from_user_since( fork_url, issue_id, user_id, check_datetime ) logger.info("Got comments", count=len(comments)) self.set_stage_data(datetime.now().timestamp()) for comment_id, comment_body in comments.items(): results = self.regex_pattern.match(comment_body) if results: logger.info("Got comment match on pattern!", comment_id=comment_id) if self.result_groups_variable: setattr( self.quest.quest_data, self.result_groups_variable, results.groups(), ) if self.result_id_variable: setattr(self.quest.quest_data, self.result_id_variable, comment_id) return True # issue incorrect response if len(comments) and self.incorrect_responses: comment_id = self.character.issue_comment_create( fork_url, issue_id, random.choice(self.incorrect_responses) ) return False
Enter fullscreen mode Exit fullscreen mode
At the core of this is the regex_pattern
. This stage will simply run the provided regex pattern against all comments seen since the last check or the last post date provided, and when it matches, the condition passes.
Some of the code also deals with minimizing how many comments need to be checked, by only searching from the date this stage was last run (using the stage data store facility), or the last post date provided from the quest data store.
There’s also some extra code that relate to capturing the result. This could be useful if we ask the player a question like “which do you pick?” and want to store the result. We can use this to check for a reply and then store the matched group in the quest data storage.
Finally, we can also randomise the response when there’s a comment that doesn’t match. So the usage looks like this:
<span>class</span> <span>CheckNumber2</span><span>(</span><span>CheckIssueCommentReply</span><span>):</span><span>children</span> <span>=</span> <span>[</span><span>"ReplyGroup2"</span><span>]</span><span>character</span> <span>=</span> <span>character_zelma</span><span>regex_pattern</span> <span>=</span> <span>re</span><span>.</span><span>compile</span><span>(</span><span>r</span><span>"(?<!\d)2(?!\d)"</span><span>)</span> <span># exactly 42, no digits either side </span> <span>issue_id_variable</span> <span>=</span> <span>"issue_id"</span><span>comment_datetime_variable</span> <span>=</span> <span>"last_comment"</span><span>incorrect_responses</span> <span>=</span> <span>[</span><span>"No, it wasn't that"</span><span>,</span><span>"Try again, that's not it"</span><span>,</span><span>"Oh, we would have known if it was that, try again"</span><span>,</span><span>"It must be something different, try again"</span><span>,</span><span>"Can't have been that, please check again"</span><span>,</span><span>]</span><span>class</span> <span>CheckNumber2</span><span>(</span><span>CheckIssueCommentReply</span><span>):</span> <span>children</span> <span>=</span> <span>[</span><span>"ReplyGroup2"</span><span>]</span> <span>character</span> <span>=</span> <span>character_zelma</span> <span>regex_pattern</span> <span>=</span> <span>re</span><span>.</span><span>compile</span><span>(</span> <span>r</span><span>"(?<!\d)2(?!\d)"</span> <span>)</span> <span># exactly 42, no digits either side </span> <span>issue_id_variable</span> <span>=</span> <span>"issue_id"</span> <span>comment_datetime_variable</span> <span>=</span> <span>"last_comment"</span> <span>incorrect_responses</span> <span>=</span> <span>[</span> <span>"No, it wasn't that"</span><span>,</span> <span>"Try again, that's not it"</span><span>,</span> <span>"Oh, we would have known if it was that, try again"</span><span>,</span> <span>"It must be something different, try again"</span><span>,</span> <span>"Can't have been that, please check again"</span><span>,</span> <span>]</span>class CheckNumber2(CheckIssueCommentReply): children = ["ReplyGroup2"] character = character_zelma regex_pattern = re.compile( r"(?<!\d)2(?!\d)" ) # exactly 42, no digits either side issue_id_variable = "issue_id" comment_datetime_variable = "last_comment" incorrect_responses = [ "No, it wasn't that", "Try again, that's not it", "Oh, we would have known if it was that, try again", "It must be something different, try again", "Can't have been that, please check again", ]
Enter fullscreen mode Exit fullscreen mode
Here, we are looking for the exact number 2
The regex pattern (?<!\d)2(?!\d)
uses negative lookbehind and lookahead to avoid matching the number 2
inside other numbers, for example 12
would not be matched.
The comment_datetime_variable
is a value set by the previous stage; and issue_id_variable
is set by the first stage that creates the issue.
Quest types and further plans?
We now are sufficiently feature-complete to author the first type of quest on LGTM, which I’ll be calling a “Type 1” quest for lack of better terminology. I think we can split the quest types (and this could govern future dev plans) into:
- Type 1: the player looks for a specific information or value in git (e.g. what was the last commit? who made this change?) and respond with a comment in the issue with the value that we can check for
- Type 2: perhaps this will be when we actually ask a player to deal with a Pull Request or merge (e.g. help merge these two branches! Help resolve this merge conflict!) and requires the player to actually deal with merges into a branch. To do this, we’ll need to be able to raise PRs, as well as read files after a commit to see if they got it right
- Type 3: perhaps this will be when we ask the player to actually make commits, eprhaps even branches, raising PRs, and even force-pushes?
LGTM (40 Part Series)
1 LGTM Devlog 0: Teaching Git through playing a game
2 LGTM Devlog 1: New Logo, and jumping the gun by buying a domain
… 36 more parts…
3 LGTM Devlog 2: Design Ideas for the game I
4 LGTM Devlog 3: Design Ideas for the game 2
5 LGTM Devlog 4: Technical Decisions – GitHub API and Python
6 LGTM Devlog 5: Technical Decisions – Serverless Architecture
7 LGTM Devlog 6: Sprint 1 plans
8 LGTM Devlog 7: Creating a Firebase project
9 LGTM Devlog 8: Starting a git repo for an open source project from scratch
10 LGTM Devlog 9: Python Google Cloud Functions with Unit Tests and Linting
11 LGTM Devlog 10: Capturing the GitHub webhook for fork requests
12 LGTM Devlog 11: Writing the Serverless Function for receiving GitHub webhooks with Pydantic validation
13 LGTM Devlog 12: CI/CD with GitHub Actions to run Unit Tests and deploy Firebase Functions
14 LGTM Devlog 13: GitHub Branch Protection and Security
15 LGTM Devlog 14: Sprint 1 Retrospective
16 LGTM Devlog 15: Sprint 2 plans
17 LGTM Devlog 16: A serverless data base access rule conundrum
18 LGTM Devlog 17: Website and GitHub OAuth
19 LGTM Devlog 18: Python Serverless functions using GitHub API to validate users
20 LGTM Devlog 19: Game data/quest storage
21 LGTM Devlog 20: Python Abstract Base Class-based data/quest storage
22 LGTM Devlog 21: Deploying Pub/Sub-triggered Python Google Cloud Functions
23 LGTM Devlog 22: Modularization
24 LGTM Devlog 23: Sprint 2 Retrospective
25 LGTM Devlog 24: Sprint 3 plans
26 LGTM Devlog 25: Some cleanup
27 LGTM Devlog 26: Python Graphlib DAGs for Quest Stages
28 LGTM Devlog 27: Branching quests
29 LGTM Devlog 28: Game event loop using Google Cloud Scheduler and PubSub
30 LGTM Devlog 29: ORM for Firestore and __init__subclass__ dunders and metaclasses
31 LGTM Devlog 30: Sprint 3 Retrospective
32 LGTM Devlog 31: Sprint 4 Plan
33 LGTM Devlog 32: Secrets Management to avoid storing API keys in services
34 LGTM Devlog 33: Using PyGithub to post GitHub issues and comments
35 LGTM Devlog 34: Characters Posting on GitHub Issues
36 LGTM Devlog 35: Responding to the player’s answers on GitHub Issues Comments
37 LGTM Devlog 36: Character profiles! On GitHub
38 LGTM Devlog 37: Sprint 4 Retrospective
39 LGTM Devlog 38: Sprint 5 Plan
40 LGTM Devlog 39: Planning the story
原文链接:LGTM Devlog 35: Responding to the player’s answers on GitHub Issues Comments
暂无评论内容