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
5867from __future__ import annotations
5968
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:
579589HEADER_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+
582603class 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
600626class 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
734760class 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
868901class 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
887931def 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 )
0 commit comments