Skip to content

Commit db2eec2

Browse files
committed
Understandable file name when initially downloaded (#58)
1 parent 9265a0b commit db2eec2

10 files changed

+11319
-126
lines changed

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@
88
"editor.formatOnSave": true,
99
"python.linting.flake8Args": [
1010
"--max-line-length=119",
11-
"--ignore=E128"
11+
"--ignore=E128,W503"
1212
]
1313
}

qiita_sync/qiita_sync.py

Lines changed: 97 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@
5454
#
5555
# So, at first try "git rev-parse --abbrev-ref HEAD", and if it's 'HEAD', then get "GITHUB_REF"
5656
#
57+
#
58+
# Qiita API
59+
# =========
60+
#
61+
# - Get user info
62+
# curl -sS -H "Authorization: Bearer ${QIITA_ACCESS_TOKEN}" https://qiita.com/api/v2/authenticated_user | python -m json.tool
63+
#
64+
# - Get an article (from page 1 with page size = 1)
65+
# curl -sS -H "Authorization: Bearer ${QIITA_ACCESS_TOKEN}" https://qiita.com/api/v2/authenticated_user/items?page=1\&per_page=1 | python -m json.tool
5766

5867
from __future__ import annotations
5968

@@ -85,6 +94,7 @@
8594
TypeVar,
8695
NamedTuple,
8796
Tuple,
97+
Iterable,
8898
Dict,
8999
Any,
90100
List,
@@ -562,12 +572,12 @@ def fromString(cls, text: str, default_title=DEFAULT_TITLE, default_tags=DEFAULT
562572
"title": default_title,
563573
"tags": default_tags
564574
},
565-
**dict(
566-
map(
567-
lambda tpl: (tpl[0].strip(), tpl[1].strip()),
568-
map(lambda line: line.split(":", 1),
569-
filter(lambda line: re.match(r"^\s*\w+\s*:.*\S", line) is not None,
570-
text.splitlines())))))
575+
**dict(
576+
map(
577+
lambda tpl: (tpl[0].strip(), tpl[1].strip()),
578+
map(lambda line: line.split(":", 1),
579+
filter(lambda line: re.match(r"^\s*\w+\s*:.*\S", line) is not None,
580+
text.splitlines())))))
571581
return cls(data["title"], QiitaTags.fromString(data["tags"]), data.get("id"),
572582
Maybe(data.get("private")).map(str2bool).getOrElse(False))
573583

@@ -579,10 +589,22 @@ def fromApi(cls, item) -> QiitaData:
579589
HEADER_REGEX = re.compile(r"^\s*\<\!\-\-\s(.*?)\s\-\-\>(.*)$", re.MULTILINE | re.DOTALL)
580590

581591

592+
#
593+
# Auxiliary information about Qiita article, which are not necessary when uploading
594+
#
595+
class QiitaArticleAux(NamedTuple):
596+
created_at: datetime
597+
598+
@classmethod
599+
def fromApi(cls, item) -> QiitaArticleAux:
600+
return cls(created_at=get_utc(item["created_at"]))
601+
602+
582603
class QiitaArticle(NamedTuple):
583604
data: QiitaData
584605
body: str
585606
timestamp: datetime
607+
aux: Optional[QiitaArticleAux]
586608

587609
def toApi(self) -> Dict[str, Any]:
588610
return {
@@ -594,7 +616,11 @@ def toApi(self) -> Dict[str, Any]:
594616

595617
@classmethod
596618
def fromApi(cls, item) -> QiitaArticle:
597-
return cls(data=QiitaData.fromApi(item), body=item["body"], timestamp=get_utc(item["updated_at"]))
619+
return cls(
620+
data=QiitaData.fromApi(item),
621+
body=item["body"],
622+
timestamp=get_utc(item["updated_at"]),
623+
aux=QiitaArticleAux.fromApi(item))
598624

599625

600626
class GitHubArticle(NamedTuple):
@@ -726,9 +752,9 @@ def qsync_get_github_article(include_patterns: List[str], exclude_patterns: List
726752
topdir = Path(git_get_topdir())
727753
return [
728754
Path(fp).resolve()
729-
for fp in (functools.reduce(
730-
lambda a, b: a | b, [set(topdir.glob(pattern)) for pattern in include_patterns]) - functools.reduce(
731-
lambda a, b: a | b, [set(topdir.glob(pattern)) for pattern in exclude_patterns]))]
755+
for fp in (functools.reduce(lambda a, b: a | b, [set(topdir.glob(pattern)) for pattern in include_patterns]) -
756+
functools.reduce(lambda a, b: a | b, [set(topdir.glob(pattern)) for pattern in exclude_patterns]))
757+
]
732758

733759

734760
class QiitaSync(NamedTuple):
@@ -788,29 +814,32 @@ def getArticleByPath(self, target: Path) -> Dict[Path, GitHubArticle]:
788814
if str(path).startswith(str(target.resolve()))])
789815

