-
Notifications
You must be signed in to change notification settings - Fork 11
Implement watch_file and watch_repository
#14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2c24746
6098f1a
9d9ea91
549c0fd
c85b6aa
cca9718
0450156
d1cd5c1
7f16629
176864a
e9e6d4a
823d286
b08a489
893aac5
c0c5caa
786fe89
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,8 +6,6 @@ on: | |
| branches: | ||
| - main | ||
| pull_request: | ||
| branches: | ||
| - main | ||
|
|
||
| jobs: | ||
| build: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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 = { | ||
|
|
@@ -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( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be better to use server-side exception indicating
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method is invoked when
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My test code did not fully cover this case.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks. WDYT about |
||
| 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 | ||
hexoul marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 { | ||
|
|
||
| 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__( | ||
hexoul marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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__() | ||
| 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) |
Uh oh!
There was an error while loading. Please reload this page.