From 3d49dd924610a65dcb9f05e769d227a700f47226 Mon Sep 17 00:00:00 2001 From: VarMonke Date: Wed, 11 May 2022 11:33:22 +0530 Subject: [PATCH] Adding files to repos --- github/client.py | 33 +++++++++++++------------ github/exceptions.py | 11 +++++++-- github/http.py | 34 ++++++++++++++++++++++---- github/objects.py | 57 ++++++++++++++++++++++++++++++++------------ github/urls.py | 4 ++++ 5 files changed, 102 insertions(+), 37 deletions(-) diff --git a/github/client.py b/github/client.py index 868147d..feba655 100644 --- a/github/client.py +++ b/github/client.py @@ -34,7 +34,7 @@ P = ParamSpec('P') class GHClient: """The main client, used to start most use-cases. - + Parameters ---------- username: Optional[:class:`str`] @@ -59,6 +59,7 @@ class GHClient: token: Optional[:class:`str`] The authenticated Client's token, if applicable. """ + has_started: bool = False def __init__( @@ -109,7 +110,7 @@ class GHClient: raise Exception('HTTP Session doesn\'t exist') from exc def __repr__(self) -> str: - return f'<{self.__class__.__name__} has_auth={bool(self.__auth)}>' + return f'' @overload 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: """Allows you to input auth information after instantiating the client. - + Parameters ---------- username: :class:`str` @@ -161,7 +162,7 @@ class GHClient: async def start(self) -> Self: """Main entry point to the wrapper, this creates the ClientSession. - + Parameters ---------- """ @@ -220,7 +221,7 @@ class GHClient: async def get_user(self, *, user: str) -> User: """:class:`User`: Fetch a Github user from their username. - + Parameters ---------- user: :class:`str` @@ -230,7 +231,7 @@ class GHClient: async def get_repo(self, *, owner: str, repo: str) -> Repository: """:class:`Repository`: Fetch a Github repository from it's name. - + Parameters ---------- owner: :class:`str` @@ -238,11 +239,11 @@ class GHClient: repo: :class:`str` 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: """:class:`Issue`: Fetch a Github Issue from it's name. - + Parameters ---------- owner: :class:`str` @@ -252,7 +253,7 @@ class GHClient: issue: :class:`int` 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( self, @@ -264,7 +265,7 @@ class GHClient: ) -> Repository: """Creates a Repository with supplied data. Requires API authentication. - + Parameters ---------- name: :class:`str` @@ -306,7 +307,7 @@ class GHClient: async def get_gist(self, gist: str) -> Gist: """Fetch a Github gist from it's id. - + Parameters ---------- gist: :class:`str` @@ -318,7 +319,9 @@ class GHClient: """ 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. Parameters @@ -342,7 +345,7 @@ class GHClient: async def delete_gist(self, gist: int) -> Optional[str]: """Delete a Github gist, requires authorisation. - + Parameters ---------- gist: :class:`int` @@ -356,12 +359,12 @@ class GHClient: async def get_org(self, org: str) -> Organization: """Fetch a Github organization from it's name. - + Parameters ---------- org: :class:`str` The name of the organization to fetch. - + Returns ------- :class:`Organization` diff --git a/github/exceptions.py b/github/exceptions.py index cf5dc0c..a339559 100644 --- a/github/exceptions.py +++ b/github/exceptions.py @@ -1,7 +1,7 @@ # == exceptions.py ==# import datetime -from typing import Tuple +from typing import Optional, Tuple from aiohttp import ClientResponse @@ -94,7 +94,7 @@ class InvalidAuthCombination(ClientException): """Raised when the username and token are both provided.""" 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) @@ -162,3 +162,10 @@ class RepositoryAlreadyExists(ResourceAlreadyExists): def __init__(self): msg = 'The requested repository already exists.' 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) diff --git a/github/http.py b/github/http.py index 8a48fb7..24cb6fb 100644 --- a/github/http.py +++ b/github/http.py @@ -1,20 +1,24 @@ # == http.py ==# from __future__ import annotations +from asyncio.base_subprocess import ReadSubprocessPipeProto +from base64 import b64encode import json import re from datetime import datetime from types import SimpleNamespace -from typing import Any, Dict, NamedTuple, Optional, Type, Tuple, Union, List -from typing_extensions import TypeAlias +from typing import Any, Dict, Literal, NamedTuple, Optional, Type, Tuple, Union, List +from typing_extensions import TypeAlias, reveal_type import platform import aiohttp from .exceptions import * 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 . import __version__ @@ -238,7 +242,7 @@ class http: result = await self.session.delete(REPO_URL.format(owner, repo_name)) if 204 <= result.status <= 299: return 'Successfully deleted repository.' - if result.status == 403: #type: ignore + if result.status == 403: # type: ignore raise MissingPermissions raise RepositoryNotFound @@ -252,7 +256,7 @@ class http: raise GistNotFound 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)) if 200 <= result.status <= 299: return await result.json() @@ -300,3 +304,23 @@ class http: if result.status == 401: raise NoAuthProvided 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 diff --git a/github/objects.py b/github/objects.py index 2079ed1..f774ec7 100644 --- a/github/objects.py +++ b/github/objects.py @@ -1,7 +1,11 @@ # == objects.py ==# 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: from .http import http @@ -35,8 +39,13 @@ def repr_dt(_datetime: datetime) -> str: 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: """Top level class for objects created from the API""" + __slots__: Tuple[str, ...] = ('_response', '_http') def __init__(self, response: Dict[str, Any], _http: http) -> None: @@ -88,7 +97,7 @@ class _BaseUser(APIObject): class User(_BaseUser): """Representation of a user object on Github. - + Attributes ---------- login: :class:`str` @@ -102,6 +111,7 @@ class User(_BaseUser): created_at: :class:`datetime.datetime` The time of creation of the user. """ + __slots__ = ( 'login', '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}>' - class PartialUser(_BaseUser): __slots__ = ( 'site_admin', @@ -158,7 +167,7 @@ class PartialUser(_BaseUser): class Repository(APIObject): """Representation of a repository on Github. - + Attributes ---------- id: :class:`int` @@ -182,6 +191,7 @@ class Repository(APIObject): default_branch: :class:`str` The name of the default branch of the repository. """ + if TYPE_CHECKING: id: int name: str @@ -198,7 +208,6 @@ class Repository(APIObject): 'disabled', 'updated_at', 'open_issues_count', - 'default_branch', 'clone_url', 'stargazers_count', 'watchers_count', @@ -253,14 +262,30 @@ class Repository(APIObject): def forks(self) -> int: 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: """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): """Representation of an issue on Github. - + Attributes ---------- id: :class:`int` @@ -278,6 +303,7 @@ class Issue(APIObject): closed_by: Optional[Union[:class:`PartialUser`, :class:`User`]] The user the issue was closed by, if applicable. """ + __slots__ = ( 'id', 'title', @@ -328,16 +354,17 @@ class Issue(APIObject): class File: """A wrapper around files and in-memory file-like objects. - + 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. If providing a StringIO instance, a filename shuold also be provided to the file. filename: :class:`str` 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.filename = filename @@ -346,12 +373,10 @@ class File: if os.path.exists(self.fp): with open(self.fp) as fp: data = fp.read() - return data - return self.fp 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 return self.fp.getvalue() @@ -360,7 +385,7 @@ class File: class Gist(APIObject): """Representation of a gist on Github. - + Attributes ---------- id: :class:`int` @@ -376,6 +401,7 @@ class Gist(APIObject): created_at: :class:`datetime.datetime` The time the gist was created at. """ + __slots__ = ( 'id', 'html_url', @@ -430,7 +456,7 @@ class Gist(APIObject): class Organization(APIObject): """Representation of an organization in the API. - + Attributes ---------- login: :class:`str` @@ -444,6 +470,7 @@ class Organization(APIObject): avatar_url: :class:`str` The url of the organization's avatar. """ + __slots__ = ( 'login', 'id', diff --git a/github/urls.py b/github/urls.py index 6c18259..7182c58 100644 --- a/github/urls.py +++ b/github/urls.py @@ -28,6 +28,10 @@ REPOS_URL = BASE_URL + '/repos/{0}' # repos of a user 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 # == gist urls ==#