1
Fork 0
mirror of https://github.com/RGBCube/GitHubWrapper synced 2025-05-17 22:45:08 +00:00

Adding files to repos

This commit is contained in:
VarMonke 2022-05-11 11:33:22 +05:30
parent 72bd647c9b
commit 3d49dd9246
5 changed files with 102 additions and 37 deletions

View file

@ -34,7 +34,7 @@ P = ParamSpec('P')
class GHClient: class GHClient:
"""The main client, used to start most use-cases. """The main client, used to start most use-cases.
Parameters Parameters
---------- ----------
username: Optional[:class:`str`] username: Optional[:class:`str`]
@ -59,6 +59,7 @@ class GHClient:
token: Optional[:class:`str`] token: Optional[:class:`str`]
The authenticated Client's token, if applicable. The authenticated Client's token, if applicable.
""" """
has_started: bool = False has_started: bool = False
def __init__( def __init__(
@ -109,7 +110,7 @@ class GHClient:
raise Exception('HTTP Session doesn\'t exist') from exc raise Exception('HTTP Session doesn\'t exist') from exc
def __repr__(self) -> str: def __repr__(self) -> str:
return f'<{self.__class__.__name__} has_auth={bool(self.__auth)}>' return f'<Client has_auth={bool(self.__auth)}>'
@overload @overload
def check_limits(self, as_dict: Literal[True] = True) -> Dict[str, Union[str, int]]: def check_limits(self, as_dict: Literal[True] = True) -> Dict[str, Union[str, int]]:
@ -142,7 +143,7 @@ class GHClient:
async def update_auth(self, username: str, token: str) -> None: async def update_auth(self, username: str, token: str) -> None:
"""Allows you to input auth information after instantiating the client. """Allows you to input auth information after instantiating the client.
Parameters Parameters
---------- ----------
username: :class:`str` username: :class:`str`
@ -161,7 +162,7 @@ class GHClient:
async def start(self) -> Self: async def start(self) -> Self:
"""Main entry point to the wrapper, this creates the ClientSession. """Main entry point to the wrapper, this creates the ClientSession.
Parameters Parameters
---------- ----------
""" """
@ -220,7 +221,7 @@ class GHClient:
async def get_user(self, *, user: str) -> User: async def get_user(self, *, user: str) -> User:
""":class:`User`: Fetch a Github user from their username. """:class:`User`: Fetch a Github user from their username.
Parameters Parameters
---------- ----------
user: :class:`str` user: :class:`str`
@ -230,7 +231,7 @@ class GHClient:
async def get_repo(self, *, owner: str, repo: str) -> Repository: async def get_repo(self, *, owner: str, repo: str) -> Repository:
""":class:`Repository`: Fetch a Github repository from it's name. """:class:`Repository`: Fetch a Github repository from it's name.
Parameters Parameters
---------- ----------
owner: :class:`str` owner: :class:`str`
@ -238,11 +239,11 @@ class GHClient:
repo: :class:`str` repo: :class:`str`
The name of the repository to fetch. The name of the repository to fetch.
""" """
return Repository(await self.http.get_repo(owner, repo), self.http) #type: ignore return Repository(await self.http.get_repo(owner, repo), self.http) # type: ignore
async def get_issue(self, *, owner: str, repo: str, issue: int) -> Issue: async def get_issue(self, *, owner: str, repo: str, issue: int) -> Issue:
""":class:`Issue`: Fetch a Github Issue from it's name. """:class:`Issue`: Fetch a Github Issue from it's name.
Parameters Parameters
---------- ----------
owner: :class:`str` owner: :class:`str`
@ -252,7 +253,7 @@ class GHClient:
issue: :class:`int` issue: :class:`int`
The ID of the issue to fetch. The ID of the issue to fetch.
""" """
return Issue(await self.http.get_repo_issue(owner, repo, issue), self.http) #type: ignore #fwiw, this shouldn't error but pyright <3 return Issue(await self.http.get_repo_issue(owner, repo, issue), self.http) # type: ignore #fwiw, this shouldn't error but pyright <3
async def create_repo( async def create_repo(
self, self,
@ -264,7 +265,7 @@ class GHClient:
) -> Repository: ) -> Repository:
"""Creates a Repository with supplied data. """Creates a Repository with supplied data.
Requires API authentication. Requires API authentication.
Parameters Parameters
---------- ----------
name: :class:`str` name: :class:`str`
@ -306,7 +307,7 @@ class GHClient:
async def get_gist(self, gist: str) -> Gist: async def get_gist(self, gist: str) -> Gist:
"""Fetch a Github gist from it's id. """Fetch a Github gist from it's id.
Parameters Parameters
---------- ----------
gist: :class:`str` gist: :class:`str`
@ -318,7 +319,9 @@ class GHClient:
""" """
return Gist(await self.http.get_gist(gist), self.http) return Gist(await self.http.get_gist(gist), self.http)
async def create_gist(self, *, files: List[File], description: str, public: bool) -> Gist: async def create_gist(
self, *, files: List[File], description: str = 'Gist from Github-Api-Wrapper', public: bool = True
) -> Gist:
"""Creates a Gist with the given files, requires authorisation. """Creates a Gist with the given files, requires authorisation.
Parameters Parameters
@ -342,7 +345,7 @@ class GHClient:
async def delete_gist(self, gist: int) -> Optional[str]: async def delete_gist(self, gist: int) -> Optional[str]:
"""Delete a Github gist, requires authorisation. """Delete a Github gist, requires authorisation.
Parameters Parameters
---------- ----------
gist: :class:`int` gist: :class:`int`
@ -356,12 +359,12 @@ class GHClient:
async def get_org(self, org: str) -> Organization: async def get_org(self, org: str) -> Organization:
"""Fetch a Github organization from it's name. """Fetch a Github organization from it's name.
Parameters Parameters
---------- ----------
org: :class:`str` org: :class:`str`
The name of the organization to fetch. The name of the organization to fetch.
Returns Returns
------- -------
:class:`Organization` :class:`Organization`

View file

@ -1,7 +1,7 @@
# == exceptions.py ==# # == exceptions.py ==#
import datetime import datetime
from typing import Tuple from typing import Optional, Tuple
from aiohttp import ClientResponse from aiohttp import ClientResponse
@ -94,7 +94,7 @@ class InvalidAuthCombination(ClientException):
"""Raised when the username and token are both provided.""" """Raised when the username and token are both provided."""
def __init__(self, msg: str): def __init__(self, msg: str):
#msg = 'The username and token cannot be used together.' # msg = 'The username and token cannot be used together.'
super().__init__(msg) super().__init__(msg)
@ -162,3 +162,10 @@ class RepositoryAlreadyExists(ResourceAlreadyExists):
def __init__(self): def __init__(self):
msg = 'The requested repository already exists.' msg = 'The requested repository already exists.'
super().__init__(msg) super().__init__(msg)
class FileAlreadyExists(ResourceAlreadyExists):
def __init__(self, msg: Optional[str] = None):
if msg is None:
msg = 'The requested file already exists.'
super().__init__(msg)

View file

@ -1,20 +1,24 @@
# == http.py ==# # == http.py ==#
from __future__ import annotations from __future__ import annotations
from asyncio.base_subprocess import ReadSubprocessPipeProto
from base64 import b64encode
import json import json
import re import re
from datetime import datetime from datetime import datetime
from types import SimpleNamespace from types import SimpleNamespace
from typing import Any, Dict, NamedTuple, Optional, Type, Tuple, Union, List from typing import Any, Dict, Literal, NamedTuple, Optional, Type, Tuple, Union, List
from typing_extensions import TypeAlias from typing_extensions import TypeAlias, reveal_type
import platform import platform
import aiohttp import aiohttp
from .exceptions import * from .exceptions import *
from .exceptions import GistNotFound, RepositoryAlreadyExists, MissingPermissions from .exceptions import GistNotFound, RepositoryAlreadyExists, MissingPermissions
from .objects import User, Gist, Repository, File from .exceptions import FileAlreadyExists
from .exceptions import ResourceAlreadyExists
from .objects import User, Gist, Repository, File, bytes_to_b64
from .urls import * from .urls import *
from . import __version__ from . import __version__
@ -238,7 +242,7 @@ class http:
result = await self.session.delete(REPO_URL.format(owner, repo_name)) result = await self.session.delete(REPO_URL.format(owner, repo_name))
if 204 <= result.status <= 299: if 204 <= result.status <= 299:
return 'Successfully deleted repository.' return 'Successfully deleted repository.'
if result.status == 403: #type: ignore if result.status == 403: # type: ignore
raise MissingPermissions raise MissingPermissions
raise RepositoryNotFound raise RepositoryNotFound
@ -252,7 +256,7 @@ class http:
raise GistNotFound raise GistNotFound
async def get_org(self, org_name: str) -> Dict[str, Union[str, int]]: async def get_org(self, org_name: str) -> Dict[str, Union[str, int]]:
"""Returns an org's public data in JSON format.""" #type: ignore """Returns an org's public data in JSON format.""" # type: ignore
result = await self.session.get(ORG_URL.format(org_name)) result = await self.session.get(ORG_URL.format(org_name))
if 200 <= result.status <= 299: if 200 <= result.status <= 299:
return await result.json() return await result.json()
@ -300,3 +304,23 @@ class http:
if result.status == 401: if result.status == 401:
raise NoAuthProvided raise NoAuthProvided
raise RepositoryAlreadyExists raise RepositoryAlreadyExists
async def add_file(self, owner: str, repo_name: str, filename: str, content: str, message: str, branch: str):
"""Adds a file to the given repo."""
data = {
'content': bytes_to_b64(content=content),
'message': message,
'branch': branch,
}
result = await self.session.put(ADD_FILE_URL.format(owner, repo_name, filename), data=json.dumps(data))
if 200 <= result.status <= 299:
return await result.json()
if result.status == 401:
raise NoAuthProvided
if result.status == 409:
raise FileAlreadyExists
if result.status == 422:
raise FileAlreadyExists('This file exists, and can only be edited.')
return await result.json(), result.status

View file

@ -1,7 +1,11 @@
# == objects.py ==# # == objects.py ==#
from __future__ import annotations from __future__ import annotations
from base64 import b64encode
import json
from typing import TYPE_CHECKING, Any, Optional, Tuple, Union, Dict, List from typing import TYPE_CHECKING, Any, Literal, Optional, Tuple, Union, Dict, List
import aiohttp
if TYPE_CHECKING: if TYPE_CHECKING:
from .http import http from .http import http
@ -35,8 +39,13 @@ def repr_dt(_datetime: datetime) -> str:
return _datetime.strftime(r'%d-%m-%Y, %H:%M:%S') return _datetime.strftime(r'%d-%m-%Y, %H:%M:%S')
def bytes_to_b64(content) -> str:
return b64encode(content.encode('utf-8')).decode('ascii')
class APIObject: class APIObject:
"""Top level class for objects created from the API""" """Top level class for objects created from the API"""
__slots__: Tuple[str, ...] = ('_response', '_http') __slots__: Tuple[str, ...] = ('_response', '_http')
def __init__(self, response: Dict[str, Any], _http: http) -> None: def __init__(self, response: Dict[str, Any], _http: http) -> None:
@ -88,7 +97,7 @@ class _BaseUser(APIObject):
class User(_BaseUser): class User(_BaseUser):
"""Representation of a user object on Github. """Representation of a user object on Github.
Attributes Attributes
---------- ----------
login: :class:`str` login: :class:`str`
@ -102,6 +111,7 @@ class User(_BaseUser):
created_at: :class:`datetime.datetime` created_at: :class:`datetime.datetime`
The time of creation of the user. The time of creation of the user.
""" """
__slots__ = ( __slots__ = (
'login', 'login',
'id', 'id',
@ -130,7 +140,6 @@ class User(_BaseUser):
return f'<{self.__class__.__name__} login: {self.login!r}, id: {self.id}, created_at: {self.created_at}>' return f'<{self.__class__.__name__} login: {self.login!r}, id: {self.id}, created_at: {self.created_at}>'
class PartialUser(_BaseUser): class PartialUser(_BaseUser):
__slots__ = ( __slots__ = (
'site_admin', 'site_admin',
@ -158,7 +167,7 @@ class PartialUser(_BaseUser):
class Repository(APIObject): class Repository(APIObject):
"""Representation of a repository on Github. """Representation of a repository on Github.
Attributes Attributes
---------- ----------
id: :class:`int` id: :class:`int`
@ -182,6 +191,7 @@ class Repository(APIObject):
default_branch: :class:`str` default_branch: :class:`str`
The name of the default branch of the repository. The name of the default branch of the repository.
""" """
if TYPE_CHECKING: if TYPE_CHECKING:
id: int id: int
name: str name: str
@ -198,7 +208,6 @@ class Repository(APIObject):
'disabled', 'disabled',
'updated_at', 'updated_at',
'open_issues_count', 'open_issues_count',
'default_branch',
'clone_url', 'clone_url',
'stargazers_count', 'stargazers_count',
'watchers_count', 'watchers_count',
@ -253,14 +262,30 @@ class Repository(APIObject):
def forks(self) -> int: def forks(self) -> int:
return self._response.get('forks') return self._response.get('forks')
@property
def default_branch(self) -> str:
""":class:`str`: The default branch of the repository."""
return self._response.get('default_branch')
async def delete(self) -> None: async def delete(self) -> None:
"""Deletes the repository.""" """Deletes the repository."""
return await self._http.delete_repo(self.owner.name, self.name,) #type: ignore return await self._http.delete_repo(
self.owner.name, # type: ignore this shit is not my fault
self.name,
) # type: ignore
async def add_file(self, filename: str, message: str, content: str, branch: Optional[str] = None) -> None:
"""Adds a file to the repository."""
if branch is None:
branch = self.default_branch
return await self._http.add_file(owner=self.owner.name, repo_name=self.name, filename=filename, content=content, message=message, branch=branch) # type: ignore
class Issue(APIObject): class Issue(APIObject):
"""Representation of an issue on Github. """Representation of an issue on Github.
Attributes Attributes
---------- ----------
id: :class:`int` id: :class:`int`
@ -278,6 +303,7 @@ class Issue(APIObject):
closed_by: Optional[Union[:class:`PartialUser`, :class:`User`]] closed_by: Optional[Union[:class:`PartialUser`, :class:`User`]]
The user the issue was closed by, if applicable. The user the issue was closed by, if applicable.
""" """
__slots__ = ( __slots__ = (
'id', 'id',
'title', 'title',
@ -328,16 +354,17 @@ class Issue(APIObject):
class File: class File:
"""A wrapper around files and in-memory file-like objects. """A wrapper around files and in-memory file-like objects.
Parameters Parameters
---------- ----------
fp: Union[:class:`str`, :class:`io.StringIO`] fp: Union[:class:`str`, :class:`io.StringIO`, :class:`io.BytesIO`]
The filepath or StringIO representing a file to upload. The filepath or StringIO representing a file to upload.
If providing a StringIO instance, a filename shuold also be provided to the file. If providing a StringIO instance, a filename shuold also be provided to the file.
filename: :class:`str` filename: :class:`str`
An override to the file's name, encouraged to provide this if using a StringIO instance. An override to the file's name, encouraged to provide this if using a StringIO instance.
""" """
def __init__(self, fp: Union[str, io.StringIO], filename: str = 'DefaultFilename.txt') -> None:
def __init__(self, fp: Union[str, io.StringIO, io.BytesIO], filename: str = 'DefaultFilename.txt') -> None:
self.fp = fp self.fp = fp
self.filename = filename self.filename = filename
@ -346,12 +373,10 @@ class File:
if os.path.exists(self.fp): if os.path.exists(self.fp):
with open(self.fp) as fp: with open(self.fp) as fp:
data = fp.read() data = fp.read()
return data return data
return self.fp return self.fp
elif isinstance(self.fp, io.BytesIO): elif isinstance(self.fp, io.BytesIO):
return self.fp.read() return self.fp.read().decode('utf-8')
elif isinstance(self.fp, io.StringIO): # type: ignore elif isinstance(self.fp, io.StringIO): # type: ignore
return self.fp.getvalue() return self.fp.getvalue()
@ -360,7 +385,7 @@ class File:
class Gist(APIObject): class Gist(APIObject):
"""Representation of a gist on Github. """Representation of a gist on Github.
Attributes Attributes
---------- ----------
id: :class:`int` id: :class:`int`
@ -376,6 +401,7 @@ class Gist(APIObject):
created_at: :class:`datetime.datetime` created_at: :class:`datetime.datetime`
The time the gist was created at. The time the gist was created at.
""" """
__slots__ = ( __slots__ = (
'id', 'id',
'html_url', 'html_url',
@ -430,7 +456,7 @@ class Gist(APIObject):
class Organization(APIObject): class Organization(APIObject):
"""Representation of an organization in the API. """Representation of an organization in the API.
Attributes Attributes
---------- ----------
login: :class:`str` login: :class:`str`
@ -444,6 +470,7 @@ class Organization(APIObject):
avatar_url: :class:`str` avatar_url: :class:`str`
The url of the organization's avatar. The url of the organization's avatar.
""" """
__slots__ = ( __slots__ = (
'login', 'login',
'id', 'id',

View file

@ -28,6 +28,10 @@ REPOS_URL = BASE_URL + '/repos/{0}' # repos of a user
REPO_URL = BASE_URL + '/repos/{0}/{1}' # a specific repo REPO_URL = BASE_URL + '/repos/{0}/{1}' # a specific repo
ADD_FILE_URL = BASE_URL + '/repos/{}/{}/contents/{}'
ADD_FILE_BRANCH = BASE_URL + ''
REPO_ISSUE_URL = REPO_URL + '/issues/{2}' # a specific issue REPO_ISSUE_URL = REPO_URL + '/issues/{2}' # a specific issue
# == gist urls ==# # == gist urls ==#