790816
def toGitHubImageLink(self, link: str, article: QiitaArticle, filepath: Path) -> str:
791-
return Maybe(diff_url(link, self.github_url)).filter(lambda x: x != link).map(lambda diff: str(
792-
rel_path(Path(self.git_dir).joinpath(diff), filepath.resolve().parent))).getOrElse(link)
817+
return Maybe(diff_url(link, self.github_url)).filter(lambda x: x != link).map(
818+
lambda diff: str(rel_path(Path(self.git_dir).joinpath(diff),
819+
filepath.resolve().parent))).getOrElse(link)
793820

794-
def toGitHubMarkdownlLink(self, link: str, article: QiitaArticle, filepath: Path) -> str:
821+
def toGitHubMarkdownlLink(self, link: str, article: QiitaArticle, filepath: Path,
822+
extra_finder: Callable[[str], Optional[Path]]) -> str:
795823
return Maybe(diff_url(link, f"{QIITA_URL_PREFIX}{self.qiita_id}/items/")).filter(
796-
lambda x: x != link).map(lambda id: Maybe(self.getFilePathById(id)).map(lambda fp: str(
797-
rel_path(fp, filepath.resolve().parent))).getOrElse(f"{id}.md")).getOrElse(link)
824+
lambda x: x != link).flatMap(lambda id: Maybe(self.getFilePathById(id) or extra_finder(id)).map(
825+
lambda fp: str(rel_path(fp, filepath.resolve().parent)))).getOrElse(link)
798826

799-
def toGitHubArticle(self, article: QiitaArticle, filepath: Path) -> GitHubArticle:
827+
def toGitHubArticle(self, article: QiitaArticle, filepath: Path,
828+
extra_finder: Callable[[str], Optional[Path]] = lambda _: None) -> GitHubArticle:
800829

801830
def to_image_link(text: str, article: QiitaArticle) -> str:
802831
return markdown_replace_image(lambda link: self.toGitHubImageLink(link, article, filepath), text)
803832

804833
def to_md_link(text: str) -> str:
805-
return markdown_replace_link(lambda link: self.toGitHubMarkdownlLink(link, article, filepath), text)
834+
return markdown_replace_link(
835+
lambda link: self.toGitHubMarkdownlLink(link, article, filepath, extra_finder), text)
806836

807837
return GitHubArticle(
808838
data=article.data,
809-
body=to_normalize_body(markdown_replace_text(
810-
lambda text: to_image_link(to_md_link(text), article), article.body)),
839+
body=to_normalize_body(
840+
markdown_replace_text(lambda text: to_image_link(to_md_link(text), article), article.body)),
811841
timestamp=article.timestamp,
812-
filepath=filepath
813-
)
842+
filepath=filepath)
814843

815844
def toQiitaImageLink(self, link: str, article: GitHubArticle) -> str:
816845
return Maybe(link).filterNot(
@@ -832,10 +861,11 @@ def to_md_link(text: str) -> str:
832861
return markdown_replace_link(lambda link: self.toQiitaMarkdownLink(link, article), text)
833862

834863
return QiitaArticle(
835-
data=article.data, body=to_normalize_body(markdown_replace_text(
836-
lambda text: to_image_link(to_md_link(text)), article.body), '\n'),
837-
timestamp=article.timestamp
838-
)
864+
data=article.data,
865+
body=to_normalize_body(
866+
markdown_replace_text(lambda text: to_image_link(to_md_link(text)), article.body), '\n'),
867+
timestamp=article.timestamp,
868+
aux=None)
839869

840870
def download(self, g_atcl: GitHubArticle):
841871
if g_atcl.data.id is not None:
@@ -849,9 +879,9 @@ def upload(self, article: GitHubArticle):
849879
qiita_patch_item(self.caller, article.data.id, self.toQiitaArticle(article).toApi())
850880
else:
851881
Maybe(qiita_post_item(self.caller,
852-
self.toQiitaArticle(article).toApi())).map(QiitaArticle.fromApi).map(
853-
lambda q_atcl: article._replace(
854-
data=q_atcl.data, timestamp=q_atcl.timestamp)).map(qsync_save_github_article)
882+
self.toQiitaArticle(article).toApi())).map(
883+
QiitaArticle.fromApi).map(lambda q_atcl: article._replace(
884+
data=q_atcl.data, timestamp=q_atcl.timestamp)).map(qsync_save_github_article)
855885

