python-github-plus 0.0.1__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,11 @@
1
+ from dotenv import load_dotenv
2
+
3
+ from python_github_plus.github_plus import GitHubPRStatus, GitHubWorkflowStatus, GitHubClient
4
+
5
+ load_dotenv()
6
+
7
+ __all__ = [
8
+ 'GitHubPRStatus',
9
+ 'GitHubWorkflowStatus',
10
+ 'GitHubClient'
11
+ ]
@@ -0,0 +1,302 @@
1
+ import os
2
+ import time
3
+ from enum import Enum
4
+ from typing import Optional, List
5
+
6
+ from github import Github, Auth, GithubException
7
+ from github.GitRef import GitRef
8
+ from github.GitTag import GitTag
9
+ from github.Repository import Repository
10
+ from github.Branch import Branch
11
+ from github.Tag import Tag
12
+ from github.PullRequest import PullRequest
13
+ from github.ContentFile import ContentFile
14
+ from custom_python_logger import get_logger
15
+ from github.Workflow import Workflow
16
+ from github.WorkflowRun import WorkflowRun
17
+
18
+
19
+ class GitHubPRStatus(Enum):
20
+ OPEN = "open"
21
+ CLOSED = "closed"
22
+ MERGED = "merged"
23
+
24
+
25
+ class GitHubWorkflowStatus(Enum):
26
+ COMPLETED = "completed"
27
+ IN_PROGRESS = "in_progress"
28
+ QUEUED = "queued"
29
+
30
+
31
+ class GitHubProjectService:
32
+ def __init__(self, repo: Repository):
33
+ self.logger = get_logger(self.__class__.__name__)
34
+ self.repo = repo
35
+
36
+ def get_info(self) -> Repository:
37
+ return self.repo
38
+
39
+ def list_members(self):
40
+ return list(self.repo.get_collaborators())
41
+
42
+ def add_member(self, username: str, permission: str = "push") -> None:
43
+ try:
44
+ self.repo.add_to_collaborators(username, permission)
45
+ self.logger.info(f"✅ Added member '{username}' with '{permission}' permission.")
46
+ except GithubException as e:
47
+ self.logger.error(f"❌ Failed to add member {username}: {e}")
48
+
49
+ def remove_member(self, username: str) -> None:
50
+ try:
51
+ self.repo.remove_from_collaborators(username)
52
+ self.logger.info(f"✅ Removed member '{username}'.")
53
+ except GithubException as e:
54
+ self.logger.error(f"❌ Failed to remove member {username}: {e}")
55
+
56
+
57
+ class GitHubWorkflowService:
58
+ def __init__(self, repo: Repository):
59
+ self.logger = get_logger(self.__class__.__name__)
60
+ self.repo = repo
61
+
62
+ def list(self) -> list[Workflow]:
63
+ """List workflows configured in the repo (.github/workflows)."""
64
+ return list(self.repo.get_workflows())
65
+
66
+ def trigger(self, workflow_name: str, branch_name: str = "main", inputs: Optional[dict] = None) -> Workflow:
67
+ """
68
+ Dispatch a workflow by name on a given branch.
69
+ Requires 'workflow_dispatch' defined in the workflow YAML.
70
+ """
71
+ workflows = {wf.name: wf for wf in self.repo.get_workflows()}
72
+ if workflow_name not in workflows:
73
+ raise ValueError(f"❌ Workflow '{workflow_name}' not found.")
74
+
75
+ workflow = workflows[workflow_name]
76
+ workflow.create_dispatch(ref=branch_name, inputs=inputs or {})
77
+ self.logger.info(f"🚀 Triggered workflow '{workflow_name}' on branch '{branch_name}'")
78
+
79
+ # Note: This returns immediately; to track status, use wait_until_finished
80
+ return workflow
81
+
82
+ def get_workflow_by_id(self, workflow_id: int) -> Workflow:
83
+ return self.repo.get_workflow(workflow_id)
84
+
85
+ def list_runs(self, workflow_name: str, branch: Optional[str] = None) -> List[WorkflowRun]:
86
+ workflows = {wf.name: wf for wf in self.repo.get_workflows()}
87
+ if workflow_name not in workflows:
88
+ raise ValueError(f"❌ Workflow '{workflow_name}' not found.")
89
+
90
+ runs = workflows[workflow_name].get_runs(branch=branch) if branch else workflows[workflow_name].get_runs()
91
+ return list(runs)
92
+
93
+ def last_run_by_id(self, workflow_id: int) -> WorkflowRun:
94
+ return self.repo.get_workflow(workflow_id).get_runs()[0]
95
+
96
+ def status(self, run_id: int) -> str:
97
+ """ Get the current status of a workflow run. run_id is the ID of the run, not the workflow. """
98
+ run = self.repo.get_workflow_run(run_id)
99
+ return run.status # "queued", "in_progress", "completed"
100
+
101
+ def conclusion(self, run_id: int) -> Optional[str]:
102
+ """ Get the conclusion of a completed workflow run. None if still running. run_id is the ID of the run, not the workflow. """
103
+ run = self.repo.get_workflow_run(run_id)
104
+ return run.conclusion # "success", "failure", "cancelled", None if still running
105
+
106
+ def wait_until_finished(self, run_id: int, check_interval=10, timeout: Optional[int] = None) -> str:
107
+ start_time = time.time()
108
+ while True:
109
+ run = self.repo.get_workflow_run(run_id)
110
+
111
+ if run.status == "completed":
112
+ self.logger.info(f"✅ Workflow run {run_id} finished with conclusion = {run.conclusion}")
113
+ return run.conclusion
114
+
115
+ if timeout and (time.time() - start_time) > timeout:
116
+ raise TimeoutError(f"⏰ Workflow run {run_id} did not complete within {timeout} seconds.")
117
+
118
+ self.logger.debug(f"⏳ Workflow run {run_id} still {run.status}...")
119
+ time.sleep(check_interval)
120
+
121
+
122
+ class GitHubBranchService:
123
+ def __init__(self, repo: Repository):
124
+ self.logger = get_logger(self.__class__.__name__)
125
+ self.repo = repo
126
+
127
+ def create(self, branch_name: str, from_branch: str) -> GitRef:
128
+ ref = self.repo.get_git_ref(f"heads/{from_branch}")
129
+ new_ref = self.repo.create_git_ref(ref=f"refs/heads/{branch_name}", sha=ref.object.sha)
130
+ self.logger.info(f"✅ Branch '{branch_name}' created from '{from_branch}'")
131
+ return new_ref
132
+
133
+ def delete(self, branch_name: str) -> None:
134
+ ref = self.repo.get_git_ref(f"heads/{branch_name}")
135
+ ref.delete()
136
+ self.logger.info(f"✅ Branch '{branch_name}' deleted")
137
+
138
+ def list(self) -> list[Branch]:
139
+ return list(self.repo.get_branches())
140
+
141
+ def protect(
142
+ self,
143
+ branch_name: str,
144
+ enforce_admins: bool = True,
145
+ require_pull_request_reviews: bool = True,
146
+ dismiss_stale_reviews: bool = True,
147
+ required_approving_review_count: int = 1,
148
+ require_status_checks: bool = False,
149
+ status_check_contexts: Optional[List[str]] = None,
150
+ ) -> None:
151
+ """
152
+ Protect a branch with common settings.
153
+ """
154
+ branch = self.repo.get_branch(branch_name)
155
+ branch.edit_protection(
156
+ required_approving_review_count=required_approving_review_count,
157
+ enforce_admins=enforce_admins,
158
+ dismiss_stale_reviews=dismiss_stale_reviews,
159
+ require_code_owner_reviews=require_pull_request_reviews,
160
+ contexts=status_check_contexts if require_status_checks else None,
161
+ )
162
+ self.logger.info(f"✅ Branch '{branch_name}' protected")
163
+
164
+ def unprotect(self, branch_name: str) -> None:
165
+ branch = self.repo.get_branch(branch_name)
166
+ branch.remove_protection()
167
+ self.logger.info(f"✅ Branch '{branch_name}' unprotected")
168
+
169
+
170
+ class GitHubTagService:
171
+ def __init__(self, repo: Repository):
172
+ self.logger = get_logger(self.__class__.__name__)
173
+ self.repo = repo
174
+
175
+ def create_by_sha(self, tag_name: str, sha: str, message: Optional[str] = None) -> GitTag:
176
+ tag = self.repo.create_git_tag(tag=tag_name, message=message or tag_name, object="main", type="commit")
177
+ self.repo.create_git_ref(ref=f"refs/tags/{tag_name}", sha=tag.sha)
178
+ self.logger.info(f"✅ Tag '{tag_name}' created at commit {sha}")
179
+ return tag
180
+
181
+ def create_by_branch(self, tag_name: str, from_branch: str, message: Optional[str] = None) -> GitTag:
182
+ branch = self.repo.get_branch(from_branch)
183
+ sha = branch.commit.sha
184
+ return self.create_by_sha(tag_name=tag_name, sha=sha, message=message)
185
+
186
+ def delete(self, tag_name: str) -> None:
187
+ ref = self.repo.get_git_ref(f"tags/{tag_name}")
188
+ ref.delete()
189
+ self.logger.info(f"✅ Tag '{tag_name}' deleted")
190
+
191
+ def list(self) -> list[Tag]:
192
+ return list(self.repo.get_tags())
193
+
194
+
195
+ class GitHubPRService:
196
+ def __init__(self, repo: Repository):
197
+ self.logger = get_logger(self.__class__.__name__)
198
+ self.repo = repo
199
+
200
+ def get_info(self, number: int) -> PullRequest:
201
+ return self.repo.get_pull(number)
202
+
203
+ def create(self, title: str, from_branch: str, target: str) -> PullRequest:
204
+ pr = self.repo.create_pull(title=title, head=from_branch, base=target)
205
+ self.logger.info(f"✅ PR '{title}' created: #{pr.number}")
206
+ return pr
207
+
208
+ def assign(self, pr_number: int, assignees: str) -> None:
209
+ issue = self.repo.get_issue(pr_number) # fetch the same PR as an issue
210
+ issue.add_to_assignees(assignees) # or multiple users
211
+ self.logger.info(f"✅ PR #{pr_number} assigned to {assignees}")
212
+
213
+ def status(self, number: int) -> str:
214
+ pr = self.repo.get_pull(number)
215
+ if pr.merged:
216
+ return GitHubPRStatus.MERGED.value
217
+ return pr.state
218
+
219
+ def merge(self, number: int) -> None:
220
+ pr = self.repo.get_pull(number)
221
+ pr.merge()
222
+ self.logger.info(f"✅ PR #{number} merged.")
223
+
224
+ def close(self, number: int) -> None:
225
+ pr = self.repo.get_pull(number)
226
+ pr.edit(state="closed")
227
+ self.logger.info(f"✅ PR #{number} closed.")
228
+
229
+ def reopen(self, number: int) -> None:
230
+ pr = self.repo.get_pull(number)
231
+ pr.edit(state="open")
232
+ self.logger.info(f"✅ PR #{number} reopened.")
233
+
234
+ def add_comment(self, number: int, comment: str) -> None:
235
+ pr = self.repo.get_pull(number)
236
+ pr.create_issue_comment(comment)
237
+ self.logger.info(f"✅ Comment added to PR #{number}.")
238
+
239
+ def approve(self, number: int, body: str = "LGTM ✅") -> None:
240
+ pr = self.repo.get_pull(number)
241
+ pr.create_review(event="APPROVE", body=body)
242
+ self.logger.info(f"✅ PR #{number} approved.")
243
+
244
+
245
+ class GitHubFileService:
246
+ def __init__(self, repo: Repository):
247
+ self.logger = get_logger(self.__class__.__name__)
248
+ self.repo = repo
249
+
250
+ def get(self, path: str, ref: str = "main") -> ContentFile:
251
+ return self.repo.get_contents(path, ref=ref)
252
+
253
+ def fetch_content(self, file_path: str, ref: str = "main") -> str:
254
+ file = self.repo.get_contents(file_path, ref=ref)
255
+ return file.decoded_content.decode("utf-8")
256
+
257
+ # def update(self, path: str, branch: str, content: str, message: str) -> None:
258
+ # file = self.repo.get_contents(path, ref=branch)
259
+ # self.repo.update_file(path, message, content, file.sha, branch=branch)
260
+ # self.logger.info(f"✅ File '{path}' updated on branch '{branch}'")
261
+ #
262
+ # def create(self, path: str, branch: str, content: str, message: str) -> None:
263
+ # self.repo.create_file(path, message, content, branch=branch)
264
+ # self.logger.info(f"✅ File '{path}' created on branch '{branch}'")
265
+ #
266
+ # def delete(self, path: str, branch: str, message: str) -> None:
267
+ # file = self.repo.get_contents(path, ref=branch)
268
+ # self.repo.delete_file(path, message, file.sha, branch=branch)
269
+ # self.logger.info(f"✅ File '{path}' deleted on branch '{branch}'")
270
+
271
+
272
+ # ----------------------- #
273
+ # Facade / User Interface #
274
+ # ----------------------- #
275
+
276
+ class GitHubClient:
277
+ def __init__(self, access_token: Optional[str], repo_full_name: str):
278
+ self.logger = get_logger(self.__class__.__name__)
279
+ self.github_access_token = access_token or os.environ.get("GITHUB_ACCESS_TOKEN")
280
+
281
+ self.github = Github(auth=Auth.Token(self.github_access_token))
282
+ self.is_connected(raise_if_not_connected=True)
283
+
284
+ repo = self.github.get_repo(repo_full_name)
285
+ self.project = GitHubProjectService(repo)
286
+ self.workflow = GitHubWorkflowService(repo)
287
+ self.branch = GitHubBranchService(repo)
288
+ self.tag = GitHubTagService(repo)
289
+ self.pull_request = GitHubPRService(repo)
290
+ self.file = GitHubFileService(repo)
291
+
292
+ def is_connected(self, raise_if_not_connected: bool = False) -> bool:
293
+ try:
294
+ user = self.github.get_user().login
295
+ self.logger.info(f"✅ Successfully connected to GitHub as {user}")
296
+ return True
297
+ except Exception as e:
298
+ msg = f"❌ Failed to authenticate with GitHub: {e}"
299
+ if raise_if_not_connected:
300
+ raise ValueError(msg)
301
+ self.logger.exception(msg)
302
+ return False
@@ -0,0 +1,257 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-github-plus
3
+ Version: 0.0.1
4
+ Summary: Enhanced Python GitHub client.
5
+ Home-page: https://github.com/aviz92/python-github-plus
6
+ Author: Avi Zaguri
7
+ Author-email:
8
+ Project-URL: Repository, https://github.com/aviz92/python-github-plus
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: setuptools
13
+ Requires-Dist: wheel
14
+ Requires-Dist: python-dotenv
15
+ Requires-Dist: pytest
16
+ Requires-Dist: pygithub
17
+ Requires-Dist: custom-python-logger
18
+ Dynamic: author
19
+ Dynamic: description
20
+ Dynamic: description-content-type
21
+ Dynamic: home-page
22
+ Dynamic: license-file
23
+ Dynamic: project-url
24
+ Dynamic: requires-dist
25
+ Dynamic: requires-python
26
+ Dynamic: summary
27
+
28
+ # Python GitHub Plus
29
+ An enhanced Python client for GitHub that extends the functionality of the official `pygithub` package, providing better error handling, merge request management, branch operations, and more.
30
+
31
+ ---
32
+
33
+ ## Features
34
+ - ✅ Simplified connection to GitHub instances
35
+ - ✅ Robust error handling with comprehensive logging
36
+ - ✅ **Service-based architecture** for organized functionality
37
+ - ✅ **Project management** (members, info, etc.)
38
+ - ✅ **Branch operations** (create, delete, protect, list, etc.)
39
+ - ✅ **Pull Request management** (create, merge, approve, assign, comment, etc.)
40
+ - ✅ **Workflow operations** (trigger, monitor, wait for completion, etc.)
41
+ - ✅ **Tag management** (create, delete, list, etc.)
42
+ - ✅ **File operations** (read, create, update, delete, etc.)
43
+
44
+ ---
45
+
46
+ ## Installation
47
+ ```bash
48
+ pip install python-github-plus
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Configuration
54
+ The package uses environment variables for authentication and configuration:
55
+
56
+ ```bash
57
+ # Required environment variables
58
+ GitHub_ACCESS_TOKEN=your_github_access_token
59
+ GitHub_URL=https://github.com # Your GitHub instance URL (default: github.com)
60
+ ```
61
+
62
+ ## Examples
63
+
64
+ ### Basic Setup and Connection
65
+ ```python
66
+ from python_github_plus import GitHubClient
67
+
68
+ # Initialize GitLab client with service-based architecture
69
+ github_client = GitHubClient(
70
+ access_token="your_access_token", # Note: parameter name changed
71
+ repo_full_name="your-repo-owner/your-repo-name",
72
+ )
73
+
74
+ # Access different services
75
+ project_service = github_client.project
76
+ branch_service = github_client.branch
77
+ pr_service = github_client.pull_request
78
+ workflows_service = github_client.workflows
79
+ tag_service = github_client.tag
80
+ file_service = github_client.file
81
+ ```
82
+
83
+ ### Branch Management
84
+ ```python
85
+ from python_github_plus import GitHubClient
86
+
87
+ github_client = GitHubClient(
88
+ access_token="your_access_token", # Note: parameter name changed
89
+ repo_full_name="your-repo-owner/your-repo-name",
90
+ )
91
+
92
+ # Create a new branch
93
+ branch = github_client.branch.create(
94
+ branch_name="feature/new-feature",
95
+ from_branch="main"
96
+ )
97
+ print(f"Created branch: {branch.ref}")
98
+
99
+ # List branches
100
+ branches = github_client.branch.list()
101
+ for branch in branches:
102
+ print(f"Branch: {branch.name}")
103
+
104
+ # Protect a branch
105
+ github_client.branch.protect("main")
106
+
107
+ # Delete a branch
108
+ github_client.branch.delete("feature/old-feature")
109
+ ```
110
+
111
+ ### Pull Request Management
112
+ ```python
113
+ from python_github_plus import GitHubClient
114
+
115
+ github_client = GitHubClient(
116
+ access_token="your_access_token", # Note: parameter name changed
117
+ repo_full_name="your-repo-owner/your-repo-name",
118
+ )
119
+
120
+ # Create a pull request
121
+ pr = github_client.pull_request.create(
122
+ title="Add new feature",
123
+ from_branch="feature/new-feature",
124
+ target="main"
125
+ )
126
+ print(f"Created PR: !{pr.number}")
127
+
128
+ # Assign PR to a user
129
+ github_client.pull_request.assign(pr.number, "username")
130
+
131
+ # Add a comment
132
+ github_client.pull_request.add_comment(pr.number, "Great work!")
133
+
134
+ # Approve the PR
135
+ github_client.pull_request.approve(pr.number)
136
+
137
+ # Merge the PR
138
+ github_client.pull_request.merge(pr.number)
139
+ ```
140
+
141
+ ### Workflow Operations
142
+
143
+ ```python
144
+ import time
145
+ from python_github_plus import GitHubClient
146
+
147
+ github_client = GitHubClient(
148
+ access_token="your_access_token", # Note: parameter name changed
149
+ repo_full_name="your-repo-owner/your-repo-name",
150
+ )
151
+
152
+ # Trigger a workflows
153
+ workflows = github_client.workflow.trigger(
154
+ workflow_name="CI",
155
+ branch_name="main",
156
+ )
157
+ print(f"Workflow triggered: {workflows.id}")
158
+
159
+ time.sleep(5) # wait a bit for the workflow to register
160
+ run_id = github_client.workflow.last_run_by_id(workflow_id=workflows.id)
161
+
162
+ # Check workflow run status
163
+ status = github_client.workflow.status(run_id=run_id.id)
164
+ print(f"Workflow run status: {status}")
165
+
166
+ # Wait for workflows completion
167
+ final_status = github_client.workflow.wait_until_finished(
168
+ run_id=run_id.id,
169
+ check_interval=30,
170
+ timeout=3600
171
+ )
172
+ print(f"Workflow run completed with status: {final_status}")
173
+ ```
174
+
175
+ ### File Operations
176
+ ```python
177
+ from python_github_plus import GitHubClient
178
+
179
+ github_client = GitHubClient(
180
+ access_token="your_access_token", # Note: parameter name changed
181
+ repo_full_name="your-repo-owner/your-repo-name",
182
+ )
183
+
184
+ # Read file content
185
+ file_content = github_client.file.fetch_content(
186
+ file_path="README.md",
187
+ ref="main"
188
+ )
189
+ print(f"File content: {file_content[:100]}...")
190
+ ```
191
+
192
+ ### Tag Management
193
+ ```python
194
+ from python_github_plus import GitHubClient
195
+
196
+ github_client = GitHubClient(
197
+ access_token="your_access_token", # Note: parameter name changed
198
+ repo_full_name="your-repo-owner/your-repo-name",
199
+ )
200
+
201
+ # Create a tag
202
+ tag = github_client.tag.create_by_branch(
203
+ tag_name="v1.0.0",
204
+ from_branch="main",
205
+ message="Release version 1.0.0"
206
+ )
207
+ print(f"Created tag: {tag.name}")
208
+
209
+ # List tags
210
+ tags = github_client.tag.list()
211
+ for tag in tags:
212
+ print(f"Tag: {tag.name}")
213
+
214
+ # Delete a tag
215
+ github_client.tag.delete("v0.9.0")
216
+ ```
217
+
218
+ ### Project Management
219
+ ```python
220
+ from python_github_plus import GitHubClient
221
+
222
+ github_client = GitHubClient(
223
+ access_token="your_access_token", # Note: parameter name changed
224
+ repo_full_name="your-repo-owner/your-repo-name",
225
+ )
226
+
227
+ # Get project information
228
+ project_info = github_client.project.get_info()
229
+ print(f"Project: {project_info.name}")
230
+ print(f"Description: {project_info.description}")
231
+
232
+ # List project members
233
+ members = github_client.project.list_members()
234
+ for member in members:
235
+ print(f"Member: {member.username}")
236
+
237
+ # Add a member
238
+ github_client.project.add_member("newuser", 30) # 30 = Developer access level
239
+
240
+ # Remove a member
241
+ github_client.project.remove_member("olduser")
242
+ ```
243
+
244
+ ---
245
+
246
+ ## 🤝 Contributing
247
+ If you have a helpful tool, pattern, or improvement to suggest:
248
+ Fork the repo <br>
249
+ Create a new branch <br>
250
+ Submit a pull request <br>
251
+ I welcome additions that promote clean, productive, and maintainable development. <br>
252
+
253
+ ---
254
+
255
+ ## 🙏 Thanks
256
+ Thanks for exploring this repository! <br>
257
+ Happy coding! <br>
@@ -0,0 +1,7 @@
1
+ python_github_plus/__init__.py,sha256=MO6TwnFs1o64c3qmWrnFD85RQZ2yHCMoVblkzTsjnRU,225
2
+ python_github_plus/github_plus.py,sha256=1wfLgbM60HSPkHb6lNU_aS5fSO-LzcUO4LH6otEo30c,12078
3
+ python_github_plus-0.0.1.dist-info/licenses/LICENSE,sha256=cSikHY6SZFsPZSBizCDAJ0-Bjjzxt-JtX6TVbKxwimo,1067
4
+ python_github_plus-0.0.1.dist-info/METADATA,sha256=4LJcwAx0vRd4QQDV7aThFEV04n-scFpH3VoW54DVgz4,6636
5
+ python_github_plus-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ python_github_plus-0.0.1.dist-info/top_level.txt,sha256=lm_rQAamcpxcN00h3Thamd05_z5PdTfy-RVSuCpN_xA,19
7
+ python_github_plus-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Avi Zaguri
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 @@
1
+ python_github_plus