Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/central-dogma.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ on:
branches:
- main
pull_request:
branches:
- main

jobs:
build:
Expand Down
3 changes: 2 additions & 1 deletion centraldogma/base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ def request(
return resp

def _set_request_headers(self, method: str, **kwargs) -> Dict:
kwargs["headers"] = self.patch_headers if method == "patch" else self.headers
default_headers = self.patch_headers if method == "patch" else self.headers
kwargs["headers"] = {**default_headers, **(kwargs.get("headers") or {})}
return kwargs

@staticmethod
Expand Down
94 changes: 93 additions & 1 deletion centraldogma/content_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,17 @@
from dataclasses import asdict
from enum import Enum
from http import HTTPStatus
from typing import List, Optional
from typing import List, Optional, TypeVar, Any, Callable
from urllib.parse import quote

from httpx import Response

from centraldogma.data.entry import Entry, EntryType
from centraldogma.data.revision import Revision
from centraldogma.exceptions import CentralDogmaException
from centraldogma.query import Query, QueryType

T = TypeVar("T")

from centraldogma.base_client import BaseClient
from centraldogma.data import Content
Expand Down Expand Up @@ -77,6 +87,7 @@ def push(
project_name: str,
repo_name: str,
commit: Commit,
# TODO(ikhoon): Make changes accept varargs?
changes: List[Change],
) -> PushResult:
params = {
Expand All @@ -89,6 +100,87 @@ def push(
handler = {HTTPStatus.OK: lambda resp: PushResult.from_dict(resp.json())}
return self.client.request("post", path, json=params, handler=handler)

def watch_repository(
self,
project_name: str,
repo_name: str,
last_known_revision: Revision,
path_pattern: str,
timeout_millis: int,
) -> Optional[Revision]:
path = f"/projects/{project_name}/repos/{repo_name}/contents"
if path_pattern[0] != "/":
path += "/**/"

if " " in path_pattern:
path_pattern = path_pattern.replace(" ", "%20")
path += path_pattern

handler = {
HTTPStatus.OK: lambda resp: Revision(resp.json()["revision"]),
HTTPStatus.NOT_MODIFIED: lambda resp: None,
}
return self._watch(last_known_revision, timeout_millis, path, handler)

def watch_file(
self,
project_name: str,
repo_name: str,
last_known_revision: Revision,
query: Query[T],
timeout_millis: int,
) -> Optional[Entry[T]]:
path = f"/projects/{project_name}/repos/{repo_name}/contents/{query.path}"
if query.query_type == QueryType.JSON_PATH:
queries = [f"jsonpath={quote(expr)}" for expr in query.expressions]
path = f"{path}?{'&'.join(queries)}"

def on_ok(response: Response) -> Entry:
json = response.json()
revision = Revision(json["revision"])
return self._to_entry(revision, json["entry"], query.query_type)

handler = {HTTPStatus.OK: on_ok, HTTPStatus.NOT_MODIFIED: lambda resp: None}
return self._watch(last_known_revision, timeout_millis, path, handler)

@staticmethod
def _to_entry(revision: Revision, json: Any, query_type: QueryType) -> Entry:
entry_path = json["path"]
received_entry_type = EntryType[json["type"]]
content = json["content"]
if query_type == QueryType.IDENTITY_TEXT:
return Entry.text(revision, entry_path, content)
elif query_type == QueryType.IDENTITY_JSON or query_type == QueryType.JSON_PATH:
if received_entry_type != EntryType.JSON:
raise CentralDogmaException(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to use server-side exception indicating 500, INTERNAL_SERVER_ERROR instead of CentralDogmaException?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is invoked when 200 OK is received. It makes no sense to use 500 INTERNAL_SERVER_ERROR.

Copy link
Copy Markdown
Collaborator

@hexoul hexoul Nov 18, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it can be caused by server's abnormal behavior? Hmm then how about UnknownException?

Copy link
Copy Markdown
Contributor Author

@ikhoon ikhoon Nov 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My test code did not fully cover this case.
Added a new test case for this. 893aac5 (#14)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. WDYT about UnknownException?

f"invalid entry type. entry type: {received_entry_type} (expected: {query_type})"
)

return Entry.json(revision, entry_path, content)
elif query_type == QueryType.IDENTITY:
if received_entry_type == EntryType.JSON:
return Entry.json(revision, entry_path, content)
elif received_entry_type == EntryType.TEXT:
return Entry.text(revision, entry_path, content)
elif received_entry_type == EntryType.DIRECTORY:
return Entry.directory(revision, entry_path)

def _watch(
self,
last_known_revision: Revision,
timeout_millis: int,
path: str,
handler: dict[int, Callable[[Response], T]],
) -> T:
normalized_timeout = (timeout_millis + 999) // 1000
headers = {
"if-none-match": f"{last_known_revision.major}",
"prefer": f"wait={normalized_timeout}",
}
return self.client.request(
"get", path, handler=handler, headers=headers, timeout=normalized_timeout
)

@staticmethod
def _change_dict(data):
return {
Expand Down
2 changes: 1 addition & 1 deletion centraldogma/data/change.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ class ChangeType(Enum):
class Change:
path: str
type: ChangeType
content: Optional[Union[map, str]] = None
content: Optional[Union[dict, str]] = None
123 changes: 123 additions & 0 deletions centraldogma/data/entry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Copyright 2021 LINE Corporation
#
# LINE Corporation licenses this file to you under the Apache License,
# version 2.0 (the "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at:
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import annotations

import json
from enum import Enum
from typing import TypeVar, Generic, Any

from centraldogma import util
from centraldogma.data.revision import Revision
from centraldogma.exceptions import EntryNoContentException


class EntryType(Enum):
JSON = "JSON"
TEXT = "TEXT"
DIRECTORY = "DIRECTORY"


T = TypeVar("T")


class Entry(Generic[T]):
"""
A file or a directory in a repository.
"""

@staticmethod
def text(revision: Revision, path: str, content: str) -> Entry[str]:
"""
Returns a newly-created ``Entry`` of a text file.

:param revision: the revision of the text file
:param path: the path of the text file
:param content: the content of the text file
"""
return Entry(revision, path, EntryType.TEXT, content)

@staticmethod
def json(revision: Revision, path: str, content: Any) -> Entry[Any]:
"""
Returns a newly-created ``Entry`` of a JSON file.

:param revision: the revision of the JSON file
:param path: the path of the JSON file
:param content: the content of the JSON file
"""
if type(content) is str:
content = json.loads(content)
return Entry(revision, path, EntryType.JSON, content)

@staticmethod
def directory(revision: Revision, path: str) -> Entry[None]:
"""
Returns a newly-created ``Entry`` of a directory.

:param revision: the revision of the directory
:param path: the path of the directory
"""
return Entry(revision, path, EntryType.DIRECTORY, None)

def __init__(
self, revision: Revision, path: str, entry_type: EntryType, content: T
):
self.revision = revision
self.path = path
self.entry_type = entry_type
self._content = content
self._content_as_text = None

def has_content(self) -> bool:
"""
Returns if this ``Entry`` has content, which is always ``True`` if it's not a directory.
"""
return self.content is not None

@property
def content(self) -> T:
"""
Returns the content.

:exception EntryNoContentException if the content is ``None``
"""
if not self._content:
raise EntryNoContentException(
f"{self.path} (type: {self.entry_type}, revision: {self.revision.major})"
)

return self._content

def content_as_text(self) -> str:
"""
Returns the textual representation of the specified content.

:exception EntryNoContentException if the content is ``None``
"""
if self._content_as_text:
return self._content_as_text

content = self.content
if self.entry_type == EntryType.TEXT:
self._content_as_text = content
else:
self._content_as_text = json.dumps(self.content)

return self._content_as_text

def __str__(self) -> str:
return util.to_string(self)

def __repr__(self) -> str:
return self.__str__()
39 changes: 39 additions & 0 deletions centraldogma/data/revision.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Copyright 2021 LINE Corporation
#
# LINE Corporation licenses this file to you under the Apache License,
# version 2.0 (the "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at:
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import annotations

from dataclasses import dataclass


@dataclass
class Revision:
"""
A revision number of a ``Commit``.
"""

major: int

@staticmethod
def init() -> Revision:
"""Revision ``1``, also known as 'INIT'."""
return _INIT

@staticmethod
def head() -> Revision:
"""Revision ``-1``, also known as 'HEAD'."""
return _HEAD


_INIT = Revision(1)
_HEAD = Revision(-1)
Loading