856886
def delete(self, article: GitHubArticle):
857887
if article.data.id is not None:
@@ -864,6 +894,9 @@ def delete(self, article: GitHubArticle):
864894
# Qiita Sync CLI
865895
########################################################################
866896

897+
# Regex for tag name that can be used as port of file name
898+
APPLICABLE_TAG_REGEX = re.compile(r"^[\w\-\.]+$", re.ASCII)
899+
867900

868901
class SyncStatus(Enum):
869902
GITHUB_ONLY = 1
@@ -880,19 +913,29 @@ def qsync_save_github_article(g_atcl: GitHubArticle):
880913
fp.write(g_atcl.toText())
881914

882915

883-
def qsync_to_github_article(qsync: QiitaSync, q_atcl: QiitaArticle) -> GitHubArticle:
884-
return qsync.toGitHubArticle(q_atcl, Path(qsync.git_dir).joinpath(f"{q_atcl.data.id or 'unknown'}.md"))
916+
def qsync_temporary_file_name(q_atcl: QiitaArticle) -> str:
917+
return '_'.join(list(filter(None,
918+
[Maybe(q_atcl.aux).map(lambda aux: aux.created_at.strftime('%Y-%m-%d')).get()]
919+
+ list(filter(None, map(lambda tag: tag.name if APPLICABLE_TAG_REGEX.match(tag.name) else None,
920+
q_atcl.data.tags)))
921+
+ [q_atcl.data.id or "unknown"]
922+
))) + ".md"
923+
924+
925+
def qsync_to_github_article(qsync: QiitaSync, q_atcl: QiitaArticle,
926+
extra_finder: Callable[[str], Optional[Path]]) -> GitHubArticle:
927+
return qsync.toGitHubArticle(q_atcl,
928+
Path(qsync.git_dir).joinpath(qsync_temporary_file_name(q_atcl)), extra_finder)
885929

886930

887931
def qsync_get_sync_status(
888-
qsync: QiitaSync, g_atcl: GitHubArticle,
889-
get_qiita_article: Callable[[str], Optional[QiitaArticle]]
890-
) -> Tuple[SyncStatus, Optional[GitHubArticle]]:
932+
qsync: QiitaSync, g_atcl: GitHubArticle,
933+
get_qiita_article: Callable[[str], Optional[QiitaArticle]]) -> Tuple[SyncStatus, Optional[GitHubArticle]]:
891934
if g_atcl.data.id is None:
892935
return (SyncStatus.GITHUB_ONLY, None)
893936
else:
894-
lq_atcl = Maybe(get_qiita_article(g_atcl.data.id)).map(
895-
lambda q_atcl: qsync.toGitHubArticle(q_atcl, g_atcl.filepath)).get()
937+
lq_atcl = Maybe(get_qiita_article(
938+
g_atcl.data.id)).map(lambda q_atcl: qsync.toGitHubArticle(q_atcl, g_atcl.filepath)).get()
896939
if lq_atcl is None:
897940
return (SyncStatus.QIITA_DELETED, None)
898941
elif g_atcl == lq_atcl:
@@ -965,23 +1008,27 @@ def qsync_str_conflict(article: GitHubArticle) -> str:
9651008
return f'{article.data.title} => Conflict'
9661009

9671010

968-
def qsync_do_check(qsync: QiitaSync, status: SyncStatus, g_atcl: GitHubArticle, lq_atcl: Optional[GitHubArticle], verbose: bool = False):
1011+
def qsync_do_check(qsync: QiitaSync,
1012+
status: SyncStatus,
1013+
g_atcl: GitHubArticle,
1014+
lq_atcl: Optional[GitHubArticle],
1015+
verbose: bool = False):
9691016
if verbose:
9701017
print("======================================================================================")
9711018
if status == SyncStatus.GITHUB_ONLY:
9721019
print(qsync_str_local_only(g_atcl))
9731020
elif status == SyncStatus.QIITA_ONLY and lq_atcl is not None:
9741021
print(qsync_str_global_only(lq_atcl))
975-
elif status == SyncStatus.GITHUB_NEW and lq_atcl is not None:
976-
print(qsync_str_local_new(g_atcl))
1022+
elif status == SyncStatus.GITHUB_NEW and lq_atcl is not None:
1023+
print(qsync_str_local_new(g_atcl))
9771024
print(os.linesep.join(qsync_str_diff(g_atcl, lq_atcl)))
978-
elif status == SyncStatus.QIITA_NEW and lq_atcl is not None:
1025+
elif status == SyncStatus.QIITA_NEW and lq_atcl is not None:
9791026
print(qsync_str_global_new(lq_atcl))
9801027
print(os.linesep.join(qsync_str_diff(g_atcl, lq_atcl)))
981-
elif status == SyncStatus.QIITA_DELETED:
1028+
elif status == SyncStatus.QIITA_DELETED:
9821029
print(qsync_str_global_deleted(g_atcl))
9831030
elif status == SyncStatus.SYNC:
984-
if verbose and lq_atcl is not None:
1031+
if verbose and lq_atcl is not None:
9851032
print(qsync_str_sync(g_atcl))
9861033
print(f"GitHub timestamp: {qsync_str_timestamp(g_atcl)}")
9871034
print(f"Qiita timestamp: {qsync_str_timestamp(lq_atcl)}")
@@ -1033,11 +1080,8 @@ def qsync_do_prune(qsync: QiitaSync, status: SyncStatus, g_atcl: GitHubArticle,
10331080
raise ApplicationError(f"{g_atcl.filepath}: Unknown status")
10341081

10351082

1036-
def qsync_traverse(
1037-
qsync: QiitaSync,
1038-
target: Path,
1039-
handler: Callable[[QiitaSync, SyncStatus, GitHubArticle, Optional[GitHubArticle]], Any]
1040-
):
1083+
def qsync_traverse(qsync: QiitaSync, target: Path,
1084+
handler: Callable[[QiitaSync, SyncStatus, GitHubArticle, Optional[GitHubArticle]], Any]):
10411085
if target == Path(qsync.git_dir):
10421086
q_atcl_dict = dict([
10431087
(article.data.id, article)
@@ -1048,16 +1092,18 @@ def qsync_traverse(
10481092
resp = qsync_get_sync_status(qsync, g_atcl, lambda id: q_atcl_dict.get(id))
10491093
handler(qsync, resp[0], g_atcl, resp[1])
10501094
for id, q_atcl in q_atcl_dict.items():
1051-
lq_atcl = qsync_to_github_article(qsync, q_atcl)
1095+
lq_atcl = qsync_to_github_article(qsync, q_atcl, lambda id: Maybe(q_atcl_dict.get(id)).map(
1096+
lambda atcl: Path(qsync.git_dir).joinpath(qsync_temporary_file_name(atcl))).get())
10521097
if qsync.getArticleById(id) is None:
10531098
handler(qsync, SyncStatus.QIITA_ONLY, lq_atcl, lq_atcl)
10541099
else:
1055-
for g_atcl in [article for article in qsync.atcl_path_map.values()
1056-
if article.filepath is not None and is_sub_prefix(article.filepath, target)]:
1100+
for g_atcl in [
1101+
article for article in qsync.atcl_path_map.values()
1102+
if article.filepath is not None and is_sub_prefix(article.filepath, target)
1103+
]:
10571104
try:
10581105
resp = qsync_get_sync_status(
1059-
qsync, g_atcl,
1060-
lambda id: Maybe(qiita_get_item(qsync.caller, id)).map(QiitaArticle.fromApi).get())
1106+
qsync, g_atcl, lambda id: Maybe(qiita_get_item(qsync.caller, id)).map(QiitaArticle.fromApi).get())
10611107
handler(qsync, resp[0], g_atcl, resp[1])
10621108
except ApplicationFileError:
10631109
handler(qsync, SyncStatus.QIITA_DELETED, g_atcl, None)

tests/cassettes/test_qiita_create_caller.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,15 @@ interactions:
2626
Content-Type:
2727
- application/json; charset=utf-8
2828
Date:
29-
- Wed, 19 Jan 2022 01:45:28 GMT
29+
- Fri, 21 Jan 2022 04:17:39 GMT
3030
ETag:
3131
- W/"2aaf04820679c58d926153e93c3c317f"
3232
Rate-Limit:
3333
- '1000'
3434
Rate-Remaining:
3535
- '972'
3636
Rate-Reset:
37-
- '1642559772'
37+
- '1642741683'
3838
Referrer-Policy:
3939
- strict-origin-when-cross-origin
4040
Server:
@@ -54,9 +54,9 @@ interactions:
5454
X-Permitted-Cross-Domain-Policies:
5555
- none
5656
X-Request-Id:
57-
- d21ea347-9dc9-4d29-8620-eefceab08f27
57+
- be34b3a6-d047-4bf4-87cd-1fe5613ba812
5858
X-Runtime:
59-
- '0.249712'
59+
- '0.175251'
6060
X-XSS-Protection:
6161
- 1; mode=block
6262
status:

0 commit comments

Comments
 (0)